Issues 102–106 and ADR 0105 from the core migration

Two birth-address outages and a registry that would have been the third; a
container that keeps a stale environment after its file changes; a host command
that applied a converged declaration to an adopted node; the hub and the vault
without seats. And the decision the operator made under it all: the hub adopts
the predecessor's tunnel in place, key and peers and range and port.
This commit is contained in:
2026-09-23 22:50:10 +02:00
parent ee2bdf220c
commit cb2117f1c4
8 changed files with 365 additions and 1 deletions
@@ -0,0 +1,117 @@
---
topic: the mesh
status: accepted
date: 2026-09-23
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
---
# 105. The mesh adopts the predecessor's tunnel in place
## Context
The control-node is adopted and its store and broker have been moved onto the ports the
predecessor served them on, so the predecessor's other machines keep reaching them. They reach
them **over the predecessor's tunnel**: a WireGuard interface on the control-node with three
peers, an address range, and a port the hosting provider already lets through. The mesh's own
private network runs beside it on a second interface, a second range and a second port — one the
provider does not let through, so no other machine can join the mesh
([research 012](../01-RESEARCH/012-the-minimum-viable-node/00-overview.md), the runbook's
measurement of the upstream filter).
[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) says the mesh's range *must not
overlap a tunnel the predecessor still runs*. That was written for coexistence. It leaves the
migration with two tunnels for as long as any predecessor machine exists, and the second one
unreachable.
The mesh already adopts in place where the thing found is the thing it would have raised: the
store and the broker were taken over as modules, keyed on the container that was running
([ADR 0078](0078-the-store-and-broker-are-modules.md)). A WireGuard interface is the same shape: a
private key, a listening port, a list of peers by public key, an address. The mesh's is not
different in kind from the predecessor's; it is a second one.
The operator's instruction: take over the tunnel interface, same range — everything stays the same.
## Considered Options
1. **Two tunnels until the last predecessor machine is gone.** Rejected: the mesh's stays
unreachable from outside, so no machine can join, so the last predecessor machine is never gone.
2. **Move the mesh's tunnel onto the predecessor's port with the mesh's own key and range.** The
peers' packets arrive and are dropped — WireGuard authenticates by key, and the mesh's key is not
the one they know. Every other machine loses its tunnel until it enrols, and it enrols over a bus
it reaches through that tunnel. Rejected.
3. **Adopt the predecessor's tunnel in place: its private key, its peers, its range, its port.**
Adopted.
## Decision
**On an adopted node that is the hub, the private network takes over the tunnel it finds.** The
mesh's interface is raised with the found interface's **private key**, on its **port**, with its
**address and range**, and every **peer** the found interface had — public key, allowed address —
carried into the mesh's peer list as a peer not yet enrolled. The found interface is stopped, never
flushed; its configuration stays on disk, kept like any held file.
**Nothing a peer knows changes.** A predecessor machine keeps the same server key, the same
endpoint, the same address and the same route; it cannot tell the tunnel changed hands. When that
machine enrols, it keeps its address: the mesh assigns an enrolling node the address the tunnel
already had for its key, and only a node with no such address is given a fresh one from the range.
**The mesh's own addresses are the range's.** The controller composes every node's private address
from the tunnel it holds, so adopting the predecessor's range moves the mesh's addresses with it —
the hub's, and every binding, hosts-file entry and endpoint derived from it. Those are readers of
the setting; they follow it, per ADR 0100's rule for ports. A reader that does not follow is
[issue 102](../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md).
**This narrows ADR 0100.** Its rule that the range must not overlap a tunnel the predecessor still
runs applies to a node that is *not* adopting the tunnel: where the found tunnel is left running
beside the mesh's, the ranges must differ. Where it is adopted, there is one tunnel and one range.
**The guard's question answers itself.** [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)
admits the mesh's own ports only from the private network's interface. With one tunnel, the
predecessor's peers arrive on it, and nothing needs to be admitted from an interface the mesh does
not own.
## Consequences
- **The order of a migration changes.** The hub's tunnel can be taken as soon as the node is
adopted, before any service — and should be, because it is what lets other machines join. The
runbook's step order is amended.
- **The hub takes the provider's open port for free.** The port the predecessor's tunnel used is
by definition one the provider passes.
- **A peer's identity precedes its enrolment.** The mesh holds public keys and addresses for
machines it has no record of. They are peers of the tunnel, not nodes of the mesh, until they
enrol; the registry must be able to say both.
- **What got harder:** the private key of the found interface is read from the machine and becomes
the mesh's — the one case where the mesh takes a credential it did not mint. It is sealed like
any own secret from then on, and the found configuration file is kept, not copied further.
- Two documents currently say the opposite: ADR 0100's non-overlap rule (narrowed above) and the
runbook's port plan, which is amended with this record.
## How it is checked
A lab bed prepares a hub the way the predecessor leaves one: a WireGuard interface with a key, a
port, a range and two peers, each peer a second machine that reaches a service on the hub through
the tunnel. Then:
- **Adopted, the tunnel changes hands and the peers notice nothing**: the found interface is down
and its file is on disk; the mesh's interface is up with the found key, port and address; each
peer's service call succeeds before, during and after, with no reconfiguration on the peer.
- **A peer enrols and keeps its address**: the machine joins the mesh over the tunnel it already
has, and its node address is the one the tunnel held for it.
- **A new machine gets a fresh address from the same range**, and reaches both the hub and the
enrolled peer.
- **Nothing derived from the address is stale**: every binding, hosts entry and endpoint the
controller composes says the adopted range, before and after a push.
Unit tests hold the controller to reading the hub's address and range from the adopted tunnel,
assigning an enrolling node the address its key already had, and refusing to hand out an address
the tunnel already holds; and the host to raising the mesh's interface with the found key and
peers and stopping the found interface without flushing it.
## References
- [ADR 0078](0078-the-store-and-broker-are-modules.md), [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md),
[ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)
- [issue 102](../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
- [research 012 — the minimum viable node](../01-RESEARCH/012-the-minimum-viable-node/00-overview.md)
+1
View File
@@ -93,6 +93,7 @@ python3 00-META/checks/index.py fail if stale
- **0102** — [The mesh writes into a shared file, never over it](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)
- **0103** — [What an adopted node holds, and what its guard refuses](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)
- **0104** — [A provision may be answered by an adapter to the predecessor](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md)
- **0105** — [The mesh adopts the predecessor's tunnel in place](0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md)
### Its tiers, from the bottom up