Compare commits

...

10 Commits

Author SHA1 Message Date
yo
00f1647aa6 Hanle multi-pattern match on a single line of log 2024-02-24 11:01:50 +01:00
ppom
0b4030905b ignoreregex test 2024-02-09 12:00:00 +01:00
Tom Hubrecht
8afa6281f4 Add support for ignore regexes in filters 2024-02-08 13:28:51 +00:00
ppom
c076177d2b release.sh & Makefile: add nft46 2024-02-07 12:00:00 +01:00
ppom
bff697dc17 Add support for nftables 2024-02-05 12:00:00 +01:00
ppom
bfe0128764 Update wiki link to https://reaction.ppom.me 2024-01-17 12:00:00 +01:00
ppom
0a1614ea4d Remove useless line 2024-01-10 12:00:00 +01:00
ppom
03862b33a6 Fix bug introduced by af2f092b. Fix #67 2024-01-10 12:00:00 +01:00
ppom
48fb6a85a5 Fix Makefile and update install doc 2024-01-10 12:00:00 +01:00
Gabriel Moreau
b8032acf97 Update file control 2024-01-10 14:21:51 +00:00
19 changed files with 274 additions and 137 deletions

2
.gitignore vendored
View File

@ -1,5 +1,6 @@
/reaction /reaction
/ip46tables /ip46tables
/nft46
/reaction*.db /reaction*.db
/reaction*.sock /reaction*.sock
/result /result
@ -7,3 +8,4 @@
/deb /deb
*.deb *.deb
*.minisig *.minisig
*.qcow2

View File

@ -2,8 +2,18 @@ Package: reaction
Version: LAST_TAG Version: LAST_TAG
Architecture: amd64 Architecture: amd64
Maintainer: ppom <> Maintainer: ppom <>
Sections: utils Section: utils
Package-Type: deb Package-Type: deb
Priority: Optional Priority: Optional
Homepage: https://framagit.org/ppom/reaction Homepage: https://framagit.org/ppom/reaction
Description: A daemon that scans program outputs for repeated patterns, and takes action. Description: A daemon that scans program outputs for repeated patterns, and takes action
A common use of reaction is to scan ssh and web server logs,
and ban hosts that cause multiple authentication errors.
reaction doesn't have all the features of the honorable fail2ban,
but it's ~10x faster and easier to configure.
Tag: admin::automation, admin::logging, admin::monitoring,
interface::commandline, interface::daemon,
network::firewall, protocol::ip, role::program,
security::authentication, security::firewall, security::ids,
security::log-analyzer, use::login, use::monitor,
works-with-format::plaintext, works-with::logfile, works-with::text

View File

