package apply import ( "context" "fmt" "time" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/firewall" "github.com/novox/mesh-host/internal/store" ) // foundFirewall settles, before anything else in an apply, which firewall this node has — and on // an adopted node that the mesh had converged, puts it back in force first (novox/hq ADR 0100). // // Only an adopted node asks. It is detected on every apply rather than remembered, so a firewall // switched on after adoption is spoken to from the next reconcile; what is remembered is what was // found first, and whether the mesh retired it. An unsupported firewall refuses the whole // declaration: the mesh could neither open what it needs through it nor say what it would close. func foundFirewall(ctx context.Context, d *declaration.Declaration, known *store.State, run Runner, log func(string)) (firewall.Kind, error) { if d.Adoption == nil { return "", nil } rec := known.Firewall if rec != nil && rec.DisabledByMesh && rec.Kind == string(firewall.UFW) { // Returned to adopted: the found firewall is enabled again before the openings are // converged through it, and the derived filter is gone with this declaration. if err := firewall.Enable(ctx, run); err != nil { return "", err } rec.DisabledByMesh = false rec.Forward = nil log(" enabled ufw again: this node is adopted, and the firewall found on it is in force") } kind, name, err := firewall.Detect(ctx, run) if err != nil { return "", err } if kind == firewall.Unsupported { return "", fmt.Errorf( "this machine is filtered by %s, and no host speaks that firewall yet. An adopted node "+ "keeps the firewall it was found with, so the mesh could neither open what it needs "+ "through it nor say what it would close; this declaration is refused whole", name) } if rec == nil { rec = &store.FoundFirewall{Kind: string(kind), WasActive: kind == firewall.UFW, FoundAt: time.Now().UTC()} } else { rec.Kind = string(kind) rec.WasActive = rec.WasActive || kind == firewall.UFW } known.Firewall = rec return kind, nil } // retireFirewall disables the found firewall once a converged declaration has applied cleanly, // which is when the mesh's derived filter has taken its place. Disabled, never flushed: its // configuration stays on disk for a return to adopted, and the container runtime's rules are not // its to take. // // Only a declaration from the mesh converges a node. A carried bundle never says a node is adopted // — it cannot — so its silence is not the controller's word that the node was converged, and an // adopted node re-applying its bundle keeps the firewall it was found with. func retireFirewall(ctx context.Context, d *declaration.Declaration, origin string, known *store.State, run Runner, log func(string)) error { rec := known.Firewall if origin != store.OriginDeclared || d.Adoption != nil || rec == nil || rec.Kind != string(firewall.UFW) || !rec.WasActive || rec.DisabledByMesh { return nil } if rec.Forward == nil { // Recorded before ufw is touched: disabling it opens the forward policy, and a retry // must know what it was (novox/hq ADR 0100). rec.Forward = firewall.ForwardPolicies(ctx, run) } if err := firewall.Disable(ctx, run, rec.Forward); err != nil { return err } rec.DisabledByMesh = true log(" disabled ufw: this node is converged and filtered by the mesh; ufw's configuration is left on disk") return nil } // applyOpening makes one opening true through the firewall found here. func applyOpening(ctx context.Context, o *declaration.Opening, run Runner, kind firewall.Kind) (Outcome, error) { out := begin(o) switch kind { case firewall.None: out.Action = "unchanged" out.Detail = "no firewall found; nothing filters this port" return out, nil case firewall.UFW: done, err := firewall.Converge(ctx, run, o) if err != nil { return out, err } out.Action = done.Action out.Detail = "through ufw, marked " + firewall.Mark(o) if done.SatisfiedBy != "" { // ufw would take a rule differing only in its comment for the same one, so the // mesh's is not added beside it (novox/hq ADR 0103). out.Detail = "satisfied by a rule found in ufw (" + done.SatisfiedBy + "); the mesh added nothing and will remove nothing" } return out, nil } return out, fmt.Errorf("no firewall is known for this node, so %s cannot be opened", o.Target()) } // removeOpening deletes the rules the mesh marked for an opening no longer declared, and nothing // the machine had before. func removeOpening(ctx context.Context, a store.Applied, run Runner, rec *store.FoundFirewall) (string, string, error) { if rec == nil || rec.Kind != string(firewall.UFW) { return "forgotten", "no firewall held a rule for it", nil } n, err := firewall.Remove(ctx, run, a.ID) if err != nil { return "", "", err } if n == 0 { return "forgotten", "ufw held no rule marked for it", nil } return "removed", fmt.Sprintf("%d ufw rule(s) marked as the mesh's deleted", n), nil }