# reaction

A daemon that tails program output, watches for patterns that repeat too often, and runs a command when they do. The canonical use is what fail2ban does: watch ssh and webserver logs, count failed logins per source IP, and ban a host that trips the threshold. But the mechanism is generic — a stream is any command whose stdout you want to watch, an action is any command you want to run, so it isn't tied to firewalls or to banning.

The pitch against fail2ban is speed and configuration weight. The author was running fail2ban for years and got tired of its CPU use and its heavy default config. reaction has fewer features but claims more than 10x lower CPU, and a config you can read top to bottom. It's funded through NLnet's NGI0 Core fund (EU Next Generation Internet money), not a solo hobby project in the abandonment sense, though it is single-author.

Version 2.x is a complete Rust rewrite at feature parity with the old Go version (1.x, now deprecated).

## How it works

The config is a single file — YAML or JSONnet, both supersets of JSON, so plain JSON works too. JSONnet is the more capable option because you can write functions and reuse them (e.g. a `banFor(time)` helper that generates matching ban/unban action pairs).

Four moving parts:

- **patterns** — named capture types, e.g. `ip: { type: ipv4 }`. A pattern named `ip` is referenced in regexes as `<ip>`. Note this is not the usual `(...)` capture-group / `$1` substitution style; reaction uses named `<placeholder>` tokens, which surprised at least one reader but reads cleanly.
- **start / stop** — lists of commands run once when the daemon comes up and goes down. Typically these create and tear down the firewall chain the ban actions write into.
- **streams** — each stream has a `cmd` that produces a line feed (`journalctl -fu sshd.service`, a `tail -f`, anything on stdout) and a set of **filters**.
- **filters** — each filter has a list of `regex`, a `retry` count, a `retryperiod` window, and named **actions**. When the same captured value (say one IP) matches `retry` times inside `retryperiod`, the filter fires its actions. An action is a `cmd`; an action can carry an `after` delay so the daemon schedules it for later — that's how `unban` runs 48h after `ban`.

Pending delayed actions (the current bans) are kept in an embedded on-disk database in the working directory, overridable with `state_directory`. The recommendation is `/var/lib/reaction`. Because state is persisted, bans survive a restart.

## Usage

Minimal ssh brute-force protection, `/etc/reaction.yml`:

```yaml
patterns:
  ip:
    type: ipv4

start:
  - [ 'iptables', '-w', '-N', 'reaction' ]
  - [ 'iptables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]

stop:
  - [ 'iptables', '-w', '-F', 'reaction' ]
  - [ 'iptables', '-w', '-X', 'reaction' ]

streams:
  ssh:
    cmd: [ 'journalctl', '-fu', 'sshd.service' ]
    filters:
      failedlogin:
        regex:
          - 'Failed password for .* from <ip>'
          - 'Invalid user .* from <ip>'
        retry: 3
        retryperiod: '6h'
        actions:
          ban:
            cmd: [ 'iptables', '-w', '-I', 'reaction', '1', '-s', '<ip>', '-j', 'DROP' ]
          unban:
            cmd: [ 'iptables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-j', 'DROP' ]
            after: '48h'
```

The CLI is small:

- `reaction start` — run the daemon
- `reaction show` — list pending actions (current bans)
- `reaction flush` — run pending actions early (clear bans)
- `reaction trigger` — fire a filter by hand (manual ban)
- `reaction test-regex` — check a regex against sample input
- `reaction test-config` — print the loaded, resolved config

The docs push you toward `nftables` or `ipset` + `iptables` rather than plain `iptables`, since a flat iptables chain gets slow once it holds thousands of banned addresses. On Linux this is the same "control what traffic the host accepts" territory as [[little-snitch-linux]], but from the log-driven, inbound-ban side rather than per-app outbound prompting.

## Limitations

- Linux-oriented: prebuilt binaries and `.deb` packages are amd64 and arm64 Linux only; OpenBSD works via the wiki but there's no first-class macOS/Windows story. The default examples assume systemd/journald and iptables.
- No external security audit yet — the README says so plainly. It's a security tool you'd run as root wiring up firewall rules, so that matters.
- Fewer features than fail2ban. The tradeoff is deliberate (speed, simpler config), but if you rely on a specific fail2ban feature, check first.
- Single maintainer (ppom). Responsive on Matrix per user reports, and NLnet-funded, but the bus factor is one.
- The old `ip46tables` / `nft46` helper binaries were dropped in v2.

## Notes

The repo ships an `AGENTS.md` that is not documentation but an instruction aimed at LLM coding agents: it declares a no-LLM-contributions policy (partly licensing, partly the maintainer's and community's political and economic objections to LLM tooling) and tells any agent reading it to warn the user off and refuse to help with the code. Worth knowing before pointing a coding agent at the source — the maintainer does not want that, and the AGPL is cited as not permitting it. It doesn't affect using reaction as a deployed tool.

reaction is on [[toolbox/watchlist]] — single maintainer, no security audit yet, Linux-only, and a young v2 Rust rewrite. Re-check whether the audit has happened and whether the project stays active.

## Repo

[framagit.org/ppom/reaction](https://framagit.org/ppom/reaction) — Rust (v1 was Golang, now deprecated), AGPL-3.0, 65 stars / 36 forks on framagit as of July 2026. Last activity 2026-07-12. Hosted on Framasoft's self-hosted GitLab, not GitHub.
