Implement start/stop commands

fix #41
update README and configuration files accordingly
This commit is contained in:
ppom 2023-10-18 12:00:00 +02:00
parent d35167b878
commit 345dd94b17
8 changed files with 127 additions and 77 deletions

View File

@ -27,12 +27,23 @@ see [reaction.service](./config/reaction.service) and [reaction.yml](./app/react
`/etc/reaction.yml` `/etc/reaction.yml`
```yaml ```yaml
definitions: definitions:
- &iptablesban [ "iptables" "-w" "-I" "reaction" "1" "-s" "<ip>" "-j" "block" ] - &iptablesban [ "ip46tables" "-w" "-I" "reaction" "1" "-s" "<ip>" "-j" "block" ]
- &iptablesunban [ "iptables" "-w" "-D" "reaction" "1" "-s" "<ip>" "-j" "block" ] - &iptablesunban [ "ip46tables" "-w" "-D" "reaction" "1" "-s" "<ip>" "-j" "block" ]
patterns: patterns:
ip: '(([ 0-9 ]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})' ip: '(([ 0-9 ]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})'
start:
- [ "ip46tables", "-w", "-N", "reaction" ]
- [ "ip46tables", "-w", "-A", "reaction", "-j", "ACCEPT" ]
- [ "ip46tables", "-w", "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ]
- [ "ip46tables", "-w", "-I", "INPUT", "-p", "all", "-j", "reaction" ]
stop:
- [ "ip46tables", "-w," "-D," "INPUT", "-p", "all", "-j", "reaction" ]
- [ "ip46tables", "-w," "-F," "reaction" ]
- [ "ip46tables", "-w," "-X," "reaction" ]
streams: streams:
ssh: ssh:
cmd: [ "journalctl" "-fu" "sshd.service" ] cmd: [ "journalctl" "-fu" "sshd.service" ]
@ -54,14 +65,26 @@ jsonnet is also supported:
`/etc/reaction.jsonnet` `/etc/reaction.jsonnet`
```jsonnet ```jsonnet
local iptablesban = ['iptables', '-w', '-A', 'reaction', '1', '-s', '<ip>', '-j', 'DROP']; local iptables(args) = [ 'ip46tables', '-w' ] + args;
local iptablesunban = ['iptables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-j', 'DROP']; local iptablesban(ip) = iptables([ '-A', 'reaction', '1', '-s', ip, '-j', 'DROP' ]);
local iptablesunban(ip) = iptables([ '-D', 'reaction', '1', '-s', ip, '-j', 'DROP' ]);
{ {
patterns: { patterns: {
ip: { ip: {
regex: @'(?:(?:[ 0-9 ]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})', regex: @'(?:(?:[ 0-9 ]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})',
}, },
}, },
start: {
iptables([ "-N", "reaction" ]),
iptables([ "-A", "reaction", "-j", "ACCEPT" ]),
iptables([ "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ]),
iptables([ "-I", "INPUT", "-p", "all", "-j", "reaction" ]),
},
stop: {
iptables([ "-D,", "INPUT", "-p", "all", "-j", "reaction" ]),
iptables([ "-F,", "reaction" ]),
iptables([ "-X,", "reaction" ]),
},
streams: { streams: {
ssh: { ssh: {
cmd: [ 'journalctl', '-fu', 'sshd.service' ], cmd: [ 'journalctl', '-fu', 'sshd.service' ],
@ -72,10 +95,10 @@ local iptablesunban = ['iptables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-
retryperiod: '6h', retryperiod: '6h',
actions: { actions: {
ban: { ban: {
cmd: iptablesban, cmd: iptablesban('<ip>'),
}, },
unban: { unban: {
cmd: iptablesunban, cmd: iptablesunban('<ip>'),
after: '48h', after: '48h',
onexit: true, onexit: true,
}, },
@ -97,15 +120,6 @@ WantedBy=multi-user.target
[ Service ] [ Service ]
ExecStart=/path/to/reaction -c /etc/reaction.yml ExecStart=/path/to/reaction -c /etc/reaction.yml
ExecStartPre=/path/to/iptables -w -N reaction
ExecStartPre=/path/to/iptables -w -A reaction -j ACCEPT
ExecStartPre=/path/to/iptables -w -I reaction 1 -s 127.0.0.1 -j ACCEPT
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 StateDirectory=reaction
RuntimeDirectory=reaction RuntimeDirectory=reaction
WorkingDirectory=/var/lib/reaction WorkingDirectory=/var/lib/reaction

View File

@ -39,6 +39,15 @@ func cmdStdout(commandline []string) chan *string {
return lines return lines
} }
func runCommands(commands [][]string, moment string) {
for _, command := range commands {
cmd := exec.Command(command[0], command[1:]...)
if err := cmd.Start(); err != nil {
logger.Printf(logger.ERROR, "couldn't execute %v command: %v", moment, err)
}
}
}
func (p *Pattern) notAnIgnore(match *string) bool { func (p *Pattern) notAnIgnore(match *string) bool {
for _, ignore := range p.Ignore { for _, ignore := range p.Ignore {
if ignore == *match { if ignore == *match {
@ -323,6 +332,8 @@ func Daemon(confFilename string) {
actions = make(ActionsMap) actions = make(ActionsMap)
matches = make(MatchesMap) matches = make(MatchesMap)
runCommands(conf.Start, "start")
go DatabaseManager(conf) go DatabaseManager(conf)
go MatchesManager() go MatchesManager()
go ActionsManager() go ActionsManager()
@ -348,16 +359,16 @@ func Daemon(confFilename string) {
logger.Printf(logger.ERROR, "%s stream finished", finishedStream.name) logger.Printf(logger.ERROR, "%s stream finished", finishedStream.name)
nbStreamsInExecution-- nbStreamsInExecution--
if nbStreamsInExecution == 0 { if nbStreamsInExecution == 0 {
quit() quit(conf)
} }
case <-sigs: case <-sigs:
logger.Printf(logger.INFO, "Received SIGINT/SIGTERM, exiting") logger.Printf(logger.INFO, "Received SIGINT/SIGTERM, exiting")
quit() quit(conf)
} }
} }
} }
func quit() { func quit(conf *Conf) {
// send stop to StreamManager·s // send stop to StreamManager·s
close(stopStreams) close(stopStreams)
logger.Println(logger.INFO, "Waiting for Streams to finish...") logger.Println(logger.INFO, "Waiting for Streams to finish...")
@ -369,6 +380,8 @@ func quit() {
// stop all actions // stop all actions
logger.Println(logger.INFO, "Waiting for Actions to finish...") logger.Println(logger.INFO, "Waiting for Actions to finish...")
wgActions.Wait() wgActions.Wait()
// run stop commands
runCommands(conf.Stop, "stop")
// delete pipe // delete pipe
err := os.Remove(*SocketPath) err := os.Remove(*SocketPath)
if err != nil { if err != nil {

View File

@ -3,8 +3,8 @@
# using YAML anchors `&name` and pointers `*name` # using YAML anchors `&name` and pointers `*name`
# definitions are not readed by reaction # definitions are not readed by reaction
definitions: definitions:
- &iptablesban [ "ip46tables" "-w" "-A" "reaction" "1" "-s" "<ip>" "-j" "DROP" ] - &iptablesban [ "ip46tables", "-w", "-A", "reaction", "1", "-s", "<ip>", "-j", "DROP" ]
- &iptablesunban [ "ip46tables" "-w" "-D" "reaction" "1" "-s" "<ip>" "-j" "DROP" ] - &iptablesunban [ "ip46tables", "-w", "-D", "reaction", "1", "-s", "<ip>", "-j", "DROP" ]
# ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory. # ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory.
# it permits to handle both ipv4/iptables and ipv6/ip6tables commands # it permits to handle both ipv4/iptables and ipv6/ip6tables commands
@ -18,6 +18,20 @@ patterns:
- 127.0.0.1 - 127.0.0.1
- ::1 - ::1
# Those commands will be executed in order at start, before everything else
start:
- [ "ip46tables", "-w", "-N", "reaction" ]
- [ "ip46tables", "-w", "-A", "reaction", "-j", "ACCEPT" ]
- [ "ip46tables", "-w", "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ]
- [ "ip46tables", "-w", "-I", "INPUT", "-p", "all", "-j", "reaction" ]
# Those commands will be executed in order at stop, after everything else
stop:
- [ "ip46tables", "-w,", "-D", "INPUT", "-p", "all", "-j", "reaction" ]
- [ "ip46tables", "-w", "-F", "reaction" ]
- [ "ip46tables", "-w", "-X", "reaction" ]
# streams are commands # streams are commands
# they're run and their ouptut is captured # they're run and their ouptut is captured
# *example:* `tail -f /var/log/nginx/access.log` # *example:* `tail -f /var/log/nginx/access.log`
@ -27,7 +41,7 @@ streams:
ssh: ssh:
# note that if the command is not in environment's `PATH` # note that if the command is not in environment's `PATH`
# its full path must be given. # its full path must be given.
cmd: [ "journalctl" "-n0" "-fu" "sshd.service" ] cmd: [ "journalctl", "-n0", "-fu", "sshd.service" ]
# filters run actions when they match regexes on a stream # filters run actions when they match regexes on a stream
filters: filters:
# filters have a user-defined name # filters have a user-defined name

View File

@ -10,6 +10,8 @@ import (
type Conf struct { type Conf struct {
Patterns map[string]*Pattern `json:"patterns"` Patterns map[string]*Pattern `json:"patterns"`
Streams map[string]*Stream `json:"streams"` Streams map[string]*Stream `json:"streams"`
Start [][]string `json:"start"`
Stop [][]string `json:"stop"`
} }
type Pattern struct { type Pattern struct {

View File

@ -1,8 +1,6 @@
local directory = '~/.local/share/watch';
// Those strings will be substitued in each shell() call // Those strings will be substitued in each shell() call
local substitutions = [ local substitutions = [
['OUTFILE', directory + '/logs-$(date %+F)'], ['OUTFILE', '"$HOME/.local/share/watch/logs-$(date +%F)"'],
['TMUXFILE', directory + '/tmux'],
['DATE', '"$(date "+%F %T")"'], ['DATE', '"$(date "+%F %T")"'],
]; ];
@ -18,19 +16,17 @@ local shell(prg) = [
sub(prg), sub(prg),
]; ];
{ local log(line) = shell('echo DATE ' + std.strReplace(line, '\n', ' ') + '>> OUTFILE');
// Startup is currently not implemented
startup: shell(|||
mkdir -p "$(dirname OUTFILE)"
echo DATE start >> OUTFILE
# tmux set-hook -g pane-focus-in[50] new-session -d 'echo tmux >> TMUXFILE'
|||),
// Stop is currently not implemented {
stop: shell(||| start: [
tmux set-hook -ug pane-focus-in[50] shell('mkdir -p "$(dirname OUTFILE)"'),
echo DATE stop >> OUTFILE log('start'),
|||), ],
stop: [
log('stop'),
],
patterns: { patterns: {
all: { regex: '.*' }, all: { regex: '.*' },
@ -47,7 +43,7 @@ local shell(prg) = [
send: { send: {
regex: ['^<all>$'], regex: ['^<all>$'],
actions: { actions: {
send: { cmd: shell('echo DATE focus <all> >> OUTFILE') }, send: { cmd: log('focus <all>') },
}, },
}, },
}, },
@ -55,12 +51,13 @@ local shell(prg) = [
// Be notified when user is away // Be notified when user is away
swayidle: { swayidle: {
cmd: ['swayidle', 'timeout', '60', 'echo sleep', 'resume', 'echo resume'], // FIXME echo stop and start instead?
cmd: ['swayidle', 'timeout', '30', 'echo sleep', 'resume', 'echo resume'],
filters: { filters: {
send: { send: {
regex: ['^<all>$'], regex: ['^<all>$'],
actions: { actions: {
send: { cmd: shell('echo DATE <all> >> OUTFILE') }, send: { cmd: log('<all>') },
}, },
}, },
}, },
@ -92,7 +89,7 @@ local shell(prg) = [
// send: { // send: {
// regex: ['^tmux <all>$'], // regex: ['^tmux <all>$'],
// actions: { // actions: {
// send: { cmd: shell('echo DATE tmux <all> >> OUTFILE') }, // send: { cmd: log('tmux <all>') },
// }, // },
// }, // },
// }, // },

View File

@ -1,11 +1,13 @@
// This file is using JSONNET, a complete configuration language based on JSON // This file is using JSONNET, a complete configuration language based on JSON
// See https://jsonnet.org // See https://jsonnet.org
// JSONNET is a superset of JSON, so one can write plain JSON files if wanted. // JSONNET is a superset of JSON, so one can write plain JSON files if wanted.
// Note that YAML is also supported. // Note that YAML is also supported, see ./example.yml
// A JSONNET function
local iptables(args) = ['ip46tables', '-w'] + args;
// variables defined for later use. // variables defined for later use.
local iptablesban = ['ip46tables', '-w', '-A', 'reaction', '1', '-s', '<ip>', '-j', 'DROP']; local iptablesban = iptables(['-A', 'reaction', '1', '-s', '<ip>', '-j', 'drop']);
local iptablesunban = ['ip46tables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-j', 'DROP']; local iptablesunban = iptables(['-D', 'reaction', '1', '-s', '<ip>', '-j', 'drop']);
// ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory. // ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory.
// it permits to handle both ipv4/iptables and ipv6/ip6tables commands // it permits to handle both ipv4/iptables and ipv6/ip6tables commands
@ -21,6 +23,30 @@ local iptablesunban = ['ip46tables', '-w', '-D', 'reaction', '1', '-s', '<ip>',
}, },
}, },
// Those commands will be executed in order at start, before everything else
start: [
// Create an iptables chain for reaction
iptables(['-N', 'reaction']),
// Set its default to ACCEPT
iptables(['-A', 'reaction', '-j', 'ACCEPT']),
// Always accept 127.0.0.1
iptables(['-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT']),
// Always accept ::1
iptables(['-I', 'reaction', '1', '-s', '::1', '-j', 'ACCEPT']),
// Insert this chain as the first item of the INPUT chain (for incoming connections)
iptables(['-I', 'INPUT', '-p', 'all', '-j', 'reaction']),
],
// Those commands will be executed in order at stop, after everything else
stop: [
// Remove the chain from the INPUT chain
iptables(['-D,', 'INPUT', '-p', 'all', '-j', 'reaction']),
// Empty the chain
iptables(['-F,', 'reaction']),
// Delete the chain
iptables(['-X,', 'reaction']),
],
// streams are commands // streams are commands
// they're run and their ouptut is captured // they're run and their ouptut is captured
// *example:* `tail -f /var/log/nginx/access.log` // *example:* `tail -f /var/log/nginx/access.log`

View File

@ -6,24 +6,6 @@ WantedBy=multi-user.target
[Service] [Service]
ExecStart=/path/to/reaction -c /etc/reaction.yml ExecStart=/path/to/reaction -c /etc/reaction.yml
# Create an iptables chain for reaction
ExecStartPre=/path/to/ip46tables -w -N reaction
# Set its default to ACCEPT
ExecStartPre=/path/to/ip46tables -w -A reaction -j ACCEPT
# Always accept 127.0.0.1
ExecStartPre=/path/to/ip46tables -w -I reaction 1 -s 127.0.0.1 -j ACCEPT
# Always accept ::1
ExecStartPre=/path/to/ip46tables -w -I reaction 1 -s ::1 -j ACCEPT
# Insert this chain as the first item of the INPUT chain (for incoming connections)
ExecStartPre=/path/to/ip46tables -w -I INPUT -p all -j reaction
# Remove the chain from the INPUT chain
ExecStopPost=/path/to/ip46tables -w -D INPUT -p all -j reaction
# Empty the chain
ExecStopPost=/path/to/ip46tables -w -F reaction
# Delete the chain
ExecStopPost=/path/to/ip46tables -w -X reaction
# Ask systemd to create /var/lib/reaction (/var/lib/ is implicit) # Ask systemd to create /var/lib/reaction (/var/lib/ is implicit)
StateDirectory=reaction StateDirectory=reaction
# Ask systemd to create /run/reaction at runtime (/run/ is implicit) # Ask systemd to create /run/reaction at runtime (/run/ is implicit)

View File

@ -69,8 +69,10 @@ func Printf(level Level, format string, args ...any) {
} }
func Fatalln(args ...any) { func Fatalln(args ...any) {
level := FATAL newargs := make([]any, 0)
log.Fatalln(level.String(), args) newargs = append(newargs, FATAL)
newargs = append(newargs, args...)
log.Fatalln(newargs...)
} }
func Fatalf(format string, args ...any) { func Fatalf(format string, args ...any) {