ADR 0140: the filter constrains what arrives from outside #171
@@ -164,6 +164,12 @@ def check_rests_on(failures, records):
|
||||
# decision is exactly what as-is is for."
|
||||
if rel(path).startswith("03-DESIGN/00-as-is/"):
|
||||
continue
|
||||
# A withdrawn record's citations are history. It instructs nobody -- every reader
|
||||
# is sent to its superseder -- so what it was built on may itself be withdrawn.
|
||||
# Refusing that would mean rewriting the lineage of a record whose reasoning is
|
||||
# the thing the immutability rule protects.
|
||||
if frontmatter(read(path)).get("status") == "superseded":
|
||||
continue
|
||||
# An extension that supersedes legitimately names what it replaced.
|
||||
this = ADR_FILE.match(os.path.basename(path))
|
||||
supersedes = records[number]["front"].get("superseded-by", "")
|
||||
@@ -241,13 +247,20 @@ def check_supersession_symmetry(failures, records):
|
||||
failures.add("supersession", rel(record["path"]), f"superseder does not exist: {by}")
|
||||
continue
|
||||
other = records[match.group(1)]
|
||||
claims = os.path.basename(str(other["front"].get("supersedes", "")))
|
||||
if claims != record["name"]:
|
||||
# `supersedes:` may name one record or several. One decision replacing two is a real
|
||||
# situation -- two records that built and refined the same wrong mechanism are withdrawn
|
||||
# by the one record that removes it -- and a check that allows only one would force
|
||||
# either a chain of pro-forma records or an unmarked supersession.
|
||||
claimed = other["front"].get("supersedes", "")
|
||||
if isinstance(claimed, str):
|
||||
claimed = [claimed] if claimed else []
|
||||
claims = [os.path.basename(str(entry)) for entry in claimed]
|
||||
if record["name"] not in claims:
|
||||
failures.add(
|
||||
"supersession",
|
||||
rel(other["path"]),
|
||||
f"ADR {number} says this supersedes it; this record does not say so "
|
||||
f"(supersedes: {claims or 'absent'})",
|
||||
f"(supersedes: {', '.join(claims) or 'absent'})",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
status: superseded
|
||||
date: 2026-09-28
|
||||
deciders: jochen
|
||||
extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||
reconstructed: false
|
||||
superseded-by: 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
|
||||
---
|
||||
|
||||
# 137. A machine says which networks it routes
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
status: superseded
|
||||
date: 2026-09-28
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md
|
||||
superseded-by: 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
|
||||
---
|
||||
|
||||
# 139. A network is forwarded because a module declared it
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-09-28
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md
|
||||
supersedes:
|
||||
- 02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md
|
||||
- 02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md
|
||||
---
|
||||
|
||||
# 140. The filter constrains what arrives from outside, and says nothing about a machine's own guests
|
||||
|
||||
## Context
|
||||
|
||||
The filter the mesh derives blocks traffic passing *through* a machine unless something allows it,
|
||||
because a container's published port is traffic passing through rather than traffic arriving at the
|
||||
machine itself ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md),
|
||||
[issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md)).
|
||||
Having blocked all of it, the filter then had to let the machine's own containers reach outward again.
|
||||
It does that by listing the address ranges those containers sit on.
|
||||
|
||||
As rendered on a converged workstation today:
|
||||
|
||||
```
|
||||
policy drop
|
||||
ct state established,related accept
|
||||
ip saddr 172.16.0.0/12 accept
|
||||
ip saddr 192.168.128.0/17 accept
|
||||
ip saddr 10.0.0.0/8 accept
|
||||
ip saddr 192.168.16.0/20 accept
|
||||
... four more
|
||||
```
|
||||
|
||||
Two of those ranges were constants in the control plane's source. The rest were typed by the operator
|
||||
after [ADR 0137](0137-a-machine-says-which-networks-it-routes.md), which existed to make the typing
|
||||
possible, because converging that workstation had cut every one of its containers off from the
|
||||
internet and nothing reported a fault
|
||||
([issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md)).
|
||||
|
||||
**The list is the mistake, not its contents.** Every attempt to make it correct fails the same way.
|
||||
A constant describes one machine. A typed range goes stale, and cannot tell a network the mesh made
|
||||
from one a predecessor left behind — measured on the control-node, where six such ranges fall outside
|
||||
the constants and two of the six belong to services the mesh does not run
|
||||
([issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)).
|
||||
[ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md) tried to generate the same
|
||||
list from the modules and put half the rule set on the machine to do it. Three records, one list, and
|
||||
the list should not exist.
|
||||
|
||||
**Because the mesh has no policy about a container reaching outward.** What the filter is for is
|
||||
stated in [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md): which port is open,
|
||||
and to whom. That is about what arrives. A container of this machine's own opening a connection to
|
||||
something else is not a port being opened to anybody, and enumerating the addresses it might do so
|
||||
from is bookkeeping about the machine's internal plumbing, which the mesh neither owns nor can know.
|
||||
|
||||
**The system being replaced never had this fault, and its rule says why.** The chain still protecting
|
||||
the control-node applies only to traffic arriving on that machine's outward link, and leaves
|
||||
everything else alone. The mesh's filter dropped that distinction and replaced it with a list of
|
||||
addresses.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep the list and generate it better** — from the modules' declared networks, or from what the
|
||||
machine reports. Rejected: [ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md)
|
||||
is that, and it puts part of the rule set on the machine, which makes the rule set partly the
|
||||
machine's and the derivation advisory.
|
||||
2. **Name the guest links instead of their addresses, and allow only those.** Rejected as more than is
|
||||
needed: it fails in the safe direction, but it is still a list that has to keep up with the
|
||||
machine, and the thing it protects against — a container reaching outward — is not a thing the mesh
|
||||
has a position on.
|
||||
3. **Do not block traffic passing through at all.** Rejected: that is
|
||||
[issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md),
|
||||
where a published port was reachable from anywhere because no rule mentioned it.
|
||||
4. **Constrain what arrives from outside, and nothing else.** Adopted.
|
||||
|
||||
## Decision
|
||||
|
||||
**The filter constrains traffic arriving from outside the machine, and says nothing about traffic that
|
||||
did not.** Traffic passing through the machine is allowed unless it arrived on one of the machine's
|
||||
outward links, in which case it is allowed only where a declared endpoint's reach admits it
|
||||
([ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)). A container of this
|
||||
machine's own reaching anywhere is not filtered, because the mesh has no position on it.
|
||||
|
||||
**A machine says which of its links face outside.** One node-level fact, reported by the machine the
|
||||
way it already reports the kind of firewall it found and the tunnel it carries — not a setting, not a
|
||||
list of addresses, and not something anybody types. It does not change when a module is added or
|
||||
removed, which is what separates it from the list it replaces.
|
||||
|
||||
**A machine that has reported no outward link is sent no filter.** Rendering a rule around a link
|
||||
whose name is not known produces a rule set that does not load, which is a machine filtering nothing
|
||||
while its unit reports success. The refusal happens in the control plane, where a person reads it, and
|
||||
the machine keeps the filter it already has.
|
||||
|
||||
**No addresses of the machine's own networks appear in the filter.** The two constants are removed and
|
||||
`node networks` is removed with them, along with everything any machine was told to say through it.
|
||||
Ports continue to follow the modules exactly as before: a module assigned to a machine opens the port
|
||||
its assignment says it reaches on, and nothing about a network is said anywhere.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Three records collapse into one rule.** 0137 and 0139 are superseded. What 0137 was right about —
|
||||
that converging a machine had silently cut off its own containers, and that nothing previewed it — is
|
||||
answered by removing the cause rather than by giving the operator a way to compensate for it.
|
||||
- **Every machine already converged loses its declared ranges and keeps working**, because the traffic
|
||||
those ranges allowed is now allowed by not having arrived from outside. The workstation's five ranges
|
||||
and the laptop's one are deleted rather than migrated.
|
||||
- **A machine's test beds stop being a special case.** A bed's network is created while the machine
|
||||
runs and was the case no list could cover; it is now covered by not being mentioned.
|
||||
- **A new fact travels in the report**, and the control plane refuses to compose a filter without it,
|
||||
so the order of the roll-out matters: the machines report before the control plane depends on it.
|
||||
- **A machine with more than one outward link says so**, and a machine that acquires one while the mesh
|
||||
is not looking is treated as internal until its next report. That window is the cost of this shape;
|
||||
it is bounded by the report interval, and it exists on machines whose outward link changes, which
|
||||
are the machines with nothing published to the outside.
|
||||
- **What got harder:** nothing in the declaration, and one more thing a machine must be able to work
|
||||
out about itself. A machine that cannot say which link faces outside cannot be given a filter.
|
||||
|
||||
## How it is checked
|
||||
|
||||
- **A machine's own container reaches outward with no network named anywhere.** A bed converges a
|
||||
machine carrying containers on several networks, none of them mentioned in any setting, and each
|
||||
reaches out afterwards. This fails against the previous behaviour, where the same flip cut them off,
|
||||
and that is how it is written.
|
||||
- **A port declared reachable from outside is reachable; one that is not, is not.** Probed from off the
|
||||
machine's private network, for a published port and for an undeclared one, before and after the flip.
|
||||
- **A network created after the filter was composed needs no new filter.** A network is made on the
|
||||
machine after its last declaration and a container on it reaches out, with nothing re-sent.
|
||||
- **No address of a machine's own networks appears in a rendered filter**, asserted on the text so a
|
||||
range cannot creep back in.
|
||||
- **A machine that reports no outward link is sent no filter, and the refusal names it** — asserted in
|
||||
the control plane, and that the machine's existing filter is left alone.
|
||||
- **A machine reporting two outward links has both constrained**, asserted per chain body so a rule
|
||||
covering one and not the other cannot pass.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — what the filter is for
|
||||
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — what admits traffic
|
||||
arriving from outside
|
||||
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) — why traffic passing through is
|
||||
filtered at all
|
||||
- [ADR 0137](0137-a-machine-says-which-networks-it-routes.md),
|
||||
[ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md) — superseded here
|
||||
- [issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md),
|
||||
[issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)
|
||||
@@ -217,9 +217,10 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0133** — [A module owns its migrations, and the mesh owns when they run](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) *(superseded)*
|
||||
- **0135** — [A module version prepares its state before it runs](0135-a-module-version-prepares-its-state-before-it-runs.md)
|
||||
- **0136** — [A step gates its module, not the machine](0136-a-step-gates-its-module-not-the-machine.md)
|
||||
- **0137** — [A machine says which networks it routes](0137-a-machine-says-which-networks-it-routes.md)
|
||||
- **0137** — [A machine says which networks it routes](0137-a-machine-says-which-networks-it-routes.md) *(superseded)*
|
||||
- **0138** — [An assignment binds an endpoint and says how far it reaches](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)
|
||||
- **0139** — [A network is forwarded because a module declared it](0139-a-network-is-forwarded-because-a-module-declared-it.md)
|
||||
- **0139** — [A network is forwarded because a module declared it](0139-a-network-is-forwarded-because-a-module-declared-it.md) *(superseded)*
|
||||
- **0140** — [The filter constrains what arrives from outside, and says nothing about a machine's own guests](0140-the-filter-constrains-what-arrives-from-outside.md)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ code:
|
||||
updated: 2026-09-28
|
||||
decisions:
|
||||
- 02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md
|
||||
- 02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md
|
||||
- 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
|
||||
- 02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md
|
||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||
- 02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md
|
||||
@@ -627,41 +627,49 @@ the found firewall reloads and reachable from a container on the node, that a ma
|
||||
enrols through the openings before and after a reload and a reboot, and that after the flip the
|
||||
declared port is open and the undeclared one closed.
|
||||
|
||||
### The networks it forwards are the ones its modules declared
|
||||
### It filters what arrives from outside, and not what the machine's own guests send
|
||||
|
||||
*2026-09-28, preparing the control-node's convergence —
|
||||
[issue 141](../../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md), settled by
|
||||
[ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md), which replaces
|
||||
[ADR 0137](../../02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md) and
|
||||
[ADR 0139](../../02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md).*
|
||||
|
||||
The forward chain denies by default, so a machine's own guests have to be allowed back in. Until now
|
||||
that was two ranges named in the control plane and, since
|
||||
[ADR 0137](../../02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md), a list a machine could
|
||||
add to. On the machine measured here, six of its container networks fell outside those ranges — and
|
||||
four of the six were networks the mesh's own modules had declared and the host had created, while two
|
||||
were the predecessor's leftovers. A range wide enough to keep the four keeps the two: a filter widened
|
||||
by hand to protect what should not be there.
|
||||
Traffic passing through a machine is filtered, because a container's published port is traffic passing
|
||||
through rather than traffic arriving at the machine itself. Having blocked it, the filter then had to
|
||||
let the machine's own containers reach outward again — and it did that by listing the address ranges
|
||||
they sit on. Two of those ranges were constants in this repository's code, and the rest were typed by an
|
||||
operator after the flip had already cut a workstation's containers off from everything.
|
||||
|
||||
**A network is forwarded because a module declared it.** The forward chain forwards the networks of the
|
||||
modules assigned to that node and, by default, nothing else; a module unassigned stops being forwarded
|
||||
at the next reconcile. The control plane says which networks, by name, and the host — which created
|
||||
them — renders their addresses, because the runtime allocates the subnet and the host is what knows it.
|
||||
That keeps §4's shape and this document's: the mesh decides, the host applies.
|
||||
**The list was the mistake, not its contents.** A constant describes one machine. A typed range goes
|
||||
stale and cannot tell a network the mesh made from one a predecessor left behind — on the control-node,
|
||||
six ranges fall outside the constants and two of the six belong to services the mesh does not run. The
|
||||
attempt to generate the list from the modules put half the rule set on the machine and made the
|
||||
derivation advisory. Three records, one list.
|
||||
|
||||
**This is derivation from the declaration, not from the machine.** 0137 rejected reading the machine,
|
||||
for two reasons that do not apply here: a network created between two declarations is already named in
|
||||
the one that asked for it, and a network nobody declared is never forwarded however it appeared. What
|
||||
0137's mechanism keeps is the case it was right for — guests no module declares, a test bed's range —
|
||||
added to the derived set and never replacing it.
|
||||
**And the mesh has no position on a container reaching outward.** §4 exists to say which port is open
|
||||
and to whom, which is about what arrives. A container of this machine's own opening a connection
|
||||
somewhere is not a port opened to anybody, and the addresses it might do that from are the machine's
|
||||
internal plumbing, which the mesh neither owns nor can know.
|
||||
|
||||
The runtime's own default bridge, which a container attaches to when it names no module network, is the
|
||||
runtime's and not a module's, so the host renders it from what the runtime reports. With that, no range
|
||||
is named in the control plane at all.
|
||||
So the filter constrains what arrives from **outside** the machine and says nothing about what did not.
|
||||
Traffic passing through is allowed unless it came in on one of the machine's outward links, and then
|
||||
only where a declared endpoint's reach admits it (§6). The machine says which of its links face
|
||||
outside — one fact it reports, like the kind of firewall it found and the tunnel it carries, not a
|
||||
setting and not a list of addresses. It does not change when a module is added or removed, which is the
|
||||
whole difference from what it replaces. A machine that has reported no outward link is sent no filter
|
||||
at all, and keeps the one it has, because a rule written around a link with no name is a rule set that
|
||||
does not load — a machine filtering nothing while its unit reports success.
|
||||
|
||||
*How it is checked:* a node with two modules declaring networks renders rules for exactly those two and
|
||||
none for a third present on the machine that nothing declared — which fails against forwarding by
|
||||
range, and is how it was written; unassigning one removes its rule at the next reconcile; the default
|
||||
bridge is asserted for a runtime whose bridge is somewhere other than the old constant named; and the
|
||||
guests of a declared network keep address and name service, per chain body.
|
||||
Ports go on following the modules exactly as before: assign a module to a machine and the port its
|
||||
assignment says it reaches on opens. Nothing about a network is said anywhere, by anybody.
|
||||
|
||||
*How it is checked:* a bed converges a machine carrying containers on several networks, none of them
|
||||
named in any setting, and each reaches outward afterwards — which fails against the previous behaviour,
|
||||
where the same flip cut them off, and is how it was written; a network made *after* the last declaration
|
||||
needs no new filter; a declared port is reachable from off the private network and an undeclared one is
|
||||
not; no address of a machine's own networks appears in a rendered filter, asserted on the text; and a
|
||||
machine reporting no outward link is refused in the control plane with its existing filter left alone.
|
||||
|
||||
## 5 — Certificates
|
||||
|
||||
|
||||
@@ -61,6 +61,18 @@ module declares — a test bed's pool — which is a much smaller residue than t
|
||||
has to know the runtime's allocations to tell whether that line is sufficient. On the machine
|
||||
measured here it read as though nothing needed saying.
|
||||
|
||||
## What was decided
|
||||
|
||||
*2026-09-28, later the same day.* The answer is not a better list. The question in the first open
|
||||
item below — should the chain be derived from the networks the modules declare — was answered *no*,
|
||||
after a converged machine's rendered rules were read: the chain blocks everything passing through the
|
||||
machine and then allows its own guests back by listing their addresses. Every route to a correct list
|
||||
fails, because the mesh has no position on a container reaching outward in the first place. The filter
|
||||
now constrains what arrives from **outside** the machine and says nothing about what did not, and a
|
||||
machine says which of its links face outside — one reported fact instead of a list. See
|
||||
[ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md), which
|
||||
supersedes both 0137 and the first attempt at answering this.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Should the forward chain be derived from the network resources the node's modules declare, with the
|
||||
|
||||
Reference in New Issue
Block a user