@ -3,22 +3,24 @@ PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin BINDIR = $(PREFIX)/bin
SYSTEMDDIR ?= /etc/systemd SYSTEMDDIR ?= /etc/systemd
all: reaction ip46tables all: reaction ip46tables nft46
clean: clean:
rm -f reaction ip46tables reaction.deb deb reaction.minisig ip46tables.minisig reaction.deb.minisig rm -f reaction ip46tables nft46 reaction.deb deb reaction.minisig ip46tables.minisig reaction.deb.minisig nft46.minisig
ip46tables: ip46tables.d/ip46tables.c ip46tables: helpers_c/ip46tables.c
$(CC) -s -static ip46tables.d/ip46tables.c -o ip46tables $(CC) -s -static helpers_c/ip46tables.c -o ip46tables
nft46: helpers_c/nft46.c
$(CC) -s -static helpers_c/nft46.c -o nft46
reaction: app/* reaction.go go.mod go.sum reaction: app/* reaction.go go.mod go.sum
CGO_ENABLED=0 go build -buildvcs=false -ldflags "-s -X main.version=`git tag --sort=v:refname | tail -n1` -X main.commit=`git rev-parse --short HEAD`" CGO_ENABLED=0 go build -buildvcs=false -ldflags "-s -X main.version=`git tag --sort=v:refname | tail -n1` -X main.commit=`git rev-parse --short HEAD`"
reaction.deb: reaction ip46tables reaction.deb: reaction ip46tables nft46
chmod +x reaction ip46tables chmod +x reaction ip46tables nft46
mkdir -p deb/reaction/usr/bin/ deb/reaction/usr/sbin/ deb/reaction/lib/systemd/system/ mkdir -p deb/reaction/usr/bin/ deb/reaction/usr/sbin/ deb/reaction/lib/systemd/system/
cp reaction deb/reaction/usr/bin/ cp reaction ip46tables nft46 deb/reaction/usr/bin/
cp ip46tables deb/reaction/usr/sbin/
cp config/reaction.debian.service deb/reaction/lib/systemd/system/reaction.service cp config/reaction.debian.service deb/reaction/lib/systemd/system/reaction.service
cp -r DEBIAN/ deb/reaction/DEBIAN cp -r DEBIAN/ deb/reaction/DEBIAN
sed -e "s/LAST_TAG/`git tag --sort=v:refname | tail -n1`/" -e "s/Version: v/Version: /" -i deb/reaction/DEBIAN/* sed -e "s/LAST_TAG/`git tag --sort=v:refname | tail -n1`/" -e "s/Version: v/Version: /" -i deb/reaction/DEBIAN/*
@ -26,12 +28,14 @@ reaction.deb: reaction ip46tables
mv deb/reaction.deb reaction.deb mv deb/reaction.deb reaction.deb
rm -rf deb/ rm -rf deb/
signatures: reaction.deb reaction ip46tables signatures: reaction.deb reaction ip46tables nft46
minisign -Sm ip46tables reaction reaction.deb minisign -Sm ip46tables nft46 reaction reaction.deb
install: all install: all
@install -m755 reaction $(DESTDIR)$(BINDIR) install -m755 reaction $(DESTDIR)$(BINDIR)
@install -m755 ip46tables $(DESTDIR)$(BINDIR) install -m755 ip46tables $(DESTDIR)$(BINDIR)
install -m755 nft46 $(DESTDIR)$(BINDIR)
install_systemd: install install_systemd: install
@install -m644 config/reaction.debian.service $(SYSTEMDDIR)/system/reaction.service install -m644 config/reaction.debian.service $(SYSTEMDDIR)/system/reaction.service
sed -i 's#/usr/bin#$(DESTDIR)$(BINDIR)#' $(SYSTEMDDIR)/system/reaction.service

View File

@ -142,7 +142,7 @@ It will execute `iptables` when detecting ipv4, `ip6tables` when detecting ipv6
## Wiki ## Wiki
You'll find more ressources, service configurations, etc. on the [Wiki](https://framagit.org/ppom/reaction-wiki)! You'll find more ressources, service configurations, etc. on the [Wiki](https://reaction.ppom.me)!
## Installation ## Installation
@ -154,6 +154,11 @@ Executables are provided [here](https://framagit.org/ppom/reaction/-/releases/),
A standard place to put such executables is `/usr/local/bin/`. A standard place to put such executables is `/usr/local/bin/`.
> Provided binaries in the previous section are compiled this way:
```shell
$ docker run -it --rm -e HOME=/tmp/ -v $(pwd):/tmp/code -w /tmp/code -u $(id -u) golang:1.20 make clean reaction.deb
$ make signaturese
```
#### Signature verification #### Signature verification
Starting at v1.0.3, all binaries are signed with public key `RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX`. You can check their authenticity with minisign: Starting at v1.0.3, all binaries are signed with public key `RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX`. You can check their authenticity with minisign:
@ -179,27 +184,27 @@ ExecStart=
ExecStart=/usr/bin/reaction start -c /etc/reaction.yml ExecStart=/usr/bin/reaction start -c /etc/reaction.yml
``` ```
#### NixOS
- [ package ](https://framagit.org/ppom/nixos/-/blob/main/pkgs/reaction/default.nix)
- [ module ](https://framagit.org/ppom/nixos/-/blob/main/modules/common/reaction.nix)
### Compilation ### Compilation
You'll need the go (>= 1.20) toolchain for reaction and a c compiler for ip46tables. You'll need the go (>= 1.20) toolchain for reaction and a c compiler for ip46tables.
```shell ```shell
$ make $ make
``` ```
Don't hesitate to take a look at the `Makefile` to understand what's happening!
Alternatively, ### Installation
To install the binaries
```shell ```shell
# creates ./reaction make install
$ go build .
# creates ./ip46tables
$ gcc ip46tables.d/ip46tables.c -o ip46tables
``` ```
Provided binaries in the previous section are compiled this way: To install the systemd file as well
```shell ```shell
$ docker run -it --rm -e HOME=/tmp/ -v $(pwd):/tmp/code -w /tmp/code -u $(id -u) golang:1.20 make clean reaction.deb make install_systemd
``` ```
### NixOS
- [ package ](https://framagit.org/ppom/nixos/-/blob/main/pkgs/reaction/default.nix)
- [ module ](https://framagit.org/ppom/nixos/-/blob/main/modules/common/reaction.nix)

View File

@ -20,7 +20,7 @@ const (
type Request struct { type Request struct {
Request int Request int
Pattern string Pattern []string
} }
type Response struct { type Response struct {
@ -85,7 +85,7 @@ func usage(err string) {
} }
func ClientShow(format, stream, filter string, regex *regexp.Regexp) { func ClientShow(format, stream, filter string, regex *regexp.Regexp) {
response := SendAndRetrieve(Request{Show, ""}) response := SendAndRetrieve(Request{Show, []string{""}})
if response.Err != nil { if response.Err != nil {
logger.Fatalln("Received error from daemon:", response.Err) logger.Fatalln("Received error from daemon:", response.Err)
} }
@ -166,7 +166,7 @@ func ClientShow(format, stream, filter string, regex *regexp.Regexp) {
os.Exit(0) os.Exit(0)
} }
func ClientFlush(pattern, streamfilter, format string) { func ClientFlush(pattern []string, streamfilter, format string) {
response := SendAndRetrieve(Request{Flush, pattern}) response := SendAndRetrieve(Request{Flush, pattern})
if response.Err != nil { if response.Err != nil {
logger.Fatalln("Received error from daemon:", response.Err) logger.Fatalln("Received error from daemon:", response.Err)

View File

@ -13,6 +13,16 @@ import (
"framagit.org/ppom/reaction/logger" "framagit.org/ppom/reaction/logger"
) )
// Compare content and ordering. Case sensitive.
func IsStringArrayEqual(one, two []string) bool {
for i, a := range one {
if a != two[i] {
return false
}
}
return true
}
// Executes a command and channel-send its stdout // Executes a command and channel-send its stdout
func cmdStdout(commandline []string) chan *string { func cmdStdout(commandline []string) chan *string {
lines := make(chan *string) lines := make(chan *string)
@ -62,6 +72,12 @@ func runCommands(commands [][]string, moment string) bool {
} }
func (p *Pattern) notAnIgnore(match *string) bool { func (p *Pattern) notAnIgnore(match *string) bool {
for _, regex := range p.compiledIgnoreRegex {
if regex.MatchString(*match) {
return false
}
}
for _, ignore := range p.Ignore { for _, ignore := range p.Ignore {
if ignore == *match { if ignore == *match {
return false return false
@ -71,44 +87,53 @@ func (p *Pattern) notAnIgnore(match *string) bool {
} }
// Whether one of the filter's regexes is matched on a line // Whether one of the filter's regexes is matched on a line
func (f *Filter) match(line *string) string { func (f *Filter) match(line *string) []string {
var result []string
for _, regex := range f.compiledRegex { for _, regex := range f.compiledRegex {
if matches := regex.FindStringSubmatch(*line); matches != nil { if matches := regex.FindStringSubmatch(*line); matches != nil {
var pnames []string
for _, p := range f.pattern {
pnames = append(pnames, p.name)
}
if f.pattern != nil { for _, p := range f.pattern {
match := matches[regex.SubexpIndex(f.pattern.name)] match := matches[regex.SubexpIndex(p.name)]
if p.notAnIgnore(&match) {
if f.pattern.notAnIgnore(&match) {
logger.Printf(logger.INFO, "%s.%s: match [%v]\n", f.stream.name, f.name, match) logger.Printf(logger.INFO, "%s.%s: match [%v]\n", f.stream.name, f.name, match)
return match result = append(result, match)
} }
} else { }
logger.Printf(logger.INFO, "%s.%s: match [.]\n", f.stream.name, f.name) if f.pattern == nil {
// No pattern, so this match will never actually be used // No pattern, so this match will never actually be used
return "." return []string{"."}
} }
} }
} }
return "" return result
} }
func (f *Filter) sendActions(match string, at time.Time) { func (f *Filter) sendActions(match []string, at time.Time) {
for _, a := range f.Actions { for _, a := range f.Actions {
actionsC <- PAT{match, a, at.Add(a.afterDuration)} actionsC <- PAT{match, a, at.Add(a.afterDuration)}
} }
} }
func (a *Action) exec(match string) { func (a *Action) exec(match []string) {
defer wgActions.Done() defer wgActions.Done()
var computedCommand []string var computedCommand []string
var cmdItem string
if a.filter.pattern != nil { if a.filter.pattern != nil {
computedCommand := make([]string, 0, len(a.Cmd)) computedCommand = make([]string, 0, len(a.Cmd))
for _, item := range a.Cmd { for _, item := range a.Cmd {
computedCommand = append(computedCommand, strings.ReplaceAll(item, a.filter.pattern.nameWithBraces, match)) cmdItem = strings.Clone(item)
for i, p := range a.filter.pattern {
cmdItem = strings.ReplaceAll(cmdItem, p.nameWithBraces, match[i])
}
computedCommand = append(computedCommand, cmdItem)
} }
} else { } else {
computedCommand = a.Cmd computedCommand = a.Cmd
@ -147,7 +172,7 @@ func ActionsManager(concurrency int) {
} }
}() }()
} }
execAction := func(a *Action, p string) { execAction := func(a *Action, p []string) {
wgActions.Add(1) wgActions.Add(1)
execActionsC <- PA{p, a} execActionsC <- PA{p, a}
} }
@ -165,10 +190,10 @@ func ActionsManager(concurrency int) {
execAction(action, pattern) execAction(action, pattern)
} else { } else {
actionsLock.Lock() actionsLock.Lock()
if actions[pa] == nil { if actions[&pa] == nil {
actions[pa] = make(map[time.Time]struct{}) actions[&pa] = make(map[time.Time]struct{})
} }
actions[pa][then] = struct{}{} actions[&pa][then] = struct{}{}
actionsLock.Unlock() actionsLock.Unlock()
go func(insidePat PAT, insideNow time.Time) { go func(insidePat PAT, insideNow time.Time) {
time.Sleep(insidePat.t.Sub(insideNow)) time.Sleep(insidePat.t.Sub(insideNow))
@ -179,8 +204,8 @@ func ActionsManager(concurrency int) {
pa := PA{pat.p, pat.a} pa := PA{pat.p, pat.a}
pattern, action, then := pat.p, pat.a, pat.t pattern, action, then := pat.p, pat.a, pat.t
actionsLock.Lock() actionsLock.Lock()
if actions[pa] != nil { if actions[&pa] != nil {
delete(actions[pa], then) delete(actions[&pa], then)
} }
actionsLock.Unlock() actionsLock.Unlock()
execAction(action, pattern) execAction(action, pattern)
@ -188,7 +213,7 @@ func ActionsManager(concurrency int) {
ret := make(ActionsMap) ret := make(ActionsMap)
actionsLock.Lock() actionsLock.Lock()
for pa := range actions { for pa := range actions {
if pa.p == fo.p { if IsStringArrayEqual(pa.p, fo.p) {
for range actions[pa] { for range actions[pa] {
execAction(pa.a, pa.p) execAction(pa.a, pa.p)
} }
@ -251,7 +276,7 @@ func matchesManagerHandleFlush(fo FlushMatchOrder) {
ret := make(MatchesMap) ret := make(MatchesMap)
matchesLock.Lock() matchesLock.Lock()
for pf := range matches { for pf := range matches {
if fo.p == pf.p { if IsStringArrayEqual(fo.p, pf.p) {
if fo.ret != nil { if fo.ret != nil {
ret[pf] = matches[pf] ret[pf] = matches[pf]
} }
@ -273,26 +298,26 @@ func matchesManagerHandleMatch(pft PFT) bool {
if filter.Retry > 1 { if filter.Retry > 1 {
// make sure map exists // make sure map exists
if matches[pf] == nil { if matches[&pf] == nil {
matches[pf] = make(map[time.Time]struct{}) matches[&pf] = make(map[time.Time]struct{})
} }
// add new match // add new match
matches[pf][then] = struct{}{} matches[&pf][then] = struct{}{}
// remove match when expired // remove match when expired
go func(pf PF, then time.Time) { go func(pf PF, then time.Time) {
time.Sleep(then.Sub(time.Now()) + filter.retryDuration) time.Sleep(then.Sub(time.Now()) + filter.retryDuration)
matchesLock.Lock() matchesLock.Lock()
if matches[pf] != nil { if matches[&pf] != nil {
// FIXME replace this and all similar occurences // FIXME replace this and all similar occurences
// by clear() when switching to go 1.21 // by clear() when switching to go 1.21
delete(matches[pf], then) delete(matches[&pf], then)
} }
matchesLock.Unlock() matchesLock.Unlock()
}(pf, then) }(pf, then)
} }
if filter.Retry <= 1 || len(matches[pf]) >= filter.Retry { if filter.Retry <= 1 || len(matches[&pf]) >= filter.Retry {
delete(matches, pf) delete(matches, &pf)
filter.sendActions(pattern, then) filter.sendActions(pattern, then)
return true return true
} }
@ -312,7 +337,7 @@ func StreamManager(s *Stream, endedSignal chan *Stream) {
return return
} }
for _, filter := range s.Filters { for _, filter := range s.Filters {
if match := filter.match(line); match != "" { if match := filter.match(line); len(match) > 0 {
matchesC <- PFT{match, filter, time.Now()} matchesC <- PFT{match, filter, time.Now()}
} }
} }

View File

@ -23,6 +23,9 @@ patterns:
ignore: ignore:
- 127.0.0.1 - 127.0.0.1
- ::1 - ::1
# Patterns can be ignored based on regexes, it will try to match the whole string detected by the pattern
# ignoreregex:
# - '10\.0\.[0-9]{1,3}\.[0-9]{1,3}'
# Those commands will be executed in order at start, before everything else # Those commands will be executed in order at start, before everything else
start: start:

View File

@ -103,6 +103,8 @@ func basicUsage() {
# remove currently active matches and run currently pending actions for the specified TARGET # remove currently active matches and run currently pending actions for the specified TARGET
# (then show flushed matches and actions) # (then show flushed matches and actions)
# e.g. reaction flush 192.168.1.1 # e.g. reaction flush 192.168.1.1
# Concatenate patterns with " / " if several patterns in TARGET
# e.g. reaction flush "192.168.1.1 / root"
# options: # options:
-s/--socket SOCKET # path to the client-daemon communication socket -s/--socket SOCKET # path to the client-daemon communication socket
@ -115,7 +117,7 @@ cat FILE | ` + bold + `reaction test-regex` + reset + ` REGEX # test REGEX again
# print version information # print version information
see usage examples, service configurations and good practices see usage examples, service configurations and good practices
on the ` + bold + `wiki` + reset + `: https://framagit.org/ppom/reaction-wiki on the ` + bold + `wiki` + reset + `: https://reaction.ppom.me
`) `)
} }
@ -209,7 +211,7 @@ func Main(version, commit string) {
logger.Fatalln("for now, -l/--limit is not supported") logger.Fatalln("for now, -l/--limit is not supported")
os.Exit(1) os.Exit(1)
} }
ClientFlush(f.Arg(0), *limit, *queryFormat) ClientFlush(strings.Split(f.Arg(0), " / "), *limit, *queryFormat)
case "test-regex": case "test-regex":
// socket not needed, no interaction with the daemon // socket not needed, no interaction with the daemon

View File

@ -134,7 +134,7 @@ func rotateDB(c *Conf, logDec *gob.Decoder, flushDec *gob.Decoder, logEnc *gob.E
}() }()
// pattern, stream, fitler → last flush // pattern, stream, fitler → last flush
flushes := make(map[PSF]time.Time) flushes := make(map[*PSF]time.Time)
for { for {
var entry LogEntry var entry LogEntry
var filter *Filter var filter *Filter
@ -160,7 +160,7 @@ func rotateDB(c *Conf, logDec *gob.Decoder, flushDec *gob.Decoder, logEnc *gob.E
} }
// store // store
flushes[PSF{entry.Pattern, entry.Stream, entry.Filter}] = entry.T flushes[&PSF{entry.Pattern, entry.Stream, entry.Filter}] = entry.T
} }
lastTimeCpt := int64(0) lastTimeCpt := int64(0)
@ -201,8 +201,8 @@ func rotateDB(c *Conf, logDec *gob.Decoder, flushDec *gob.Decoder, logEnc *gob.E
} }
// check if it hasn't been flushed // check if it hasn't been flushed
lastGlobalFlush := flushes[PSF{entry.Pattern, "", ""}].Unix() lastGlobalFlush := flushes[&PSF{entry.Pattern, "", ""}].Unix()
lastLocalFlush := flushes[PSF{entry.Pattern, entry.Stream, entry.Filter}].Unix() lastLocalFlush := flushes[&PSF{entry.Pattern, entry.Stream, entry.Filter}].Unix()
entryTime := entry.T.Unix() entryTime := entry.T.Unix()
if lastLocalFlush > entryTime || lastGlobalFlush > entryTime { if lastLocalFlush > entryTime || lastGlobalFlush > entryTime {
continue continue

View File

@ -7,6 +7,7 @@ import (
"path" "path"
"sync" "sync"
"time" "time"
"strings"
"framagit.org/ppom/reaction/logger" "framagit.org/ppom/reaction/logger"
) )
@ -24,7 +25,7 @@ func genClientStatus(local_actions ActionsMap, local_matches MatchesMap, local_a
if cs[filter.stream.name][filter.name] == nil { if cs[filter.stream.name][filter.name] == nil {
cs[filter.stream.name][filter.name] = make(MapPatternStatus) cs[filter.stream.name][filter.name] = make(MapPatternStatus)
} }
cs[filter.stream.name][filter.name][pattern] = &PatternStatus{len(times), nil} cs[filter.stream.name][filter.name][strings.Join(pattern, " / ")] = &PatternStatus{len(times), nil}
} }
local_matchesLock.Unlock() local_matchesLock.Unlock()
@ -39,10 +40,10 @@ func genClientStatus(local_actions ActionsMap, local_matches MatchesMap, local_a
if cs[action.filter.stream.name][action.filter.name] == nil { if cs[action.filter.stream.name][action.filter.name] == nil {
cs[action.filter.stream.name][action.filter.name] = make(MapPatternStatus) cs[action.filter.stream.name][action.filter.name] = make(MapPatternStatus)
} }
if cs[action.filter.stream.name][action.filter.name][pattern] == nil { if cs[action.filter.stream.name][action.filter.name][strings.Join(pattern, " / ")] == nil {
cs[action.filter.stream.name][action.filter.name][pattern] = new(PatternStatus) cs[action.filter.stream.name][action.filter.name][strings.Join(pattern, " / ")] = new(PatternStatus)
} }
ps := cs[action.filter.stream.name][action.filter.name][pattern] ps := cs[action.filter.stream.name][action.filter.name][strings.Join(pattern, " / ")]
if ps.Actions == nil { if ps.Actions == nil {
ps.Actions = make(map[string][]string) ps.Actions = make(map[string][]string)
} }

View File

@ -13,6 +13,7 @@ import (
"framagit.org/ppom/reaction/logger" "framagit.org/ppom/reaction/logger"
"github.com/google/go-jsonnet" "github.com/google/go-jsonnet"
"golang.org/x/exp/slices"
) )
func (c *Conf) setup() { func (c *Conf) setup() {
@ -39,6 +40,17 @@ func (c *Conf) setup() {
logger.Fatalf("Bad configuration: pattern ignore '%v' doesn't match pattern %v! It should be fixed or removed.", ignore, pattern.nameWithBraces) logger.Fatalf("Bad configuration: pattern ignore '%v' doesn't match pattern %v! It should be fixed or removed.", ignore, pattern.nameWithBraces)
} }
} }
// Compile ignore regexes
for _, regex := range pattern.IgnoreRegex {
// Enclose the regex to make sure that it matches the whole detected string
compiledRegex, err := regexp.Compile("^" + regex + "$")
if err != nil {
log.Fatalf("%vBad configuration: in ignoreregex of pattern %s: %v", logger.FATAL, pattern.name, err)
}
pattern.compiledIgnoreRegex = append(pattern.compiledIgnoreRegex, *compiledRegex)
}
} }
if len(c.Streams) == 0 { if len(c.Streams) == 0 {
@ -63,17 +75,17 @@ func (c *Conf) setup() {
filter.name = filterName filter.name = filterName
if strings.Contains(filter.name, ".") { if strings.Contains(filter.name, ".") {
logger.Fatalf("Bad configuration: character '.' is not allowed in filter names: '%v'", filter.name) logger.Fatalf(fmt.Sprintf("Bad configuration: character '.' is not allowed in filter names: '%v'", filter.name))
} }
// Parse Duration // Parse Duration
if filter.RetryPeriod == "" { if filter.RetryPeriod == "" {
if filter.Retry > 1 { if filter.Retry > 1 {
logger.Fatalf("Bad configuration: retry but no retryperiod in %v.%v", stream.name, filter.name) logger.Fatalf(fmt.Sprintf("Bad configuration: retry but no retryperiod in %v.%v", stream.name, filter.name))
} }
} else { } else {
retryDuration, err := time.ParseDuration(filter.RetryPeriod) retryDuration, err := time.ParseDuration(filter.RetryPeriod)
if err != nil { if err != nil {
logger.Fatalf("Bad configuration: Failed to parse retry time in %v.%v: %v", stream.name, filter.name, err) logger.Fatalf(fmt.Sprintf("Bad configuration: Failed to parse retry time in %v.%v: %v", stream.name, filter.name, err))
} }
filter.retryDuration = retryDuration filter.retryDuration = retryDuration
} }
@ -84,27 +96,17 @@ func (c *Conf) setup() {
// Compute Regexes // Compute Regexes
// Look for Patterns inside Regexes // Look for Patterns inside Regexes
for _, regex := range filter.Regex { for _, regex := range filter.Regex {
for patternName, pattern := range c.Patterns { for _, pattern := range c.Patterns {
if strings.Contains(regex, pattern.nameWithBraces) { if strings.Contains(regex, pattern.nameWithBraces) {
if !slices.Contains(filter.pattern, pattern) {
if filter.pattern == nil { filter.pattern = append(filter.pattern, pattern)
filter.pattern = pattern
} else if filter.pattern == pattern {
// no op
} else {
logger.Fatalf(
"Bad configuration: Can't mix different patterns (%s, %s) in same filter (%s.%s)\n",
filter.pattern.name, patternName, streamName, filterName,
)
} }
// FIXME should go in the `if filter.pattern == nil`?
regex = strings.Replace(regex, pattern.nameWithBraces, pattern.Regex, 1) regex = strings.Replace(regex, pattern.nameWithBraces, pattern.Regex, 1)
} }
} }
compiledRegex, err := regexp.Compile(regex) compiledRegex, err := regexp.Compile(regex)
if err != nil { if err != nil {
log.Fatalf("%vBad configuration: regex of filter %s.%s: %v", logger.FATAL, stream.name, filter.name, err) log.Fatal(fmt.Sprintf("Bad configuration: regex of filter %s.%s: %v", stream.name, filter.name, err))
} }
filter.compiledRegex = append(filter.compiledRegex, *compiledRegex) filter.compiledRegex = append(filter.compiledRegex, *compiledRegex)
} }
@ -114,7 +116,7 @@ func (c *Conf) setup() {
} }
for actionName := range filter.Actions { for actionName := range filter.Actions {
action := filter.Actions[actionName] action := filter.Actions[actionName]
action.filter = filter action.filter = filter
action.name = actionName action.name = actionName

View File

@ -19,6 +19,9 @@ type Pattern struct {
Regex string `json:"regex"` Regex string `json:"regex"`
Ignore []string `json:"ignore"` Ignore []string `json:"ignore"`
IgnoreRegex []string `json:"ignoreregex"`
compiledIgnoreRegex []regexp.Regexp `json:"-"`
name string `json:"-"` name string `json:"-"`
nameWithBraces string `json:"-"` nameWithBraces string `json:"-"`
} }
@ -39,7 +42,7 @@ type Filter struct {
Regex []string `json:"regex"` Regex []string `json:"regex"`
compiledRegex []regexp.Regexp `json:"-"` compiledRegex []regexp.Regexp `json:"-"`
pattern *Pattern `json:"-"` pattern []*Pattern `json:"-"`
Retry int `json:"retry"` Retry int `json:"retry"`
RetryPeriod string `json:"retryperiod"` RetryPeriod string `json:"retryperiod"`
@ -64,7 +67,7 @@ type Action struct {
type LogEntry struct { type LogEntry struct {
T time.Time T time.Time
S int64 S int64
Pattern string Pattern []string
Stream, Filter string Stream, Filter string
SF int SF int
Exec bool Exec bool
@ -79,37 +82,43 @@ type WriteDB struct {
file *os.File file *os.File
enc *gob.Encoder enc *gob.Encoder
} }
// https://stackoverflow.com/a/69691894
type MatchesMap map[PF]map[time.Time]struct{} type MatchesMap map[*PF]map[time.Time]struct{}
type ActionsMap map[PA]map[time.Time]struct{} type ActionsMap map[*PA]map[time.Time]struct{}
// Helper structs made to carry information // Helper structs made to carry information
// Stream, Filter
type SF struct{ s, f string } type SF struct{ s, f string }
type PSF struct{ p, s, f string } // Pattern, Stream, Filter
type PSF struct{
p []string
s string
f string
}
type PF struct { type PF struct {
p string p []string
f *Filter f *Filter
} }
type PFT struct { type PFT struct {
p string p []string
f *Filter f *Filter
t time.Time t time.Time
} }
type PA struct { type PA struct {
p string p []string
a *Action a *Action
} }
type PAT struct { type PAT struct {
p string p []string
a *Action a *Action
t time.Time t time.Time
} }
type FlushMatchOrder struct { type FlushMatchOrder struct {
p string p []string
ret chan MatchesMap ret chan MatchesMap
} }
type FlushActionOrder struct { type FlushActionOrder struct {
p string p []string
ret chan ActionsMap ret chan ActionsMap
} }

View File

@ -29,6 +29,8 @@ local banFor(time) = {
// simple version: regex: @'(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})', // simple version: regex: @'(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})',
regex: @'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))', regex: @'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))',
ignore: ['127.0.0.1', '::1'], ignore: ['127.0.0.1', '::1'],
// Patterns can be ignored based on regexes, it will try to match the whole string detected by the pattern
// ignoreregex: [@'10\.0\.[0-9]{1,3}\.[0-9]{1,3}'],
}, },
}, },

View File

@ -2,24 +2,14 @@
patterns: { patterns: {
num: { num: {
regex: '[0-9]+', regex: '[0-9]+',
ignore: ['1'],
ignoreregex: ['2.?'],
}, },
}, },
start: [
['err'],
['sleep', '1'],
],
stop: [
['sleep', '1'],
// ['false'],
['true'],
],
streams: { streams: {
tailDown1: { tailDown1: {
cmd: ['sh', '-c', "echo 1 2 3 4 5 5 | tr ' ' '\n' | while read i; do sleep 1; echo found $(($i % 10)); done"], cmd: ['sh', '-c', "echo 1 2 3 4 5 11 12 21 22 33 | tr ' ' '\n' | while read i; do sleep 1; echo found $i; done"],
// cmd: ['sh', '-c', "echo 1 2 3 4 5 1 2 3 4 5 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 | tr ' ' '\n' | while read i; do sleep 2; echo found $(($i % 10)); done"],
filters: { filters: {
findIP: { findIP: {
regex: ['^found <num>$'], regex: ['^found <num>$'],
@ -38,25 +28,5 @@
}, },
}, },
}, },
// tailDown2: {
// cmd: ['sh', '-c', 'echo coucou; sleep 2m'],
// filters: {
// findIP: {
// regex: ['^found <num>$'],
// retry: 3,
// retryperiod: '30s',
// actions: {
// damn: {
// cmd: ['echo', '<num>'],
// },
// undamn: {
// cmd: ['echo', 'undamn', '<num>'],
// after: '30s',
// onexit: true,
// },
// },
// },
// },
// },
}, },
} }

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.20
require ( require (
github.com/google/go-jsonnet v0.20.0 github.com/google/go-jsonnet v0.20.0
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
sigs.k8s.io/yaml v1.1.0 sigs.k8s.io/yaml v1.1.0
) )

2
go.sum
View File

@ -1,6 +1,8 @@
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g=
github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

97
helpers_c/nft46.c Normal file
View File

@ -0,0 +1,97 @@
#include<ctype.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
// nft46 'add element inet reaction ipvXbans { 1.2.3.4 }' → nft 'add element inet reaction ipv4bans { 1.2.3.4 }'
// nft46 'add element inet reaction ipvXbans { a:b::c:d }' → nft 'add element inet reaction ipv6bans { a:b::c:d }'
//
// the character X is replaced by 4 or 6 depending on the address family of the specified IP
//
// Limitations:
// - nft46 must receive exactly one argument
// - only one IP must be given per command
// - the IP must be between { braces }
int isIPv4(char *tab, int len) {
int i;
// IPv4 addresses are at least 7 chars long
if (len < 7 || !isdigit(tab[0]) || !isdigit(tab[len-1])) {
return 0;
}
// Each char must be a digit or a dot between 2 digits
for (i=1; i<len-1; i++) {
if (!isdigit(tab[i]) && !(tab[i] == '.' && isdigit(tab[i-1]) && isdigit(tab[i+1]))) {
return 0;
}
}
return 1;
}
int isIPv6(char *tab, int len) {
int i;
// IPv6 addresses are at least 3 chars long
if (len < 3) {
return 0;
}
// Each char must be a digit, :, a-f, or A-F
for (i=0; i<len; i++) {
if (!isdigit(tab[i]) && tab[i] != ':' && !(tab[i] >= 'a' && tab[i] <= 'f') && !(tab[i] >= 'A' && tab[i] <= 'F')) {
return 0;
}
}
return 1;
}
int findchar(char *tab, char c, int i, int len) {
while (i < len && tab[i] != c) i++;
if (i == len) {
printf("nft46: one %c must be present", c);
exit(1);
}
return i;
}
void adapt_args(char *tab) {
int i, len, X, startIP, endIP, startedIP;
X = startIP = endIP = -1;
startedIP = 0;
len = strlen(tab);
i = 0;
X = i = findchar(tab, 'X', i, len);
startIP = i = findchar(tab, '{', i, len);
while (startIP + 1 <= (i = findchar(tab, ' ', i, len))) startIP = i + 1;
i = startIP;
endIP = i = findchar(tab, ' ', i, len) - 1;
if (isIPv4(tab+startIP, endIP-startIP+1)) {
tab[X] = '4';
return;
}
if (isIPv6(tab+startIP, endIP-startIP+1)) {
tab[X] = '6';
return;
}
printf("nft46: no IP address found\n");
exit(1);
}
int exec(char *str, char **argv) {
argv[0] = str;
execvp(str, argv);
// returns only if fails
printf("nft46: exec failed %d\n", errno);
}
int main(int argc, char **argv) {
if (argc != 2) {
printf("nft46: Exactly one argument must be given\n");
exit(1);
}
adapt_args(argv[1]);
exec("nft", argv);
}

View File

@ -2,29 +2,31 @@
set -exu set -exu
git push --tags
docker run -it --rm -e HOME=/tmp/ -v "$(pwd)":/tmp/code -w /tmp/code -u "$(id -u)" golang:1.20 make reaction.deb docker run -it --rm -e HOME=/tmp/ -v "$(pwd)":/tmp/code -w /tmp/code -u "$(id -u)" golang:1.20 make reaction.deb
make signatures make signatures
TAG="$(git tag --sort=v:refname | tail -n1)" TAG="$(git tag --sort=v:refname | tail -n1)"
rsync -avz -e 'ssh -J pica01' ./ip46tables ./reaction ./reaction.deb ./ip46tables.minisig ./reaction.minisig ./reaction.deb.minisig akesi:/var/www/static/reaction/releases/"$TAG" rsync -avz -e 'ssh -J pica01' ./ip46tables ./nft46 ./reaction ./reaction.deb ./nft46.minisig ./ip46tables.minisig ./reaction.minisig ./reaction.deb.minisig akesi:/var/www/static/reaction/releases/"$TAG"
TOKEN="$(rbw get framagit.org token)" TOKEN="$(rbw get framagit.org token)"
DATA='{ DATA='{
"tag_name":"'"$TAG"'", "tag_name":"'"$TAG"'",
"assets":{"links":[ "assets":{"links":[
{"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/nft46", "name": "nft46 (x86-64)", "link_type": "package"},
{"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/reaction", "name": "reaction (x86-64)", "link_type": "package"}, {"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/reaction", "name": "reaction (x86-64)", "link_type": "package"},
{"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/ip46tables", "name": "ip46tables (x86-64)", "link_type": "package"}, {"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/ip46tables", "name": "ip46tables (x86-64)", "link_type": "package"},
{"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/reaction.deb", "name": "reaction.deb (x86-64)", "link_type": "package"}, {"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/reaction.deb", "name": "reaction.deb (x86-64)", "link_type": "package"},
{"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/nft46.minisig", "name": "nft46.minisig", "link_type": "other"},
{"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/reaction.minisig", "name": "reaction.minisig", "link_type": "other"}, {"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/reaction.minisig", "name": "reaction.minisig", "link_type": "other"},
{"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/ip46tables.minisig", "name": "ip46tables.minisig", "link_type": "other"}, {"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/ip46tables.minisig", "name": "ip46tables.minisig", "link_type": "other"},
{"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/reaction.deb.minisig", "name": "reaction.deb.minisig", "link_type": "other"} {"url": "https://static.ppom.me/reaction/releases/'"$TAG"'/reaction.deb.minisig", "name": "reaction.deb.minisig", "link_type": "other"}
]}}' ]}}'
DATA="$(echo "$DATA" | tr '\n' ' ')"
curl \ curl \
--fail-with-body \ --fail-with-body \
--location \ --location \