Compare commits

15 Commits

19 changed files with 266 additions and 116 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

@ -8,6 +8,7 @@ import (
"net"
"os"
"regexp"
"strings"
"framagit.org/ppom/reaction/logger"
"sigs.k8s.io/yaml"
@ -137,9 +138,15 @@ func ClientShow(format, stream, filter string, regex *regexp.Regexp) {
if regex != nil {
for streamName := range response.ClientStatus {
for filterName := range response.ClientStatus[streamName] {
for patternName := range response.ClientStatus[streamName][filterName] {
if !regex.MatchString(patternName) {
delete(response.ClientStatus[streamName][filterName], patternName)
for patterns := range response.ClientStatus[streamName][filterName] {
pmatch := false
for _, p := range strings.Split(patterns, "\x00") {
if regex.MatchString(p) {
pmatch = true
}
}
if !pmatch {
delete(response.ClientStatus[streamName][filterName], patterns)
}
}
if len(response.ClientStatus[streamName][filterName]) == 0 {
@ -162,12 +169,22 @@ func ClientShow(format, stream, filter string, regex *regexp.Regexp) {
if err != nil {
logger.Fatalln("Failed to convert daemon binary response to text format:", err)
}
// Replace \0 joined string with space joined string ("1.2.3.4\0root" -> "1.2.3.4 root")
for streamName := range response.ClientStatus {
for filterName := range response.ClientStatus[streamName] {
for patterns := range response.ClientStatus[streamName][filterName] {
text = []byte(strings.ReplaceAll(string(text), strings.Join(strings.Split(patterns, "\x00"), "\\0"), strings.Join(strings.Split(patterns, "\x00"), " ")))
}
}
}
fmt.Println(string(text))
os.Exit(0)
}
func ClientFlush(pattern, streamfilter, format string) {
response := SendAndRetrieve(Request{Flush, pattern})
func ClientFlush(patterns []string, streamfilter, format string) {
response := SendAndRetrieve(Request{Flush, strings.Join(patterns, "\x00")})
if response.Err != nil {
logger.Fatalln("Received error from daemon:", response.Err)
os.Exit(1)

View File

@ -62,6 +62,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
@ -72,25 +78,38 @@ 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
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
if len(result) == 0 {
result = match
} else {
result = strings.Join([]string{result, match}, "\x00")
}
}
} 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 ""
}
}
}
return ""
if len(strings.Split(result, "\x00")) == len(f.pattern) {
return result
} else {
// Incomplete match = no match
return ""
}
}
func (f *Filter) sendActions(match string, at time.Time) {
@ -105,10 +124,14 @@ func (a *Action) exec(match string) {
var computedCommand []string
if a.filter.pattern != nil {
computedCommand := make([]string, 0, len(a.Cmd))
computedCommand = make([]string, 0, len(a.Cmd))
matches := strings.Split(match, "\x00")
for _, item := range a.Cmd {
computedCommand = append(computedCommand, strings.ReplaceAll(item, a.filter.pattern.nameWithBraces, match))
for i, p := range a.filter.pattern {
item = strings.ReplaceAll(item, p.nameWithBraces, matches[i])
}
computedCommand = append(computedCommand, item)
}
} else {
computedCommand = a.Cmd
@ -238,7 +261,7 @@ func MatchesManager() {
matchesManagerHandleFlush(fo)
case pft = <-matchesC:
entry := LogEntry{pft.t, 0, pft.p, pft.f.stream.name, pft.f.name, 0, false}
entry := LogEntry{pft.t, 0, strings.Join(strings.Split(pft.p, "\x00"), " / "), pft.f.stream.name, pft.f.name, 0, false}
entry.Exec = matchesManagerHandleMatch(pft)
@ -268,7 +291,7 @@ func matchesManagerHandleMatch(pft PFT) bool {
matchesLock.Lock()
defer matchesLock.Unlock()
filter, pattern, then := pft.f, pft.p, pft.t
filter, patterns, then := pft.f, pft.p, pft.t
pf := PF{pft.p, pft.f}
if filter.Retry > 1 {
@ -293,7 +316,7 @@ func matchesManagerHandleMatch(pft PFT) bool {
if filter.Retry <= 1 || len(matches[pf]) >= filter.Retry {
delete(matches, pf)
filter.sendActions(pattern, then)
filter.sendActions(patterns, then)
return true
}
return false
@ -312,7 +335,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

@ -60,7 +60,8 @@ func subCommandParse(f *flag.FlagSet, maxRemainingArgs int) {
basicUsage()
os.Exit(0)
}
if len(f.Args()) > maxRemainingArgs {
// -1 = no limit to remaining args
if maxRemainingArgs > -1 && len(f.Args()) > maxRemainingArgs {
fmt.Printf("ERROR unrecognized argument(s): %v\n", f.Args()[maxRemainingArgs:])
basicUsage()
os.Exit(1)
@ -102,7 +103,7 @@ func basicUsage() {
` + bold + `reaction flush` + reset + ` TARGET
# 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
# e.g. reaction flush 192.168.1.1 root
# options:
-s/--socket SOCKET # path to the client-daemon communication socket
@ -115,7 +116,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
`)
}
@ -194,7 +195,7 @@ func Main(version, commit string) {
SocketPath = addSocketFlag(f)
queryFormat := addFormatFlag(f)
limit := addLimitFlag(f)
subCommandParse(f, 1)
subCommandParse(f, -1)
if *queryFormat != "yaml" && *queryFormat != "json" {
logger.Fatalln("only yaml and json formats are supported")
f.PrintDefaults()
@ -209,7 +210,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(f.Args(), *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

@ -17,14 +17,14 @@ func genClientStatus(local_actions ActionsMap, local_matches MatchesMap, local_a
// Painful data manipulation
for pf, times := range local_matches {
pattern, filter := pf.p, pf.f
patterns, filter := pf.p, pf.f
if cs[filter.stream.name] == nil {
cs[filter.stream.name] = make(map[string]MapPatternStatus)
}
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][patterns] = &PatternStatus{len(times), nil}
}
local_matchesLock.Unlock()
@ -32,17 +32,17 @@ func genClientStatus(local_actions ActionsMap, local_matches MatchesMap, local_a
// Painful data manipulation
for pa, times := range local_actions {
pattern, action := pa.p, pa.a
patterns, action := pa.p, pa.a
if cs[action.filter.stream.name] == nil {
cs[action.filter.stream.name] = make(map[string]MapPatternStatus)
}
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][patterns] == nil {
cs[action.filter.stream.name][action.filter.name][patterns] = new(PatternStatus)
}
ps := cs[action.filter.stream.name][action.filter.name][pattern]
ps := cs[action.filter.stream.name][action.filter.name][patterns]
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 {
@ -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("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"`
@ -84,8 +87,14 @@ 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
f *Filter

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 \