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
/ip46tables
/nft46
/reaction*.db
/reaction*.sock
/result
@ -7,3 +8,4 @@
/deb
*.deb
*.minisig
*.qcow2

View File

@ -2,8 +2,18 @@ Package: reaction
Version: LAST_TAG
Architecture: amd64
Maintainer: ppom <>
Sections: utils
Section: 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.
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
SYSTEMDDIR ?= /etc/systemd
all: reaction ip46tables
all: reaction ip46tables nft46
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
$(CC) -s -static ip46tables.d/ip46tables.c -o ip46tables
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
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
chmod +x reaction ip46tables
reaction.deb: reaction ip46tables nft46
chmod +x reaction ip46tables nft46
mkdir -p deb/reaction/usr/bin/ deb/reaction/usr/sbin/ deb/reaction/lib/systemd/system/
cp reaction deb/reaction/usr/bin/
cp ip46tables deb/reaction/usr/sbin/
cp reaction ip46tables nft46 deb/reaction/usr/bin/
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/*
@ -26,12 +28,14 @@ reaction.deb: reaction ip46tables
mv deb/reaction.deb reaction.deb
rm -rf deb/
signatures: reaction.deb reaction ip46tables
minisign -Sm ip46tables reaction reaction.deb
signatures: reaction.deb reaction ip46tables nft46
minisign -Sm ip46tables nft46 reaction reaction.deb
install: all
@install -m755 reaction $(DESTDIR)$(BINDIR)
@install -m755 ip46tables $(DESTDIR)$(BINDIR)
install -m755 reaction $(DESTDIR)$(BINDIR)
install -m755 ip46tables $(DESTDIR)$(BINDIR)
install -m755 nft46 $(DESTDIR)$(BINDIR)
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
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
@ -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/`.
> 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:
@ -179,27 +184,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!
Alternatively,
### Installation
To install the binaries
```shell
# creates ./reaction
$ go build .
# creates ./ip46tables
$ gcc ip46tables.d/ip46tables.c -o ip46tables
make install
```
Provided binaries in the previous section are compiled this way:
To install the systemd file as well
```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 {
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, ""})
response := SendAndRetrieve(Request{Show, []string{""}})
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, streamfilter, format string) {
func ClientFlush(pattern []string, streamfilter, format string) {
response := SendAndRetrieve(Request{Flush, pattern})
if response.Err != nil {
logger.Fatalln("Received error from daemon:", response.Err)

View File

@ -13,6 +13,16 @@ 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)
@ -62,6 +72,12 @@ 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
@ -71,44 +87,53 @@ 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 {
func (f *Filter) match(line *string) []string {
var result []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)
}
if f.pattern != nil {
match := matches[regex.SubexpIndex(f.pattern.name)]
if f.pattern.notAnIgnore(&match) {
for _, p := range f.pattern {
match := matches[regex.SubexpIndex(p.name)]
if p.notAnIgnore(&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
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 {
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 {
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 {
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)
execActionsC <- PA{p, a}
}
@ -165,10 +190,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))
@ -179,8 +204,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)
@ -188,7 +213,7 @@ func ActionsManager(concurrency int) {
ret := make(ActionsMap)
actionsLock.Lock()
for pa := range actions {
if pa.p == fo.p {
if IsStringArrayEqual(pa.p, fo.p) {
for range actions[pa] {
execAction(pa.a, pa.p)
}
@ -251,7 +276,7 @@ func matchesManagerHandleFlush(fo FlushMatchOrder) {
ret := make(MatchesMap)
matchesLock.Lock()
for pf := range matches {
if fo.p == pf.p {
if IsStringArrayEqual(fo.p, pf.p) {
if fo.ret != nil {
ret[pf] = matches[pf]
}
@ -273,26 +298,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
}
@ -312,7 +337,7 @@ func StreamManager(s *Stream, endedSignal chan *Stream) {
return
}
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()}
}
}

View File

@ -23,6 +23,9 @@ 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,6 +103,8 @@ 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
@ -115,7 +117,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://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")
os.Exit(1)
}
ClientFlush(f.Arg(0), *limit, *queryFormat)
ClientFlush(strings.Split(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,6 +7,7 @@ import (
"path"
"sync"
"time"
"strings"
"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 {
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()
@ -39,10 +40,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][pattern] == nil {
cs[action.filter.stream.name][action.filter.name][pattern] = new(PatternStatus)
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)
}
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 {
ps.Actions = make(map[string][]string)
}

View File

@ -13,6 +13,7 @@ import (
"framagit.org/ppom/reaction/logger"
"github.com/google/go-jsonnet"
"golang.org/x/exp/slices"
)
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)
}
}
// 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 {
@ -63,17 +75,17 @@ func (c *Conf) setup() {
filter.name = filterName
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
if filter.RetryPeriod == "" {
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 {
retryDuration, err := time.ParseDuration(filter.RetryPeriod)
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
}
@ -84,27 +96,17 @@ func (c *Conf) setup() {
// Compute Regexes
// Look for Patterns inside Regexes
for _, regex := range filter.Regex {
for patternName, pattern := range c.Patterns {
for _, pattern := range c.Patterns {
if strings.Contains(regex, pattern.nameWithBraces) {
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,
)
if !slices.Contains(filter.pattern, pattern) {
filter.pattern = append(filter.pattern, pattern)
}
// 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.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)
}
@ -114,7 +116,7 @@ func (c *Conf) setup() {
}
for actionName := range filter.Actions {
action := filter.Actions[actionName]
action := filter.Actions[actionName]
action.filter = filter
action.name = actionName

View File

@ -19,6 +19,9 @@ 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:"-"`
}
@ -39,7 +42,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"`
@ -64,7 +67,7 @@ type Action struct {
type LogEntry struct {
T time.Time
S int64
Pattern string
Pattern []string
Stream, Filter string
SF int
Exec bool
@ -79,37 +82,43 @@ type WriteDB struct {
file *os.File
enc *gob.Encoder
}
type MatchesMap map[PF]map[time.Time]struct{}
type ActionsMap map[PA]map[time.Time]struct{}
// https://stackoverflow.com/a/69691894
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 }
type PSF struct{ p, s, f string }
// Pattern, Stream, Filter
type PSF struct{
p []string
s string
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,6 +29,8 @@ 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,24 +2,14 @@
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 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"],
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"],
filters: {
findIP: {
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 (
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,6 +1,8 @@
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=

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
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 ./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)"
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 \