New unified CLI design
fixes #25 thanks @bertille-ddp for comments && suggestions!
This commit is contained in:
parent
cbfad9303f
commit
799ba88823
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
/reaction.db
|
/reaction.db
|
||||||
/result
|
/result
|
||||||
|
/reaction
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -21,12 +23,11 @@ type Request struct {
|
|||||||
type Response struct {
|
type Response struct {
|
||||||
Err error
|
Err error
|
||||||
Actions ReadableMap
|
Actions ReadableMap
|
||||||
|
Number int
|
||||||
}
|
}
|
||||||
|
|
||||||
const SocketPath = "/run/reaction/reaction.sock"
|
|
||||||
|
|
||||||
func SendAndRetrieve(data Request) Response {
|
func SendAndRetrieve(data Request) Response {
|
||||||
conn, err := net.Dial("unix", SocketPath)
|
conn, err := net.Dial("unix", *SocketPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Error opening connection top daemon:", err)
|
log.Fatalln("Error opening connection top daemon:", err)
|
||||||
}
|
}
|
||||||
@ -50,26 +51,37 @@ func usage(err string) {
|
|||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CLI() {
|
func ClientQuery(streamfilter string) {
|
||||||
if len(os.Args) <= 1 {
|
response := SendAndRetrieve(Request{Query, streamfilter})
|
||||||
response := SendAndRetrieve(Request{Query, ""})
|
|
||||||
if response.Err != nil {
|
if response.Err != nil {
|
||||||
log.Fatalln("Received error from daemon:", response.Err)
|
log.Fatalln("Received error from daemon:", response.Err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Println(response.Actions.ToString())
|
fmt.Println(response.Actions.ToString())
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
switch os.Args[1] {
|
|
||||||
case "flush":
|
func ClientFlush(pattern, streamfilter string) {
|
||||||
if len(os.Args) != 3 {
|
response := SendAndRetrieve(Request{Flush, pattern})
|
||||||
usage("flush takes one <PATTERN> argument")
|
|
||||||
}
|
|
||||||
response := SendAndRetrieve(Request{Flush, os.Args[2]})
|
|
||||||
if response.Err != nil {
|
if response.Err != nil {
|
||||||
log.Fatalln("Received error from daemon:", response.Err)
|
log.Fatalln("Received error from daemon:", response.Err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
fmt.Printf("flushed pattern %v times\n", response.Number)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
default:
|
}
|
||||||
usage("first argument must be `flush`")
|
|
||||||
|
func Match(reg *regexp.Regexp, line string) {
|
||||||
|
if reg.MatchString(line) {
|
||||||
|
fmt.Printf("\033[32mmatching\033[0m: %v\n", line)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\033[31mno match\033[0m: %v\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchStdin(reg *regexp.Regexp) {
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for scanner.Scan() {
|
||||||
|
Match(reg, scanner.Text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package app
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"flag"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
// "fmt"
|
// "fmt"
|
||||||
@ -198,18 +197,10 @@ var wgActions sync.WaitGroup
|
|||||||
|
|
||||||
var db *gob.Encoder
|
var db *gob.Encoder
|
||||||
|
|
||||||
func Main() {
|
func Daemon(confFilename string) {
|
||||||
confFilename := flag.String("c", "", "configuration file. see an example at https://framagit.org/ppom/reaction/-/blob/main/reaction.yml")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *confFilename == "" {
|
|
||||||
flag.PrintDefaults()
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
actionStore.store = make(ActionMap)
|
actionStore.store = make(ActionMap)
|
||||||
|
|
||||||
conf := parseConf(*confFilename)
|
conf := parseConf(confFilename)
|
||||||
db = conf.updateFromDB()
|
db = conf.updateFromDB()
|
||||||
|
|
||||||
// Ready to start
|
// Ready to start
|
||||||
@ -226,7 +217,7 @@ func Main() {
|
|||||||
go stream.handle(endSignals)
|
go stream.handle(endSignals)
|
||||||
}
|
}
|
||||||
|
|
||||||
go Serve()
|
go ServeSocket()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@ -251,7 +242,7 @@ func quit() {
|
|||||||
// wait for them to complete
|
// wait for them to complete
|
||||||
wgActions.Wait()
|
wgActions.Wait()
|
||||||
// delete pipe
|
// delete pipe
|
||||||
err := os.Remove(SocketPath)
|
err := os.Remove(*SocketPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to remove socket:", err)
|
log.Println("Failed to remove socket:", err)
|
||||||
}
|
}
|
196
app/main.go
Normal file
196
app/main.go
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addStringFlag(names []string, defvalue string, f *flag.FlagSet) *string {
|
||||||
|
var value string
|
||||||
|
for _, name := range names {
|
||||||
|
f.StringVar(&value, name, defvalue, "")
|
||||||
|
}
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
func addBoolFlag(names []string, f *flag.FlagSet) *bool {
|
||||||
|
var value bool
|
||||||
|
for _, name := range names {
|
||||||
|
f.BoolVar(&value, name, false, "")
|
||||||
|
}
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
var SocketPath *string
|
||||||
|
|
||||||
|
func addSocketFlag(f *flag.FlagSet) *string {
|
||||||
|
return addStringFlag(
|
||||||
|
[]string{"s", "socket"},
|
||||||
|
"/run/reaction/reaction.sock",
|
||||||
|
f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addConfFlag(f *flag.FlagSet) *string {
|
||||||
|
return addStringFlag(
|
||||||
|
[]string{"c", "config"},
|
||||||
|
"",
|
||||||
|
f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFormatFlag(f *flag.FlagSet) *string {
|
||||||
|
return addStringFlag(
|
||||||
|
[]string{"f", "format"},
|
||||||
|
"yaml",
|
||||||
|
f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func subCommandParse(f *flag.FlagSet, maxRemainingArgs int) {
|
||||||
|
help := addBoolFlag([]string{"h", "help"}, f)
|
||||||
|
f.Parse(os.Args[2:])
|
||||||
|
if *help {
|
||||||
|
basicUsage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
if len(f.Args()) > maxRemainingArgs {
|
||||||
|
fmt.Printf("ERROR unrecognized argument(s): %v\n", f.Args()[maxRemainingArgs:])
|
||||||
|
basicUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func basicUsage() {
|
||||||
|
const (
|
||||||
|
bold = "\033[1m"
|
||||||
|
reset = "\033[0m"
|
||||||
|
)
|
||||||
|
fmt.Print(`usage:
|
||||||
|
|
||||||
|
` + bold + `reaction start` + reset + `
|
||||||
|
# start the daemon
|
||||||
|
|
||||||
|
# options:
|
||||||
|
-c/--config CONFIG_FILE # configuration file (required)
|
||||||
|
-s/--socket SOCKET # path to the client-daemon communication socket
|
||||||
|
# (default: /run/reaction/reaction.sock)
|
||||||
|
|
||||||
|
` + bold + `reaction example-conf` + reset + `
|
||||||
|
# print a configuration file example
|
||||||
|
|
||||||
|
` + bold + `reaction show` + reset + ` [.STREAM[.FILTER]]
|
||||||
|
# show which actions are still to be run
|
||||||
|
# (e.g know what is currenly banned)
|
||||||
|
|
||||||
|
# optional argument: limit to STREAM and FILTER
|
||||||
|
|
||||||
|
# options:
|
||||||
|
-f/--format yaml|json # (default: yaml)
|
||||||
|
-s/--socket SOCKET # path to the client-daemon communication socket
|
||||||
|
|
||||||
|
` + bold + `reaction flush` + reset + ` TARGET [.STREAM[.FILTER]]
|
||||||
|
# run currently pending actions for the specified TARGET
|
||||||
|
|
||||||
|
# optional argument: limit to STREAM and FILTER
|
||||||
|
|
||||||
|
# options:
|
||||||
|
-s/--socket SOCKET # path to the client-daemon communication socket
|
||||||
|
|
||||||
|
` + bold + `reaction test-regex` + reset + ` REGEX LINE # test REGEX against LINE
|
||||||
|
cat FILE | ` + bold + `reaction test-regex` + reset + ` REGEX # test REGEX against each line of FILE
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed reaction.yml
|
||||||
|
var exampleConf string
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
if len(os.Args) <= 1 {
|
||||||
|
fmt.Println("No argument provided")
|
||||||
|
basicUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
} else if os.Args[1] == "-h" || os.Args[1] == "--help" {
|
||||||
|
basicUsage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
f := flag.NewFlagSet(os.Args[1], flag.ContinueOnError)
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "help", "-h", "--help":
|
||||||
|
basicUsage()
|
||||||
|
|
||||||
|
case "example-conf":
|
||||||
|
subCommandParse(f, 0)
|
||||||
|
fmt.Print(exampleConf)
|
||||||
|
|
||||||
|
case "start":
|
||||||
|
SocketPath = addSocketFlag(f)
|
||||||
|
confFilename := addConfFlag(f)
|
||||||
|
subCommandParse(f, 0)
|
||||||
|
if *confFilename == "" {
|
||||||
|
fmt.Println("no configuration file provided")
|
||||||
|
basicUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
Daemon(*confFilename)
|
||||||
|
|
||||||
|
case "show":
|
||||||
|
SocketPath = addSocketFlag(f)
|
||||||
|
queryFormat := addFormatFlag(f)
|
||||||
|
subCommandParse(f, 1)
|
||||||
|
// if *queryFormat != "yaml" && *queryFormat != "json" {
|
||||||
|
// fmt.Println("only `yaml` and `json` formats are supported.")
|
||||||
|
// f.PrintDefaults()
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
if *queryFormat != "yaml" {
|
||||||
|
fmt.Println("for now, only `yaml` format is supported.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if f.Arg(0) != "" {
|
||||||
|
fmt.Println("for now, .STREAM.FILTER is not supported")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// f.Arg(0) is "" if there is no remaining argument
|
||||||
|
ClientQuery(f.Arg(0))
|
||||||
|
|
||||||
|
case "flush":
|
||||||
|
SocketPath = addSocketFlag(f)
|
||||||
|
subCommandParse(f, 2)
|
||||||
|
if f.Arg(0) == "" {
|
||||||
|
fmt.Println("subcommand flush takes at least one TARGET argument")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if f.Arg(1) != "" {
|
||||||
|
fmt.Println("for now, the .stream[.filter] argument is not supported")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ClientFlush(f.Arg(0), f.Arg(1))
|
||||||
|
|
||||||
|
case "test-regex":
|
||||||
|
// socket not needed, no interaction with the daemon
|
||||||
|
subCommandParse(f, 2)
|
||||||
|
if f.Arg(0) == "" {
|
||||||
|
fmt.Println("subcommand test-regex takes at least one REGEX argument")
|
||||||
|
basicUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
regex, err := regexp.Compile(f.Arg(0))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("ERROR the specified regex is invalid: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if f.Arg(1) == "" {
|
||||||
|
fmt.Println("INFO no second argument. reading from stdin.")
|
||||||
|
|
||||||
|
MatchStdin(regex)
|
||||||
|
} else {
|
||||||
|
Match(regex, f.Arg(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Println("subcommand not recognized")
|
||||||
|
basicUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
19
app/pipe.go
19
app/pipe.go
@ -60,17 +60,20 @@ func (a *ActionStore) Quit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Called by a CLI
|
// Called by a CLI
|
||||||
func (a *ActionStore) Flush(pattern string) {
|
func (a *ActionStore) Flush(pattern string) int {
|
||||||
|
var cpt int
|
||||||
a.mutex.Lock()
|
a.mutex.Lock()
|
||||||
defer a.mutex.Unlock()
|
defer a.mutex.Unlock()
|
||||||
if a.store[pattern] != nil {
|
if a.store[pattern] != nil {
|
||||||
for _, action := range a.store[pattern] {
|
for _, action := range a.store[pattern] {
|
||||||
for sig := range action {
|
for sig := range action {
|
||||||
close(sig)
|
sig <- true
|
||||||
}
|
}
|
||||||
|
cpt++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete(a.store, pattern)
|
delete(a.store, pattern)
|
||||||
|
return cpt
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by a CLI
|
// Called by a CLI
|
||||||
@ -111,19 +114,19 @@ func (r ReadableMap) ToString() string {
|
|||||||
// Socket-related, server-related functions
|
// Socket-related, server-related functions
|
||||||
|
|
||||||
func createOpenSocket() net.Listener {
|
func createOpenSocket() net.Listener {
|
||||||
err := os.MkdirAll(path.Dir(SocketPath), 0755)
|
err := os.MkdirAll(path.Dir(*SocketPath), 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("FATAL Failed to create socket directory")
|
log.Fatalln("FATAL Failed to create socket directory")
|
||||||
}
|
}
|
||||||
_, err = os.Stat(SocketPath)
|
_, err = os.Stat(*SocketPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Println("WARN socket", SocketPath, "already exists: Is the daemon already running? Deleting.")
|
log.Println("WARN socket", SocketPath, "already exists: Is the daemon already running? Deleting.")
|
||||||
err = os.Remove(SocketPath)
|
err = os.Remove(*SocketPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("FATAL Failed to remove socket:", err)
|
log.Fatalln("FATAL Failed to remove socket:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ln, err := net.Listen("unix", SocketPath)
|
ln, err := net.Listen("unix", *SocketPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("FATAL Failed to create socket:", err)
|
log.Fatalln("FATAL Failed to create socket:", err)
|
||||||
}
|
}
|
||||||
@ -131,7 +134,7 @@ func createOpenSocket() net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle connections
|
// Handle connections
|
||||||
func Serve() {
|
func ServeSocket() {
|
||||||
ln := createOpenSocket()
|
ln := createOpenSocket()
|
||||||
defer ln.Close()
|
defer ln.Close()
|
||||||
for {
|
for {
|
||||||
@ -154,7 +157,7 @@ func Serve() {
|
|||||||
case Query:
|
case Query:
|
||||||
response.Actions = actionStore.store.ToReadable()
|
response.Actions = actionStore.store.ToReadable()
|
||||||
case Flush:
|
case Flush:
|
||||||
actionStore.Flush(request.Pattern)
|
response.Number = actionStore.Flush(request.Pattern)
|
||||||
default:
|
default:
|
||||||
log.Println("ERROR Invalid Message from cli: unrecognised Request type")
|
log.Println("ERROR Invalid Message from cli: unrecognised Request type")
|
||||||
return
|
return
|
||||||
|
54
app/reaction.yml
Normal file
54
app/reaction.yml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
# definitions are just a place to put chunks of conf you want to reuse in another place
|
||||||
|
# they're not readed by reaction
|
||||||
|
definitions:
|
||||||
|
- &iptablesban [ "iptables" "-w" "-A" "reaction" "1" "-s" "<ip>" "-j" "DROP" ]
|
||||||
|
- &iptablesunban [ "iptables" "-w" "-D" "reaction" "1" "-s" "<ip>" "-j" "DROP" ]
|
||||||
|
|
||||||
|
# patterns are substitued in regexes.
|
||||||
|
# when a filter performs an action, it replaces the found pattern
|
||||||
|
patterns:
|
||||||
|
ip:
|
||||||
|
# reaction regex syntax is defined here: https://github.com/google/re2/wiki/Syntax
|
||||||
|
regex: '(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})'
|
||||||
|
ignore:
|
||||||
|
- 127.0.0.1
|
||||||
|
- ::1
|
||||||
|
|
||||||
|
# streams are command that are run
|
||||||
|
# their output will be used by one or more filters
|
||||||
|
streams:
|
||||||
|
# streams have a user-defined name
|
||||||
|
ssh:
|
||||||
|
# note that if the command is not in environment's `PATH`
|
||||||
|
# its full path must be given.
|
||||||
|
cmd: [ "journalctl" "-fu" "sshd.service" ]
|
||||||
|
# filters are a set of regexes on a stream
|
||||||
|
# when a regex matches, it will trigger the filter's actions
|
||||||
|
filters:
|
||||||
|
# filters have a user-defined name
|
||||||
|
failedlogin:
|
||||||
|
# reaction regex syntax is defined here: https://github.com/google/re2/wiki/Syntax
|
||||||
|
regex:
|
||||||
|
- authentication failure;.*rhost=<ip>
|
||||||
|
# if retry and retry-period are defined,
|
||||||
|
# the actions will only take place if a same pattern is
|
||||||
|
# found `retry` times in a `retry-period` interval
|
||||||
|
retry: 3
|
||||||
|
# format is defined here: https://pkg.go.dev/time#ParseDuration
|
||||||
|
retry-period: 6h
|
||||||
|
actions:
|
||||||
|
# actions have a user-defined name
|
||||||
|
ban:
|
||||||
|
# YAML substitutes *reference by the value at &reference
|
||||||
|
cmd: *iptablesban
|
||||||
|
unban:
|
||||||
|
cmd: *iptablesunban
|
||||||
|
# if after is defined, the action will not take place immediately, but after a specified duration.
|
||||||
|
# same format as retry-period
|
||||||
|
after: 48h
|
||||||
|
# let's say reaction is quitting. does it run all those pending commands which had an `after` duration set?
|
||||||
|
# if you want reaction to run those pending commands before exiting, you can set this:
|
||||||
|
# onexit: true
|
||||||
|
# (defaults to false)
|
||||||
|
# here it is not useful because we will flush the chain containing the bans anyway (see ./reaction.service)
|
@ -17,3 +17,6 @@ streams:
|
|||||||
actions:
|
actions:
|
||||||
damn:
|
damn:
|
||||||
cmd: [ "echo", "<ip>" ]
|
cmd: [ "echo", "<ip>" ]
|
||||||
|
undamn:
|
||||||
|
cmd: [ "echo", "undamn", "<ip>" ]
|
||||||
|
after: 1m
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
---
|
|
||||||
# definitions are just a place to put chunks of conf you want to reuse in another place
|
|
||||||
# they're not readed by reaction
|
|
||||||
definitions:
|
|
||||||
- &iptablesban [ "iptables" "-w" "-A" "reaction" "1" "-s" "<ip>" "-j" "DROP" ]
|
|
||||||
- &iptablesunban [ "iptables" "-w" "-D" "reaction" "1" "-s" "<ip>" "-j" "DROP" ]
|
|
||||||
|
|
||||||
# patterns are substitued in regexes.
|
|
||||||
# when a filter performs an action, it replaces the found pattern
|
|
||||||
patterns:
|
|
||||||
ip:
|
|
||||||
regex: '(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})'
|
|
||||||
ignore:
|
|
||||||
- 127.0.0.1
|
|
||||||
- ::1
|
|
||||||
|
|
||||||
# streams are command that are run
|
|
||||||
# their output will be used by one or more filters
|
|
||||||
streams:
|
|
||||||
# streams have a user-defined name
|
|
||||||
ssh:
|
|
||||||
# note that if the command is not in environment's `PATH`
|
|
||||||
# its full path must be given.
|
|
||||||
cmd: [ "journalctl" "-fu" "sshd.service" ]
|
|
||||||
# filters are a set of regexes on a stream
|
|
||||||
# when a regex matches, it will trigger the filter's actions
|
|
||||||
filters:
|
|
||||||
# filters have a user-defined name
|
|
||||||
failedlogin:
|
|
||||||
regex:
|
|
||||||
- authentication failure;.*rhost=<ip>
|
|
||||||
# if retry and retry-period are defined,
|
|
||||||
# the actions will only take place if a same pattern is
|
|
||||||
# found `retry` times in a `retry-period` interval
|
|
||||||
retry: 3
|
|
||||||
# format is defined here: https://pkg.go.dev/time#ParseDuration
|
|
||||||
retry-period: 6h
|
|
||||||
actions:
|
|
||||||
# actions have a user-defined name
|
|
||||||
ban:
|
|
||||||
# YAML substitutes *reference by the value at &reference
|
|
||||||
cmd: *iptablesban
|
|
||||||
unban:
|
|
||||||
cmd: *iptablesunban
|
|
||||||
# if after is defined, the action will not take place immediately, but after a specified duration.
|
|
||||||
# same format as retry-period
|
|
||||||
after: 48h
|
|
||||||
# let's say reaction is quitting. does it run all those pending commands which had an `after` duration set?
|
|
||||||
# if you want reaction to run those pending commands before exiting, you can set this:
|
|
||||||
# onexit: true
|
|
||||||
# (defaults to false)
|
|
||||||
# here it is not useful because we will flush the chain containing the bans anyway (see ./reaction.service)
|
|
1
config/reaction.yml
Symbolic link
1
config/reaction.yml
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
app/reaction.yml
|
@ -1,9 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"framagit.org/ppom/reaction/app"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app.CLI()
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user