diff --git a/README.md b/README.md index ebe1637..db77d4c 100644 --- a/README.md +++ b/README.md @@ -60,4 +60,24 @@ ExecStartPre=/path/to/iptables -w -I INPUT -p all -j reaction ExecStopPost=/path/to/iptables -w -D INPUT -p all -j reaction ExecStopPost=/path/to/iptables -w -F reaction ExecStopPost=/path/to/iptables -w -X reaction + +StateDirectory=reaction +WorkingDirectory=/var/lib/reaction ``` +See [reaction.service](./reaction.service) and [reaction.yml](./reaction.yml) for the fully commented examples. + +## documentation + +### configuration reference + + +`cmd`: note that if program is not in environment's `PATH`, the full path to the command should be given. + +`/etc/systemd/system/reaction.service` (again, commented) +```systemd +``` + +### implicit configuration + +the working directory of `reaction` will be used to create and read from the embedded [lmdb](https://www.symas.com/lmdb) database. +if you don't know where to start it, `/var/lib/reaction` should be a sane choice. diff --git a/app/db.go b/app/db.go new file mode 100644 index 0000000..28bfd18 --- /dev/null +++ b/app/db.go @@ -0,0 +1,119 @@ +package app + +import ( + "fmt" + "log" + "runtime" + "time" + + "github.com/bmatsuo/lmdb-go/lmdb" +) + +func numberOfFilters(conf *Conf) int { + n := 0 + for _, s := range conf.Streams { + n += len(s.Filters) + } + return n +} + +type CmdTime struct { + cmd []string + t time.Time +} + +// Remove Cmd if last of its set +type CmdExecuted struct { + filter *Filter + pattern *string + value CmdTime + err chan error +} + +// Append Cmd set +type AppendCmd struct { + filter *Filter + pattern *string + value []CmdTime + err chan error +} + +// Append match, remove old ones and check match number +type AppendMatch struct { + filter *Filter + pattern *string + t time.Time + ret chan struct { + shouldExec bool + err error + } +} + +func databaseHandler(env *lmdb.Env, chCE chan CmdExecuted, chAC chan AppendCmd, chAM chan AppendMatch) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + defer env.Close() + + select { + case ce := <-chCE: + ce = ce + // TODO + case ac := <-chAC: + ac = ac + // TODO + case am := <-chAM: + am = am + // TODO + } +} + +func initDatabase(conf *Conf) (chan CmdExecuted, chan AppendCmd, chan AppendMatch) { + env, err := lmdb.NewEnv() + if err != nil { + log.Fatalln("LMDB.NewEnv failed") + } + + err = env.SetMapSize(1 << 30) + if err != nil { + log.Fatalln("LMDB.SetMapSize failed") + } + + filterNumber := numberOfFilters(conf) + + err = env.SetMaxDBs(filterNumber * 2) + if err != nil { + log.Fatalln("LMDB.SetMaxDBs failed") + } + + matchDBs := make(map[*Filter]lmdb.DBI, filterNumber) + cmdDBs := make(map[*Filter]lmdb.DBI, filterNumber) + + runtime.LockOSThread() + + for _, stream := range conf.Streams { + for _, filter := range stream.Filters { + err = env.UpdateLocked(func(txn *lmdb.Txn) (err error) { + matchDBs[filter], err = txn.CreateDBI(fmt.Sprintln("%s.%s.match", stream.name, filter.name)) + if err != nil { + return err + } + + cmdDBs[filter], err = txn.CreateDBI(fmt.Sprintln("%s.%s.cmd", stream.name, filter.name)) + return err + }) + if err != nil { + log.Fatalln("LMDB.CreateDBI failed") + } + } + } + + runtime.UnlockOSThread() + + chCE := make(chan CmdExecuted) + chAC := make(chan AppendCmd) + chAM := make(chan AppendMatch) + + go databaseHandler(env, chCE, chAC, chAM) + + return chCE, chAC, chAM +} diff --git a/go.mod b/go.mod index 7915659..f77a22d 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module reaction go 1.19 -require gopkg.in/yaml.v3 v3.0.1 // indirect +require ( + github.com/bmatsuo/lmdb-go v1.8.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 4bc0337..d428690 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/bmatsuo/lmdb-go v1.8.0 h1:ohf3Q4xjXZBKh4AayUY4bb2CXuhRAI8BYGlJq08EfNA= +github.com/bmatsuo/lmdb-go v1.8.0/go.mod h1:wWPZmKdOAZsl4qOqkowQ1aCrFie1HU8gWloHMCeAUdM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/reaction.service b/reaction.service new file mode 100644 index 0000000..429a651 --- /dev/null +++ b/reaction.service @@ -0,0 +1,25 @@ +# vim: ft=systemd +[Unit] +WantedBy=multi-user.target + +[Service] +ExecStart=/path/to/reaction -c /etc/reaction.yml + +# Create an iptables chain for reaction +ExecStartPre=/path/to/iptables -w -N reaction +# Set its default to ACCEPT +ExecStartPre=/path/to/iptables -w -A reaction -j ACCEPT +# Insert this chain as the first item of the INPUT chain (for incoming connections) +ExecStartPre=/path/to/iptables -w -I INPUT -p all -j reaction + +# Remove the chain from the INPUT chain +ExecStopPost=/path/to/iptables -w -D INPUT -p all -j reaction +# Empty the chain +ExecStopPost=/path/to/iptables -w -F reaction +# Delete te chain +ExecStopPost=/path/to/iptables -w -X reaction + +# Ask systemd to create /var/lib/reaction (/var/lib/ is implicit) +StateDirectory=reaction +# Start reaction in its state directory +WorkingDirectory=/var/lib/reaction diff --git a/reaction.test.yml b/reaction.test.yml new file mode 100644 index 0000000..2cfbc5b --- /dev/null +++ b/reaction.test.yml @@ -0,0 +1,19 @@ +--- +patterns: + ip: '(([0-9]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})' + +streams: + tailDown: + cmd: [ "sh", "-c", "echo 'found 1.1.1.1' && sleep 2s && echo 'found 1.1.1.2' && sleep 2s && echo 'found 1.1.1.1' && sleep 1s" ] + filters: + findIP: + regex: + - found + retry: 2 + retry-period: 5s + actions: + damn: + cmd: [ "echo", "" ] + sleepdamn: + cmd: [ "echo", "sleep", "" ] + after: 1s diff --git a/reaction.yml b/reaction.yml index b1e9b0f..a1d9979 100644 --- a/reaction.yml +++ b/reaction.yml @@ -1,23 +1,23 @@ --- definitions: - - &iptablesban iptables -I reaction 1 -s -j block - - &iptablesunban iptables -D reaction 1 -s -j block + - &iptablesban [ "iptables" "-w" "-I" "reaction" "1" "-s" "" "-j" "block" ] + - &iptablesunban [ "iptables" "-w" "-D" "reaction" "1" "-s" "" "-j" "block" ] patterns: ip: '(([0-9]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})' streams: - tailDown: - cmd: [ "sh", "-c", "echo 'found 1.1.1.1' && sleep 2s && echo 'found 1.1.1.2' && sleep 2s && echo 'found 1.1.1.1' && sleep 1s" ] + ssh: + cmd: [ "journalctl" "-fu" "sshd.service" ] filters: - findIP: + failedlogin: regex: - - found - retry: 2 - retry-period: 5s + - authentication failure;.*rhost= + retry: 3 + retry-period: 6h actions: - damn: - cmd: [ "echo", "" ] - sleepdamn: - cmd: [ "echo", "sleep", "" ] - after: 1s + ban: + cmd: *iptablesban + unban: + cmd: *iptablesunban + after: 2d