mesh/merge-gate head 93d58f3740a0 merged into trunk 5cd608ba8898 — pass: builds fail2ban → ace, g14, novox, shanks; no bus step; every machine composes wit…
mesh/repo-check head 93d58f3740a0 merged into trunk 5cd608ba8898 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery failed: its walk failed, carrying its own commit: a gate on a first machine (what it carried put back), a build, a machine
An agent polling the forge from the operator's home got that home banned for a day and then four weeks, and nothing in the house reached the control-node. Every jail spared only private ranges. The mesh-wide setting own-addresses reaches the machine as a mergeable file whose own content is the empty list, so the module composes while it is unset. A new watch process (root) writes its entries into a list every jail's base ignoreip names as file:, which fail2ban reloads without a restart; it checks every jail names that list and keeps each failure a jail ignored because of it for a day. The new fail2ban_health tool judges the watch, so the controller raises a warning naming node, jail, address and count. The ban verb refuses an address on the setting or on the jail's ignore list before the daemon is asked.
90 lines
4.4 KiB
Go
90 lines
4.4 KiB
Go
// fail2ban-tools (novox/hq to-be 31, ADR 0179): the intrusion prevention's tools. One binary, launched
|
|
// by the machine's tool runtime and speaking MCP to it over stdio through the Go SDK (ADR 0193, ADR
|
|
// 0198): the node-intrusion-prevention seat's four verbs — who is banned, the jails' state, ban one,
|
|
// let one go — and the module's own reading of a jail's settings. The jails themselves are composed
|
|
// by the mesh from the modules a machine runs and written as declared resources; these touch only
|
|
// what the running daemon holds.
|
|
//
|
|
// Started as `fail2ban-tools watch` it is the module's long-running process, as root (watch.go, novox/hq
|
|
// ADR 0339): it writes the operator's own addresses into the list every jail ignores, checks that every jail
|
|
// names that list, and keeps every failure a jail ignored from one of them, for the health tool to say.
|
|
//
|
|
// stdout is the MCP channel; everything this module says, it says on stderr.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// Seat is the role this module holds.
|
|
const Seat = "node-intrusion-prevention"
|
|
|
|
func main() {
|
|
if len(os.Args) > 1 {
|
|
if os.Args[1] != "watch" || len(os.Args) != 2 {
|
|
fmt.Fprintln(os.Stderr, "usage: fail2ban-tools [watch]")
|
|
os.Exit(2)
|
|
}
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
|
|
defer stop()
|
|
w := &Watcher{F: Fail2ban{Run: execRunner}, Now: time.Now}
|
|
if err := w.Watch(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "[fail2ban] %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
if err := stdio.Serve("", tools(Fail2ban{Run: execRunner})); err != nil {
|
|
fmt.Fprintf(os.Stderr, "[fail2ban] %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func str(description string) map[string]any {
|
|
return map[string]any{"type": "string", "description": description}
|
|
}
|
|
|
|
func arg(a map[string]any, k string) string {
|
|
v, _ := a[k].(string)
|
|
return strings.TrimSpace(v)
|
|
}
|
|
|
|
// verb is one of the seat's verbs: listed as `<seat>.<verb>`, so the runtime serves it on the seat's
|
|
// subject. The module's own tools keep their bare names.
|
|
func verb(name, description string, input map[string]any, run func(a map[string]any) (any, error)) stdio.Tool {
|
|
return stdio.Tool{Name: Seat + "." + name, Description: description, Input: input, Run: run}
|
|
}
|
|
|
|
func tools(f Fail2ban) []stdio.Tool {
|
|
ctx := context.Background()
|
|
oneJail := map[string]any{"jail": str("one jail (optional)")}
|
|
return []stdio.Tool{
|
|
verb("status", "Every jail on this machine with what it watches, how many addresses it is counting failures against and holding now, and the totals since it started; one jail's detail when named.",
|
|
oneJail, func(a map[string]any) (any, error) { return f.Status(ctx, arg(a, "jail")) }),
|
|
verb("banned", "Every address banned on this machine right now, with the jail that holds it, when it was banned and when the ban ends.",
|
|
oneJail, func(a map[string]any) (any, error) { return f.Banned(ctx, arg(a, "jail")) }),
|
|
verb("ban", "Ban one address in one jail now, for the jail's ban time — an operator's act on the live ban list, which the mesh never writes itself.",
|
|
map[string]any{"ip": str("the address"), "jail": str("the jail to hold it (recidive for the long ban)")},
|
|
func(a map[string]any) (any, error) { return f.Ban(ctx, arg(a, "ip"), arg(a, "jail")) }),
|
|
verb("unban", "Let one address go, from one jail or from every jail when none is named.",
|
|
map[string]any{"ip": str("the address"), "jail": str("one jail (optional)")},
|
|
func(a map[string]any) (any, error) { return f.Unban(ctx, arg(a, "ip"), arg(a, "jail")) }),
|
|
{Name: "fail2ban_settings",
|
|
Description: "One jail's effective settings on this machine: ban time, window, tries, the addresses it never bans, its actions and what it reads.",
|
|
Input: map[string]any{"jail": str("the jail")},
|
|
Run: func(a map[string]any) (any, error) { return f.Settings(ctx, arg(a, "jail")) }},
|
|
{Name: "fail2ban_health",
|
|
Description: "Whether every jail on this machine ignores the operator's own addresses (the mesh-wide setting own-addresses, novox/hq ADR 0339) and none of them failed in the last day: each jail and address with the failures it ignored, how many and when the last was, and what the module's watch last saw. Not healthy while one did, so the mesh raises it as a warning; it clears a day after the last.",
|
|
Input: map[string]any{},
|
|
Run: func(map[string]any) (any, error) { return ReadHealth(f.Root, time.Now()), nil }},
|
|
}
|
|
}
|