Files
mesh-host/internal/apply/opening.go
T
jschoubben f57386cdea A package may be declared absent, and an uninstalled front end is retired for good (hq ADR 0175)
absent: true on a package has the host remove it through the machine's own
package manager when it is installed and leave alone a machine that never had
it; read back either way. Undeclaring a package still removes nothing. A
found firewall whose command is gone is recorded as removed, said once, and
asked nothing of.
2026-10-02 16:28:27 +02:00

178 lines
7.8 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
}
if !firewall.Installed(ctx, run) {
// Uninstalled (novox/hq ADR 0175): retired for good, by the module that replaced it. Said
// once, and nothing is asked of a command that is not there.
if rec.RetiredBy != firewall.RetiredRemoved {
rec.RetiredBy = firewall.RetiredRemoved
log(" the found firewall (ufw) is no longer installed; the mesh's filter is what filters this machine")
return "removed: ufw is no longer installed; the mesh's filter is what filters this machine", nil
}
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
}