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.
168 lines
7.3 KiB
Go
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
|
|
}
|