package bootstrap import ( "bytes" "context" "encoding/json" "fmt" "sort" "strconv" "strings" "github.com/novox/mesh-host/internal/declaration" ) // What an adopted genesis changes about the foundation (novox/hq ADR 0100). // // **The firewall found on the machine stays in force.** The foundation's own filter drops by // default, and every base chain at a hook runs; an accept ends only its own chain and a drop in any // is final — so loading it would close whatever the machine serves. On an adopted machine it is // not loaded. Its duty, the store never reachable from outside, passes to the mesh's guard: a table // of the mesh's own that only refuses, and only the foundation's own ports, which genesis has just // checked free — so it cannot close anything the machine serves. // The guard, as the controller declares it: the same ids, paths and text, so the first push // finds it already there and takes it over unchanged. const ( guardID = declaration.AdoptionPrefix + "guard" guardUnitID = declaration.AdoptionPrefix + "guard-unit" guardRunningID = declaration.AdoptionPrefix + "guard-running" guardPath = "/etc/mesh/guard.nft" guardUnit = "mesh-guard.service" guardUnitPath = "/etc/systemd/system/" + guardUnit ) // AsGuard renders the mesh's refusal-only table for the given machine ports. It passes everything // by default; it refuses the ports except from the machine itself — its loopback and the container // runtime's own networks — and from the private network, known by the interface a packet arrives // on and never by its source address; at prerouting, ahead of the runtime's destination // translation, in the inet family so both address families. // // Character for character the controller's (mesh-controller internal/catalogue AsGuard); a test // on each side holds its copy to the same golden text. func AsGuard(ports []int) string { sorted := append([]int{}, ports...) sort.Ints(sorted) listed := make([]string, len(sorted)) for i, p := range sorted { listed[i] = strconv.Itoa(p) } var b strings.Builder b.WriteString("table inet mesh_guard {}\n") b.WriteString("delete table inet mesh_guard\n") b.WriteString("table inet mesh_guard {\n") b.WriteString("\tchain prerouting {\n") b.WriteString("\t\ttype filter hook prerouting priority raw; policy accept;\n") fmt.Fprintf(&b, "\t\tiifname != \"lo\" iifname != \"docker0\" iifname != \"br-*\" "+ "iifname != \"mesh0\" tcp dport { %s } drop\n", strings.Join(listed, ", ")) b.WriteString("\t}\n") b.WriteString("}\n") return b.String() } // guardUnitText is the unit that loads the guard. Stopping it deletes only its own table — never a // flush, which would take the container runtime's rules and the found firewall with it. func guardUnitText() string { return "[Unit]\n" + "Description=The mesh's guard: refuses its own ports from outside (novox/hq ADR 0100)\n" + "After=network-pre.target\n" + "Wants=network-pre.target\n" + "\n" + "[Service]\n" + "Type=oneshot\n" + "RemainAfterExit=yes\n" + "ExecStart=nft -f " + guardPath + "\n" + "ExecReload=nft -f " + guardPath + "\n" + "ExecStop=nft delete table inet mesh_guard\n" + "\n" + "[Install]\n" + "WantedBy=multi-user.target\n" } // guardResources are the guard as three resources of kinds the host already has. func guardResources(ports []int) []map[string]any { return []map[string]any{ {"id": guardID, "type": "file", "path": guardPath, "content": AsGuard(ports), "mode": "0644"}, {"id": guardUnitID, "type": "file", "path": guardUnitPath, "content": guardUnitText(), "mode": "0644"}, {"id": guardRunningID, "type": "service", "unit": guardUnit, "state": "running", "boot": "enabled", "restart-on": []any{guardID, guardUnitID}}, } } // guardAfter is where the guard goes: once the container runtime runs, before anything publishes // a port. const guardAfter = "container-runtime-running" // AdoptedRewrite says what RewriteAdopted did. type AdoptedRewrite struct { Removed []string Guarded []int } // RewriteAdopted makes the produced bundle one for an adopted machine: the foundation's own filter // taken out, and the mesh's guard put in its place, guarding the store's and the broker's // management ports on this node. The nftables package stays: the guard is loaded with it, and // installing a package loads no table. Openings are not the bundle's — the first push declares // them, once there is a controller to derive them. func RewriteAdopted(r *Rewritten, p FoundationPorts) (AdoptedRewrite, error) { var out AdoptedRewrite p = p.orDefaults() bundle := r.Bundle var err error for _, id := range []string{"base-filter-loaded", "base-filter"} { if !r.declares(id) { continue } if bundle, err = removeResource(bundle, id); err != nil { return out, err } out.Removed = append(out.Removed, id) } out.Guarded = []int{p.Store, p.Management} var text bytes.Buffer text.WriteString(",\n // The mesh's guard (novox/hq ADR 0100): this machine is adopted, so its own firewall\n" + " // stays in force and the foundation's filter is not loaded. The guard only refuses: the\n" + " // store's and the broker's management ports, except from the machine and the private network.") for _, res := range guardResources(out.Guarded) { var one bytes.Buffer enc := json.NewEncoder(&one) enc.SetEscapeHTML(false) if err := enc.Encode(res); err != nil { return out, err } text.WriteString("\n ") text.Write(bytes.TrimSpace(one.Bytes())) text.WriteString(",") } insert := bytes.TrimSuffix(text.Bytes(), []byte(",")) _, _, to, err := resourceAt(bundle, guardAfter) if err != nil { return out, fmt.Errorf("the guard goes after %q, and %w", guardAfter, err) } rest := bundle[to:] joined := make([]byte, 0, len(bundle)+len(insert)) joined = append(joined, bundle[:to]...) joined = append(joined, insert...) // What followed the resource — its own comma, or the end of the list — now follows the guard. if trimmed := bytes.TrimLeft(rest, " \t\r\n"); len(trimmed) > 0 && trimmed[0] != ',' && trimmed[0] != ']' { return out, fmt.Errorf("the bundle does not separate %q from what follows it the way a list does", guardAfter) } joined = append(joined, rest...) parsed, err := declaration.ParseFileTrusted(joined) if err != nil { return out, fmt.Errorf("the bundle stopped being a declaration once it was made an adopted one, which is this installer's fault: %w", err) } r.Bundle, r.Declaration, r.Resources = joined, parsed, len(parsed.Resources) return out, nil } // declares is whether the produced bundle names a resource. func (r Rewritten) declares(id string) bool { for _, res := range r.Declaration.Resources { if res.Identity() == id { return true } } return false } // genesisTakes are the modules an adopted genesis takes as it installs them: the foundation's and // the mesh's own, whose names genesis checked free, so taking them replaces nothing a predecessor // ran. The private network is not among them — it rewrites the machine's hosts file and the // container runtime's configuration whole — and neither is anything the operator installs later. var genesisTakes = map[string]bool{ RegistryModule: true, ControlPlaneModule: true, BuilderModule: true, "postgres": true, "lavinmq": true, "mesh-vault": true, "mesh-catalog": true, } // takeIfAdopted takes one of genesis's own modules on an adopted node, once it is assigned and // before the push that raises it. func takeIfAdopted(ctx context.Context, o Options, control controlPlane, module string, say func(string)) error { if !o.Adopted || !genesisTakes[module] { return nil } if _, err := control.tell(ctx, "take", o.Node, module); err != nil { return err } say(" taken " + module + " on " + o.Node + " — the mesh's own, its name checked free") return nil }