package catalogue import ( "fmt" "sort" "strings" ) // A node's fail2ban jails, composed from the modules it runs (novox/hq to-be 31). // // **The same shape as the firewall.** Every module's `listens` become the node's rule set; every // module's `jails` become the node's fail2ban config. A module that runs an authenticating service // declares what a break-in on it looks like and how to ban it, naming no node and no path (ADR // 0112); the intrusion-prevention holder — the one module with `jailing` — gathers them and writes // them where it owns. A node not running a module has none of its jails. // jailsInto composes every jail declared by the modules on a node into the files the holder writes: // one jail file (all stanzas, so the fail2ban service restarts on a single resource) and one filter // file per jail (its failregex, which fail2ban references by the jail's name). // // Owned by the holder, because the directory is: two modules writing into one fail2ban is the // collision the holder model exists to prevent. Empty when nothing declares a jail — then the file // is written empty rather than absent, so removing the last jail is an ordinary change the service // restarts on rather than a file that vanishes. func jailsInto(modules []Manifest, j *Jailing) []map[string]any { type declared struct { module string jail Jail } var jails []declared for _, m := range modules { for _, jail := range m.Jails { jails = append(jails, declared{m.Module, jail}) } } // A stable order the host applies as given (ADR 0005), and so the same set composes byte for // byte every time rather than differing by map iteration. sort.Slice(jails, func(a, b int) bool { return jails[a].jail.Name < jails[b].jail.Name }) var composed strings.Builder composed.WriteString("# The mesh's jails, composed from the modules this node runs. Do not edit —\n") composed.WriteString("# replaced whenever the node's modules change (novox/hq to-be 31).\n") out := make([]map[string]any, 0, len(jails)+1) for _, d := range jails { fmt.Fprintf(&composed, "\n# from %s\n[%s]\nenabled = true\nfilter = %s\n%s\n", d.module, d.jail.Name, d.jail.Name, strings.TrimRight(d.jail.Jail, "\n")) // The filter is a file of its own, named as the jail's filter= references it. out = append(out, map[string]any{ "id": "filter-" + d.jail.Name, "type": "file", "path": strings.TrimRight(j.FilterInto, "/") + "/" + d.jail.Name + ".conf", "mode": "0644", "content": "# Generated by the mesh (from module " + d.module + "). Do not edit.\n" + "[Definition]\nfailregex = " + d.jail.Failregex + "\n", }) } // The one jail file, first, with the fixed id the fail2ban service names in its restart-on. return append([]map[string]any{{ "id": ComposedJailsID(), "type": "file", "path": j.Into, "mode": "0644", "content": composed.String(), }}, out...) }