Compare commits

..

No commits in common. "00f1647aa651b9d4b5df4562dda57ea21850c706" and "ea29a0467af8de14388ebea9405517b47be1aa9b" have entirely different histories.

19 changed files with 137 additions and 274 deletions

2
.gitignore vendored
View File

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

View File

@ -2,18 +2,8 @@ Package: reaction
Version: LAST_TAG
Architecture: amd64
Maintainer: ppom <>
Section: utils
Sections: utils
Package-Type: deb
Priority: Optional
Homepage: https://framagit.org/ppom/reaction
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
Description: A daemon that scans program outputs for repeated patterns, and takes action.

View File

@ -3,24 +3,22 @@ PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin
SYSTEMDDIR ?= /etc/systemd
all: reaction ip46tables nft46
all: reaction ip46tables
clean:
rm -f reaction ip46tables nft46 reaction.deb deb reaction.minisig ip46tables.minisig reaction.deb.minisig nft46.minisig
rm -f reaction ip46tables reaction.deb deb reaction.minisig ip46tables.minisig reaction.deb.minisig
ip46tables: helpers_c/ip46tables.c
$(CC) -s -static helpers_c/ip46tables.c -o ip46tables
nft46: helpers_c/nft46.c
$(CC) -s -static helpers_c/nft46.c -o nft46
ip46tables: ip46tables.d/ip46tables.c
$(CC) -s -static ip46tables.d/ip46tables.c -o ip46tables
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`"
reaction.deb: reaction ip46tables nft46
chmod +x reaction ip46tables nft46
reaction.deb: reaction ip46tables
chmod +x reaction ip46tables
mkdir -p deb/reaction/usr/bin/ deb/reaction/usr/sbin/ deb/reaction/lib/systemd/system/
cp reaction ip46tables nft46 deb/reaction/usr/bin/
cp reaction deb/reaction/usr/bin/
cp ip46tables deb/reaction/usr/sbin/
cp config/reaction.debian.service deb/reaction/lib/systemd/system/reaction.service
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/*
@ -28,14 +26,12 @@ reaction.deb: reaction ip46tables nft46
mv deb/reaction.deb reaction.deb
rm -rf deb/
signatures: reaction.deb reaction ip46tables nft46
minisign -Sm ip46tables nft46 reaction reaction.deb
signatures: reaction.deb reaction ip46tables
minisign -Sm ip46tables reaction reaction.deb
install: all
install -m755 reaction $(DESTDIR)$(BINDIR)
install -m755 ip46tables $(DESTDIR)$(BINDIR)
install -m755 nft46 $(DESTDIR)$(BINDIR)
@install -m755 reaction $(DESTDIR)$(BINDIR)
@install -m755 ip46tables $(DESTDIR)$(BINDIR)
install_systemd: install
install -m644 config/reaction.debian.service $(SYSTEMDDIR)/system/reaction.service
sed -i 's#/usr/bin#$(DESTDIR)$(BINDIR)#' $(SYSTEMDDIR)/system/reaction.service
@install -m644 config/reaction.debian.service $(SYSTEMDDIR)/system/reaction.service

View File

@ -142,7 +142,7 @@ It will execute `iptables` when detecting ipv4, `ip6tables` when detecting ipv6
## Wiki
You'll find more ressources, service configurations, etc. on the [Wiki](https://reaction.ppom.me)!
You'll find more ressources, service configurations, etc. on the [Wiki](https://framagit.org/ppom/reaction-wiki)!
## Installation
@ -154,11 +154,6 @@ Executables are provided [here](https://framagit.org/ppom/reaction/-/releases/),
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
Starting at v1.0.3, all binaries are signed with public key `RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX`. You can check their authenticity with minisign:
@ -184,27 +179,27 @@ ExecStart=
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
You'll need the go (>= 1.20) toolchain for reaction and a c compiler for ip46tables.
```shell
$ make
```
Don't hesitate to take a look at the `Makefile` to understand what's happening!
### Installation
To install the binaries
Alternatively,
```shell
make install
# creates ./reaction
$ go build .
# creates ./ip46tables
$ gcc ip46tables.d/ip46tables.c -o ip46tables
```
To install the systemd file as well
Provided binaries in the previous section are compiled this way:
```shell
make install_systemd
$ docker run -it --rm -e HOME=/tmp/ -v $(pwd):/tmp/code -w /tmp/code -u $(id -u) golang:1.20 make clean reaction.deb
```
### 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 {
Request int
Pattern []string
Pattern string
}
type Response struct {
@ -85,7 +85,7 @@ func usage(err string) {
}
func ClientShow(format, stream, filter string, regex *regexp.Regexp) {
response := SendAndRetrieve(Request{Show, []string{""}})
response := SendAndRetrieve(Request{Show, ""})
if response.Err != nil {
logger.Fatalln("Received error from daemon:", response.Err)
}
@ -166,7 +166,7 @@ func ClientShow(format, stream, filter string, regex *regexp.Regexp) {
os.Exit(0)
}
func ClientFlush(pattern []string, streamfilter, format string) {
func ClientFlush(pattern, streamfilter, format string) {
response := SendAndRetrieve(Request{Flush, pattern})
if response.Err != nil {
logger.Fatalln("Received error from daemon:", response.Err)

View File

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

View File

@ -23,9 +23,6 @@ patterns:
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}'
# Those commands will be executed in order at start, before everything else
start:

View File

@ -103,8 +103,6 @@ func basicUsage() {
# remove currently active matches and run currently pending actions for the specified TARGET
# (then show flushed matches and actions)
# 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:
-s/--socket SOCKET # path to the client-daemon communication socket
@ -117,7 +115,7 @@ cat FILE | ` + bold + `reaction test-regex` + reset + ` REGEX # test REGEX again
# print version information
see usage examples, service configurations and good practices
on the ` + bold + `wiki` + reset + `: https://reaction.ppom.me
on the ` + bold + `wiki` + reset + `: https://framagit.org/ppom/reaction-wiki
`)
}
@ -211,7 +209,7 @@ func Main(version, commit string) {
logger.Fatalln("for now, -l/--limit is not supported")
os.Exit(1)
}
ClientFlush(strings.Split(f.Arg(0), " / "), *limit, *queryFormat)
ClientFlush(f.Arg(0), *limit, *queryFormat)
case "test-regex":
// 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
flushes := make(map[*PSF]time.Time)
flushes := make(map[PSF]time.Time)
for {
var entry LogEntry
var filter *Filter
@ -160,7 +160,7 @@ func rotateDB(c *Conf, logDec *gob.Decoder, flushDec *gob.Decoder, logEnc *gob.E
}
// store
flushes[&PSF{entry.Pattern, entry.Stream, entry.Filter}] = entry.T
flushes[PSF{entry.Pattern, entry.Stream, entry.Filter}] = entry.T
}
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
lastGlobalFlush := flushes[&PSF{entry.Pattern, "", ""}].Unix()
lastLocalFlush := flushes[&PSF{entry.Pattern, entry.Stream, entry.Filter}].Unix()
lastGlobalFlush := flushes[PSF{entry.Pattern, "", ""}].Unix()
lastLocalFlush := flushes[PSF{entry.Pattern, entry.Stream, entry.Filter}].Unix()
entryTime := entry.T.Unix()
if lastLocalFlush > entryTime || lastGlobalFlush > entryTime {
continue

View File

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

View File

@ -13,7 +13,6 @@ import (
"framagit.org/ppom/reaction/logger"
"github.com/google/go-jsonnet"
"golang.org/x/exp/slices"
)
func (c *Conf) setup() {
@ -40,17 +39,6 @@ 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)
}
}
// 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 {
@ -75,17 +63,17 @@ func (c *Conf) setup() {
filter.name = filterName
if strings.Contains(filter.name, ".") {
logger.Fatalf(fmt.Sprintf("Bad configuration: character '.' is not allowed in filter names: '%v'", filter.name))
logger.Fatalf("Bad configuration: character '.' is not allowed in filter names: '%v'", filter.name)
}
// Parse Duration
if filter.RetryPeriod == "" {
if filter.Retry > 1 {
logger.Fatalf(fmt.Sprintf("Bad configuration: retry but no retryperiod in %v.%v", stream.name, filter.name))
logger.Fatalf("Bad configuration: retry but no retryperiod in %v.%v", stream.name, filter.name)
}
} else {
retryDuration, err := time.ParseDuration(filter.RetryPeriod)
if err != nil {
logger.Fatalf(fmt.Sprintf("Bad configuration: Failed to parse retry time in %v.%v: %v", stream.name, filter.name, err))
logger.Fatalf("Bad configuration: Failed to parse retry time in %v.%v: %v", stream.name, filter.name, err)
}
filter.retryDuration = retryDuration
}
@ -96,17 +84,27 @@ func (c *Conf) setup() {
// Compute Regexes
// Look for Patterns inside Regexes
for _, regex := range filter.Regex {
for _, pattern := range c.Patterns {
for patternName, pattern := range c.Patterns {
if strings.Contains(regex, pattern.nameWithBraces) {
if !slices.Contains(filter.pattern, pattern) {
filter.pattern = append(filter.pattern, pattern)
if filter.pattern == nil {
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)
}
}
compiledRegex, err := regexp.Compile(regex)
if err != nil {
log.Fatal(fmt.Sprintf("Bad configuration: regex of filter %s.%s: %v", stream.name, filter.name, err))
log.Fatalf("%vBad configuration: regex of filter %s.%s: %v", logger.FATAL, stream.name, filter.name, err)
}
filter.compiledRegex = append(filter.compiledRegex, *compiledRegex)
}

View File

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

View File

@ -29,8 +29,6 @@ local banFor(time) = {
// 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]))',
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,14 +2,24 @@
patterns: {
num: {
regex: '[0-9]+',
ignore: ['1'],
ignoreregex: ['2.?'],
},
},
start: [
['err'],
['sleep', '1'],
],
stop: [
['sleep', '1'],
// ['false'],
['true'],
],
streams: {
tailDown1: {
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 5 | tr ' ' '\n' | while read i; do sleep 1; echo found $(($i % 10)); 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: {
findIP: {
regex: ['^found <num>$'],
@ -28,5 +38,25 @@
},
},
},
// 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,7 +4,6 @@ go 1.20
require (
github.com/google/go-jsonnet v0.20.0
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
sigs.k8s.io/yaml v1.1.0
)

2
go.sum
View File

@ -1,8 +1,6 @@
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/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/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,97 +0,0 @@
#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,31 +2,29 @@
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
make signatures
TAG="$(git tag --sort=v:refname | tail -n1)"
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"
rsync -avz -e 'ssh -J pica01' ./ip46tables ./reaction ./reaction.deb ./ip46tables.minisig ./reaction.minisig ./reaction.deb.minisig akesi:/var/www/static/reaction/releases/"$TAG"
TOKEN="$(rbw get framagit.org token)"
DATA='{
"tag_name":"'"$TAG"'",
"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"'/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"'/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"'/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"}
]}}'
DATA="$(echo "$DATA" | tr '\n' ' ')"
curl \
--fail-with-body \
--location \