The legacy reader required every path into a chain of refusals to come from a built-in whose policy accepts. The home server's ban chain hangs off the container runtime's user chain, whose forward policy the runtime set to DROP, so the machine reported the mesh's own intrusion prevention as a rule set the mesh did not write. A chain is a ban when every refusal names its sources and the chain accepts nothing — the rule the nftables side already used. A chain that accepts anything is still not a ban. Fixture captured from the machine. The citations for the uninstalled front end move to ADR 0180, which another session's renumber had left pointing at an unrelated record.
178 lines
7.8 KiB
Go
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 0180): 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
|
|
}
|