Compare commits

..
4 changed files with 83 additions and 4 deletions
@@ -1,10 +1,10 @@
---
status: located
status: resolved
opened: 2026-09-29
located-in:
- mesh-host internal/apply/opening.go (retireFirewall)
- mesh-host internal/apply/apply.go (the condition it is called under)
fixed-by:
fixed-by: mesh-host 67 (retire on every converged apply; found-inactive apart from disabled-by-mesh; a skipped step said), mesh-controller 211 (the found firewall's state on node show)
amended-design:
---
@@ -108,3 +108,13 @@ convergence is a state the host keeps — the found firewall active again is ret
reconcile that finds it inactive records *found so* and never *done by the mesh*, and a step skipped
after a failed apply is said. Built in mesh-host on `feat/one-thing-filters-a-converged-machine`; the
record of both machines of this mesh is corrected by the first report under it.
## Resolved, 2026-10-02
mesh-host 67 and mesh-controller 211, live on every machine at 10:10Z. The step now runs on every
converged apply and says what it did; a found firewall enabled again is retired again. The record's
one inherited lie stands as history: on the control node the machine's own record already said the
mesh had disabled the firewall, and the host trusts its record, so `node show` says "retired by the
mesh" there. From this build on, a reconcile that finds the firewall inactive records *found inactive*
and never the other thing. Whether the flip's step took on 2026-09-29 is not recoverable and is not
owed by this record any more.
@@ -1,10 +1,10 @@
---
status: located
status: resolved
opened: 2026-09-29
located-in:
- mesh-host internal/apply/opening.go
- mesh-controller cmd/mesh-controller (the converge preview)
fixed-by:
fixed-by: mesh-host 67 (every refusing table and legacy chain classified with an owner; the runtime's user chain is other), mesh-controller 211 (kept, shown on node show, named by status, previewed with fates)
amended-design:
---
@@ -91,3 +91,15 @@ chain's refusals as *other*; `node show`, `status` and the converge preview say
`feat/one-thing-filters-a-converged-machine` in mesh-host and mesh-controller. On 2026-10-02 the home
server still carries the predecessor's chain in its legacy filter; the record's live row is reading it
there.
## Resolved, 2026-10-02
mesh-host 67 and mesh-controller 211, live at 10:10Z. The live row of
[ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) was read the
same hour: the home server's record names the predecessor's chain in the legacy filter's user chain
as *other*, with what it refuses, beside two chains a retired front end left in the IPv6 legacy filter;
the control node's record names the same two leftovers; the laptop and the workstation read *the mesh
alone*. `status` names both machines and is not well until the operator removes what the mesh did not
write. The allowance the predecessor's chain carried is
[issue 145](../145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)'s,
and that record is not closed by this one.
@@ -0,0 +1,43 @@
---
status: resolved
opened: 2026-10-02
located-in: [mesh-host internal/outward (Links reported only the links carrying a default route)]
fixed-by: mesh-host PR 66 (a link backed by a physical device is named outward, up or down), live 2026-10-02
amended-design: []
---
# 197 — A physical link that is down is not filtered when it comes up
## What was observed
A sweep of every machine's filter on 2026-10-02. A laptop-class machine connected by its radio has a
wired port that was unplugged. Its filter guarded the radio and the tunnel, and accepted everything
arriving on any other link:
```
iifname != { "mesh0", "<radio>" } accept
```
The wired port was not in the list. Plugged in, everything arriving on it would have been accepted,
every port of the machine open to whatever network the cable reached. That would last until the
machine reported again and was pushed a new filter.
## Why it matters
**The filter's one rule about links fails open.** [ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md)
has the filter constrain what arrives from outside, and has the machine say which links face outside.
Everything not named is treated as the machine's own, its containers and bridges. So a link the machine
fails to name is not filtered at all. The host named only the links carrying a default route at the
moment it reported. A cable plugged in later is the ordinary case for a laptop. A second wired network
that never carries the default route, such as a direct link to a storage box, is never named at all.
## Open questions
- A virtual link that faces outside (a VPN client's interface, a USB tether that appears as a virtual
device) has no physical device behind it. It is named only while it carries the default route. Is
that enough?
## Resolved (2026-10-02)
Live on the affected machine after the host was delivered and one more push: its filter now guards the
radio, the tunnel and the unplugged wired port, before anything is plugged into it.
@@ -0,0 +1,14 @@
# Diagnosis
*2026-10-02.*
**Located in `mesh-host` `internal/outward`.** `Links` read the kernel's routing tables and returned the
interfaces carrying a default route. An unplugged port carries none, so it was never reported, and the
controller rendered the filter around the links it was given.
**The fix.** A link faces outside if it carries a default route **or** has a physical device behind it.
The kernel lists every interface under `/sys/class/net`, with a `device` entry for one backed by
hardware. A bridge, a veth, the tunnel and the loopback have none, so they stay the machine's own. The
wired port is now reported up or down, and the filter guards it before anything is plugged in. Tested
with a radio carrying the default route and an unplugged wired port beside a bridge, a veth, the docker
bridge, the tunnel and the loopback: the two physical links are reported, nothing else.