Files
mesh-host/internal/apply/opening.go
T
jschoubben 627ac97d4f The host says what filters the machine, with owners, and keeps the found firewall retired on every converged apply (hq ADR 0168)
Every table and chain that refuses traffic is reported with whose it is:
the mesh's, the found firewall's, the container runtime's own, a ban, or
other — the runtime's user chain is other, which is where both predecessors
kept their rules, in the legacy filter on one machine and invisible to the
mesh. Adoption's threshold does not move; a converged machine's report
grows by its filters and its found firewall's state.

Convergence is a state the host keeps: a found firewall enabled again is
retired again and said; a reconcile that finds it inactive records that it
was found so, never that the mesh did it; a step skipped after a failed
apply is said. A retirement the mesh began and did not finish is finished.

Fixtures are rulesets captured from three machines of the first mesh.
2026-10-02 11:58:16 +02:00

168 lines
7.3 KiB
Go

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 keeps the found firewall retired on a converged machine (novox/hq ADR 0100, ADR
// 0168): disabled, never flushed, its configuration left on disk for a return to adopted, and the
// container runtime's rules not its to take.
//
// **Convergence is a state the host keeps, not a step it takes once.** Every converged apply reads
// whether the front end is in force; enabled again by a package, a boot or a hand, it is retired
// again and said. The record says how it came to be inactive — the mesh disabled it, or a reconcile
// found it so — and the two are never confused: a flip that did not take, followed by a hand that
// did, used to be recorded as the mesh's doing (issue 143).
//
// 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.
//
// Returned is what this apply did about the found firewall, for the report; empty when the machine
// has none or is not converged.
func retireFirewall(ctx context.Context, d *declaration.Declaration, origin string, known *store.State,
run Runner, log func(string)) (string, error) {
rec := known.Firewall
if origin != store.OriginDeclared || d.Adoption != nil || rec == nil || rec.Kind != string(firewall.UFW) || !rec.WasActive {
return "", nil
}
active := firewall.Active(ctx, run)
if !active && !(rec.Forward != nil && !rec.DisabledByMesh) {
// Inactive, and either the mesh's doing already or nobody's recorded here: said as found,
// never as done (issue 143's second fault). A retirement the mesh began and did not finish —
// the forward policy recorded, ufw down, the restore failed — is the one inactive state that
// is still the mesh's to complete, below.
if rec.RetiredBy == "" {
if rec.DisabledByMesh {
rec.RetiredBy = firewall.RetiredByMesh
} else {
rec.RetiredBy = firewall.RetiredFoundSo
log(" ufw is inactive on this converged node, and not by the mesh; recorded as found so")
}
}
return "", nil
}
// **Nothing is retired until what replaces it is in force** (novox/hq ADR 0100). The flip
// loads the mesh's derived filter in ufw's place; disabling ufw before that table is actually
// loaded — a filter module not assigned, or a unit that did not load — leaves the machine with
// no filter at all.
loaded, err := firewall.MeshTableLoaded(ctx, run)
if err != nil {
return "", err
}
if !loaded {
return "", fmt.Errorf("this node is converged and the mesh's own filter (table %s) is not loaded on "+
"this machine, so ufw was left in force: retiring it would leave the machine filtering "+
"nothing. Assign a filter module to this node, or return it to adopted", firewall.MeshTable)
}
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
}
again := rec.DisabledByMesh || rec.RetiredBy != ""
rec.DisabledByMesh = true
rec.RetiredBy = firewall.RetiredByMesh
if again {
log(" disabled ufw again: it had been enabled since the mesh retired it; this node is converged and filtered by the mesh")
return "disabled again: ufw had been enabled since the mesh retired it", nil
}
log(" disabled ufw: this node is converged and filtered by the mesh; ufw's configuration is left on disk")
return "disabled: this node is converged and filtered by the mesh; ufw's configuration is left on disk", 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
}