Skip to content

Commit

Permalink
initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
MatejLach committed Aug 28, 2023
0 parents commit f730c61
Show file tree
Hide file tree
Showing 14 changed files with 1,840 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea/
.vscode/
cmd/dynafire/dynafire
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
dynafire - real-time threat detection for any Linux system
=

[Turris Sentinel](https://view.sentinel.turris.cz/?period=1w) is a real-time threat detection & attack prevention system from
the creators of the [Turris](https://www.turris.com/en/) series of open-source routers, however this service is normally only available via the router interface.
This makes it impractical to use the real-time data provided by Turris Sentinel on a VPS for example, which you cannot easily put behind a Turris router hardware.

`dynafire` is a lightweight Linux daemon that lets any Linux system running the industry standard `firewalld` firewall update its firewall rules in real-time based on Sentinel data.

Installation via package managers
-
TODO

Manual installation
-
Because `dynafire` ships as a single binary, it is easy to install it manually on practically any `systemd`-based distro.

Before proceeding please ensure that both `NetworkManager` and `firewalld` are installed and running:

```shell
$ sudo systemctl check NetworkManager
active

$ sudo systemctl check firewalld
active
```

Download the binary:

TODO

Ensure the binary is executable:

`$ chmod +x dynafire`

Copy the binary to your `$PATH`:

`$ sudo cp dynafire /usr/bin/`


Next, download the `systemd` service definition file:

TODO

Building from source
-

TODO

Configuration
-

The `dynafire` configuration file is created upon first launch under `/etc/dynafire/config.json`.
By default, it has the following values:

```json
{
"log_level": "INFO",
"zone_target_policy": "ACCEPT"
}
```

The `log_level` can be set to `DEBUG` (most verbose), `INFO` and `ERROR` (least verbose).

By default, the `dynafire` firewalld zone is set to `ACCEPT` every packet that is NOT on the Turris Sentinel blacklist, so as not to accidentally block legitimate traffic.
However, you can make this stricter by changing the `zone_target_policy` to i.e. `REJECT` or `DROP`, see [firewalld zone options](https://firewalld.org/documentation/zone/options.html) for details.

Contributing
-
Bug reports and pull requests are welcome. Do not hesitate to open a PR / file an issue or a feature request.
99 changes: 99 additions & 0 deletions cmd/dynafire/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package main

import (
"context"
"fmt"
"log/slog"
"os"
"sync"

"github.com/MatejLach/dynafire/firewall/firewalld"
"github.com/MatejLach/dynafire/provider/turris"
)

func main() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))

fwc, err := firewalld.New()
if err != nil {
slog.Error("Initialization failed; host system pre-requisites not met", "details", err)
os.Exit(1)
}

tc, err := turris.NewClient(turris.Url, turris.Port)
if err != nil {
slog.Error("Unable to initialize Turris dynafire client", "details", err)
os.Exit(1)
}

err = tc.Connect()
if err != nil {
slog.Error("Unable to connect to Turris firewall update server", "details", err)
os.Exit(1)
}

wg := sync.WaitGroup{}
sem := make(chan struct{}, 1)

wg.Add(1)
go func() {
defer wg.Done()
tc.RequestMessages(context.Background())
}()

wg.Add(1)
go func() {
defer wg.Done()
for listMsg := range tc.ListChan {
slog.Info(fmt.Sprintf("adding %d IPs to the blacklist", len(listMsg.Blacklist)))
sem <- struct{}{}

err = fwc.ResetFirewallRules()
if err != nil {
slog.Error("unable to clear old IP blacklist", "details", err)
os.Exit(1)
}

err = fwc.BlockIPList(listMsg.Blacklist)
if err != nil {
slog.Error("unable to initialize IP blacklist", "details", err)
os.Exit(1)
}
slog.Info("Starting to process delta updates...")
<-sem
}
}()

wg.Add(1)
go func() {
defer wg.Done()

for deltaMsg := range tc.DeltaChan {
sem <- struct{}{}

// 'positive' operation adds an IP to the blacklist
// 'negative' removes an existing IP from the blacklist
switch deltaMsg.Operation {
case "positive":
err = fwc.BlockIP(deltaMsg.IP)
if err != nil {
slog.Error("unable to blacklist IP", "details", err)
os.Exit(1)
}

slog.Debug("blacklisting", "IP", deltaMsg.IP.String())
case "negative":
err = fwc.UnblockIP(deltaMsg.IP)
if err != nil {
slog.Error("unable to whitelist IP", "details", err)
os.Exit(1)
}

slog.Debug("whitelisting", "IP", deltaMsg.IP.String())
}
<-sem
}
}()

wg.Wait()
}
14 changes: 14 additions & 0 deletions dist/systemd/dynafire.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Unit]
Description=dynafire - real-time threat detection for any Linux system
After=network.target

[Install]
WantedBy=multi-user.target

[Service]
User=root
Type=simple
ExecStart="/usr/bin/dynafire"
TimeoutStopSec=20
KillMode=process
Restart=on-failure
10 changes: 10 additions & 0 deletions firewall/blocker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package firewall

import "net"

type Blocker interface {
BlockIP(address net.IP) error
BlockIPList(blacklist []net.IP) error
UnblockIP(address net.IP) error
ResetFirewallRules() error
}
79 changes: 79 additions & 0 deletions firewall/firewalld/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package firewalld

import (
"encoding/json"
"log/slog"
"os"
"strings"
)

type Config struct {
LogLevel string `json:"log_level"`
ZoneTargetPolicy string `json:"zone_target_policy"`
}

func initConfig() {
slog.Info("No config.json found, creating new config...")
config := Config{
LogLevel: "INFO",
ZoneTargetPolicy: "ACCEPT",
}

jsonBytes, err := json.MarshalIndent(config, "", " ")
if err != nil {
slog.Error("unable to parse config file", "details", err)
os.Exit(1)
}

err = os.MkdirAll("/etc/dynafire", 0775)
if err != nil {
slog.Error("unable to make config directory", "details", err)
os.Exit(1)
}

err = os.WriteFile("/etc/dynafire/config.json", jsonBytes, 0775)
if err != nil {
slog.Error("unable to save default config file", "details", err)
os.Exit(1)
}

slog.Info("New config.json created, feel free to modify the defaults, then restart for your changes to take effect.")
}

func parseConfig() (Config, error) {
var config Config
cfgData, err := os.ReadFile("/etc/dynafire/config.json")
if err != nil {
return Config{}, err
}

err = json.Unmarshal(cfgData, &config)
if err != nil {
return Config{}, err
}

slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: parseLogLevel(config.LogLevel)})))

return config, nil
}

func configExists() bool {
if _, err := os.Stat("/etc/dynafire/config.json"); os.IsNotExist(err) {
return false
}

return true
}

func parseLogLevel(level string) slog.Level {
switch strings.ToUpper(level) {
case "DEBUG":
return slog.LevelDebug
case "INFO":
return slog.LevelInfo
case "ERROR":
return slog.LevelError
default:
return slog.LevelInfo
}
}

0 comments on commit f730c61

Please sign in to comment.