A taken tunnel's found configuration is retired once the take is proven (hq ADR 0119)

Kept on disk it was the take's fallback; once the mesh's interface is up in its place and a peer
has handshaken with it, it is an unmaintained way back onto the network, held for ever. It is now
removed from where its unit reads it, its kept original verified first and left as it is, and the
hold ends. Until proven — no handshake, or wg not answering — it is kept and the report says why.
The retirement is recorded apart from holds, so later applies, an undeclare, and a reassignment
find it retired rather than missing, and nothing writes it back.
This commit is contained in:
jochen
2026-09-27 00:47:57 +02:00
parent 23a4436499
commit b462f461c6
9 changed files with 628 additions and 14 deletions
+161 -7
View File
@@ -27,6 +27,16 @@ import (
// Every apply, not once: a found unit somebody starts again would take the port back from the
// mesh's interface, so it is stopped again and said so. That is the one place an adopted node
// undoes something done by hand, and it is because the tunnel is the mesh's now.
//
// **Until the take is proven, and then the found configuration is retired** (novox/hq ADR 0119).
// Keeping it on disk was the caution the take needed: if the mesh's interface does not come up,
// the found unit is started again and the peers never notice. That caution is spent once the
// tunnel is taken — the found unit down and disabled, the mesh's interface up — and a peer has
// handshaken with the mesh's interface. From then on a configuration nothing maintains, one
// command away from raising a second way onto the network, is not a rollback path but a door
// nobody watches. So it is removed from where its unit reads it; its original, kept before
// anything happened to it (ADR 0100), stays kept; and the hold on it ends. A take never proven
// keeps it, and says so — a broken take is visible, not silently retired.
// TakenTunnel is what an apply says about a tunnel it took over, for the node's report.
type TakenTunnel struct {
@@ -36,7 +46,9 @@ type TakenTunnel struct {
Peers int
// State is "not-taken" (the found interface still up, the mesh's not), "taken" (the found one
// down and disabled, the mesh's up with its key) or "down" (the found one down and the mesh's
// not up: the peers reach nothing). Note is what this apply did about it.
// not up: the peers reach nothing). Note is what this apply did about it — and, for a taken
// tunnel, whether the take is proven and its found configuration retired (novox/hq ADR 0119).
// Kept is where the found configuration's original is, retired or not.
State string
Note string
Kept string
@@ -84,14 +96,35 @@ func takeOver(ctx context.Context, sys system.System, svc *declaration.Service,
// 1. The configuration, kept like any held file. A synthetic file resource stands for it, so
// the same code keeps its original, digests it and notices it changing.
//
// **Unless it was retired** (novox/hq ADR 0119): the take was proven and the mesh removed
// it, so there is nothing to hold and nothing missing — only where its original is, which
// the retirement recorded. A hold still standing is let go: that is what retiring it meant.
// One put back at its path by a person is on the machine again, with no hold, and is found
// and kept afresh — the same content kept once, as any original is — and retired again by
// the first apply that finds the take still proven.
file := &declaration.File{ID: id, Type: declaration.TypeFile, Path: t.Config}
was, already := known.HeldAt(id)
out, held, err := hold(ctx, sys, file, module, was, already,
"the configuration of the tunnel "+t.Interface+", taken over by "+svc.Unit, run, keep, now)
if err != nil {
return begin(file), facts, false, fmt.Errorf("keeping the found tunnel's configuration: %w", err)
var held store.Held
retired, wasRetired := known.RetiredAt(t.Config)
if wasRetired && !present(t.Config) {
known.Release(id)
held = store.Held{Kept: retired.Kept}
out = begin(file)
out.Action = "unchanged"
facts.Note = "the found configuration " + t.Config + " was retired once the take was proven; " +
"its original is kept at " + retired.Kept + " and the mesh never brings it back"
} else {
if wasRetired {
known.Unretire(t.Config)
}
was, already := known.HeldAt(id)
out, held, err = hold(ctx, sys, file, module, was, already,
"the configuration of the tunnel "+t.Interface+", taken over by "+svc.Unit, run, keep, now)
if err != nil {
return begin(file), facts, false, fmt.Errorf("keeping the found tunnel's configuration: %w", err)
}
known.RecordHeld(held)
}
known.RecordHeld(held)
facts.Kept = held.Kept
// What the file says, for the report: from the machine, or from the kept original when the
// machine's copy is gone. The private key stays in the file; nothing here keeps it.
@@ -185,6 +218,9 @@ func takeOver(ctx context.Context, sys system.System, svc *declaration.Service,
}
out.Detail = "the tunnel " + t.Interface + "'s configuration, kept as found"
if wasRetired && out.Action == "unchanged" {
out.Detail = "the tunnel " + t.Interface + "'s configuration, retired once the take was proven"
}
if held.Kept != "" {
out.Detail += " (original at " + held.Kept + ")"
}
@@ -335,6 +371,124 @@ func restoreFound(ctx context.Context, sys system.System, unit string, run Runne
facts.Note += "; " + unit + " was started again, so the machine has the tunnel it had"
}
// retireFound removes the found tunnel's configuration from where its unit reads it, once the take
// is proven, and ends the hold on it (novox/hq ADR 0119). Asked after the mesh's service applied
// and the tunnel reads as taken; retired says whether this apply retired it, and out is then what
// replaces the take's outcome for the configuration.
//
// **Proven is taken and a handshake.** Taken alone — the found unit down and disabled, the mesh's
// interface up — says the mesh's interface exists, not that any peer reaches it: an interface up
// with the wrong key is taken and carries nothing. A peer that has completed a handshake with it
// has checked its key, so that is the proof, asked of the kernel through `wg`. Anything short of
// one — no peer yet, every time zero, `wg` missing or failing — keeps the file, and the account
// says which: a take that never proves itself is visible rather than silently retired.
//
// **The original must still be kept.** It is the record of what the predecessor was and a
// person's only way back (ADR 0100); a kept copy that has gone missing is said, and the file is
// not removed, since removing it then would lose the only copy. What is on disk now, if something
// other than the mesh rewrote it since it was found, is kept too before it goes — by content, so
// the first original is never overwritten.
//
// The found unit is left disabled; without its configuration it cannot raise the interface, so
// every later apply's check of it finds nothing to do. Nothing here ever writes the file back.
func retireFound(ctx context.Context, svc *declaration.Service, known *store.State, run Runner, keep Keep,
facts *TakenTunnel, now time.Time) (out Outcome, retired bool) {
t := svc.TakesOver
id := takeOverID(svc)
if facts.State != Taken {
return out, false
}
held, isHeld := known.HeldAt(id)
if !isHeld {
// Retired already (takeOver let any hold go and said so), or never held: nothing to do.
return out, false
}
say := func(note string) {
if facts.Note != "" {
facts.Note += "; "
}
facts.Note += note
}
mesh := strings.TrimPrefix(svc.Unit, "wg-quick@")
peers, err := tunnel.Handshaken(ctx, tunnel.Runner(run), mesh)
if err != nil {
say("taken, not yet proven: " + err.Error() + "; the found configuration " + t.Config + " is kept")
return out, false
}
if peers == 0 {
say("taken, not yet proven: no peer has handshaken on " + mesh + "; the found configuration " +
t.Config + " is kept")
return out, false
}
proven := fmt.Sprintf("proven: %d peer(s) handshaken on %s", peers, mesh)
// The kept original, read back — not just named in a record.
if held.Kept == "" {
say(proven + ", and the found configuration " + t.Config + " is not retired: no original of it " +
"was kept, so removing it would leave no record of what the predecessor was")
return out, false
}
original, err := os.ReadFile(held.Kept)
if err != nil || (held.Digest != "" && digestOf(string(original)) != held.Digest) {
why := "is missing"
if err == nil {
why = "no longer holds what was found"
} else if !errors.Is(err, os.ErrNotExist) {
why = "cannot be read (" + err.Error() + ")"
}
say(proven + ", and the found configuration " + t.Config + " is not retired: its kept original " +
held.Kept + " " + why + ", so removing it would lose the only copy")
return out, false
}
gone := !present(t.Config)
if !gone {
current, err := os.ReadFile(t.Config)
if err != nil {
say(proven + ", and the found configuration " + t.Config + " is not retired: it cannot be read (" +
err.Error() + ")")
return out, false
}
if held.Digest != "" && digestOf(string(current)) != held.Digest {
if keep == nil {
say(proven + ", and the found configuration " + t.Config + " is not retired: it was rewritten " +
"since it was found and this host has nowhere to keep what it holds now")
return out, false
}
if _, err := keep(t.Config, current, 0o600); err != nil {
say(proven + ", and the found configuration " + t.Config + " is not retired: keeping what it " +
"holds now failed (" + err.Error() + ")")
return out, false
}
}
if err := os.Remove(t.Config); err != nil && !errors.Is(err, os.ErrNotExist) {
say(proven + ", and the found configuration " + t.Config + " could not be removed (" + err.Error() + ")")
return out, false
}
if present(t.Config) {
say(proven + ", and the found configuration " + t.Config + " is still there after it was removed")
return out, false
}
}
known.RecordRetired(store.Retired{ID: id, Path: t.Config, Kept: held.Kept, At: now})
known.Release(id)
facts.Kept = held.Kept
say(proven + "; the found configuration " + t.Config + " is retired — its original kept at " +
held.Kept + ", " + t.Unit + " left disabled, and the mesh never brings it back")
out = Outcome{ID: id, Type: string(declaration.TypeFile), Target: t.Config, Action: "removed",
Detail: "retired: the take of " + t.Interface + " is " + proven + "; original kept at " + held.Kept}
if gone {
// Already gone — removed by something other than the mesh, or by an apply whose record was
// never saved. Nothing removed here; the hold ends all the same.
out.Action = "unchanged"
out.Detail = "retired: the take of " + t.Interface + " is " + proven + " and " + t.Config +
" was already gone; original kept at " + held.Kept
}
return out, true
}
// takesOver is the service in a declaration that takes over a tunnel, if any: one per node, since
// a machine has one private network.
func takesOver(d *declaration.Declaration) *declaration.Service {