6.8 KiB
topic, status, date, deciders, extends, reconstructed
| topic | status | date | deciders | extends | reconstructed |
|---|---|---|---|---|---|
| what runs on it | accepted | 2026-09-28 | jochen | 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md | false |
137. A machine says which networks it routes
Context
The filter the mesh derives denies forwarding by default, because without a forward chain it says nothing about a container's published port (ADR 0100, issue 047). To keep a machine's own containers working it then allows two ranges: the container runtime's default bridge pool, and the pool its compose files are given. Those two are named in the controller's code, with a comment saying what the gap is:
A machine whose runtime is configured with something else needs this to say so — which is a thing the mesh cannot derive and a reason this list is named here rather than computed.
There was no way to say so. The list was a constant. A machine whose guests live anywhere else was filtered by a rule that looked deliberate and was a guess.
Measured, on the day a workstation was converged. Flipping it cut egress for five of its container networks at once, and for every network its test beds create — the beds allocate a fresh range per run, from a pool neither default covers. Nothing reported a fault. The containers could not reach anything, the machine went on reporting that it had applied what it was told, and the converge preview had said nothing about it either, because the preview lists what listens and routing is not a listener.
And two questions, not one. A guest also asks its host for an address and for names. Both arrive at the input chain, where nothing declared them, so denying by default left the guests of a routed network with no address and no resolution — which is not a closed port but a network that does not function, asked for by this machine's own guest.
Why the machine cannot simply be read. A test bed creates its bridge while it runs, between one declaration and the next, so a filter derived from what the machine last reported would be correct only for the networks that already existed when it was composed. A declared range covers the ones that do not exist yet.
Decision
A machine says which networks it routes for what it hosts, and the filter forwards them. A node-level fact, beside the node's public domain (ADR 0066) and for the same reason: the machine routes them, and the module that loads the filter holds a seat and may be replaced.
Added to the runtime's defaults, never replacing them. A machine that names one range has not stopped hosting whatever was already on the runtime's own pools, and replacing would trade one silent breakage for another.
Their guests keep address and name service. For a network that was named, the input chain admits that network's own DHCP and DNS, and nothing else: everything else a guest might want from its host is a port somebody declares, like every other port on this machine.
Said in CIDR form and checked when it is said. An entry that does not parse is a line nftables refuses, and a refused ruleset is a machine filtering nothing while its unit reports a fault — so the refusal happens where a person can read it, not on the machine.
A machine that says nothing is filtered exactly as before. Every machine already converged is untouched by this.
Options considered
- Leave it constant and edit the code per installation. Rejected: the value is a property of one machine, the code is the whole mesh's, and the two ranges as they stand describe a machine whose runtime was left at its defaults. It is also how this got here.
- Derive it from what the machine reports. Rejected as insufficient, not as wrong: it cannot cover a network created between two declarations, which is precisely the case that was broken. It would also make the filter follow whatever appeared on the machine, which is a firewall that widens itself.
- A per-node setting on the module that loads the filter. Rejected: the machine routes the networks. The filter module holds a node-scoped seat and is meant to be replaceable, and a replacement must not lose the machine's own truth.
- Replace the defaults with what is said. Rejected: see the decision. The first machine to name its bed range would lose its containers.
- Admit all input from a routed network, not only address and name service. Rejected: that is every port on the machine open to anything it hosts, which is the derivation abandoned.
Consequences
The converge preview says what a machine routes, including when it routes nothing but the defaults, with the command that changes it. The preview's own sentence about traffic it cannot preview stays, because a tunnel and the found firewall's NAT are still not previewable.
A machine whose guests are already broken by an earlier flip is fixed by saying its networks and pushing, with no flip to undo.
The list is one more thing that can be wrong and stale. A range removed from the machine and left here keeps forwarding for a network that no longer exists, which admits nothing, because there is no guest on it to admit. That is the safe direction of being out of date.
How this is checked
- What a machine says it routes is forwarded, and its guests keep address and name service. A test renders a ruleset for a machine that names one range and asserts both chains, per chain body so a line in the wrong chain cannot pass it. It fails against the previous behaviour, which is how it was written.
- The runtime's own defaults survive naming a range. Asserted in the same test.
- A machine that names nothing renders byte-identically to one that names nil, so every machine already behind this filter is untouched.
- Each family is matched in its own syntax. A test with one v4 and one v6 network asserts
ip saddrandip6 saddr, because one set holding both is a syntax error and a ruleset that does not load is a machine filtering nothing. - An entry that is not a network is refused where it is said, by the parse in the setter.