diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-08-19 10:41:03 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-08-19 10:41:03 -0400 |
| commit | f05383bb3447f7fbd44c9051c4e273caeb33352f (patch) | |
| tree | d2d2da9c9c5596bc42574a036b79d23faad20351 /fw/doc/design.md | |
| parent | dee31d66b705734be68c1bb21ff5dfc7bdaae23b (diff) | |
fw: a dead firewall leaves the network down, and can be restarted into it
Two things can happen when a firewall dies: the traffic it was
filtering carries on unfiltered, or it stops. Only the second is
defensible. A machine briefly off the network is a machine somebody
notices and fixes; a machine briefly on the network with no rules is
the thing the firewall was installed to prevent, and nobody notices it
at all.
fw already does the second, in all three modes, and by mechanism rather
than by care. Measured rather than assumed:
before fw device: /net/ether1 addr: 10.9.9.1 route: 10.9.9.254
fw running device: pkt0 addr: 10.9.9.1 route: 10.9.9.254
fw killed device: addr: route:
pktmedium is unbindonclose, so the interface and the address go when
fw's fds close, and the card is left bound to nothing with nothing
reading it. In a namespace it is harder still: /net answers "i/o on
hungup channel" and bind -a '#I' /net answers "mount/attach disallowed",
because the device mask was dropped before the program started.
So todo item 1 -- "a dead fw takes the network with it", open since the
first commit -- was the requirement written down as a defect. It is
now design.md and a FAILURE section in fw(8), and the tests assert it,
which is the point: this is exactly the property a later helpful change
reverses without meaning to. putback() was that change, written and
never run; deleting it removed a fail-open path, not just dead code.
What was actually broken is recovery, and in a way nobody had reached:
a fw that dies leaves its control filesystem mounted, and a corpse of a
mount fails everything asked of it -- including the access() check fw
makes before touching a card, which then refuses the restart:
fw: /tmp/rdbg/ctl: clone failed; not touching /net/ether1 until it exists
That check exists so fw does not take a card it cannot then serve, and
it was keeping fw from ever coming back. Now the dead mount is cleared
first, the same way reclaim() clears the pkt interface the same dead fw
left behind: both are its own wreckage.
With that and -a/-g in the service file -- so a restart does not need
the address it just lost -- the whole cycle works and never passes
through open: crash, network down, restart, network up and filtered.
Ten checks, three of which fail against the previous fw.c. 87 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'fw/doc/design.md')
| -rw-r--r-- | fw/doc/design.md | 42 |
1 files changed, 40 insertions, 2 deletions
diff --git a/fw/doc/design.md b/fw/doc/design.md index 1060ba6..98eca29 100644 --- a/fw/doc/design.md +++ b/fw/doc/design.md @@ -3,8 +3,11 @@ **A firewall for 9front. One program, filtering at a network card, between two networks, or in front of one namespace.** -Status: working and tested on the init-test VM, in all three modes. Not -yet fit to run on a machine you care about — see `todo.md`. +Status: working on the init-test VM in all three modes, with 87 checks +in `test/fwtest.rc` covering every bug that has shipped. What is left +open is in `todo.md`, and none of it is a reason not to run this any +more; what has never been tested is the wire — a second machine on the +same segment, and IPv6 anywhere. ## Why @@ -143,6 +146,41 @@ forbid are dropped. pf and iptables leave them running until they time out; that is a wart everyone has to learn, and "I blocked it, why is the transfer still going" is the wrong thing to discover at 3am. +**A dead firewall takes the network with it.** There are two things +that can happen when a firewall dies: the traffic it was filtering +flows unfiltered, or it stops. Only the second is defensible. A machine +that is briefly off the network is a machine an operator notices and +fixes; a machine that is briefly on the network with no rules is a +machine nobody notices, and it is exactly the moment you built the +firewall for. + +`fw` does the second, in all three modes, and it is the mechanism +rather than anything careful: + + card: pktmedium is unbindonclose, so the interface and the + address go when fw's fds close. The card is left + bound to nothing and nothing is reading it. + gateway: the same, on both sides at once. + namespace: the mount is hung up, and #I was dropped from the + process group's device mask, so the program cannot + bind the real stack back in its place. + +Measured, not assumed — a card-mode fw killed leaves `device` with +nothing after it, no address in `ipselftab` and no default route; a +sandboxed program gets `i/o on hungup channel` from `/net` and +`mount/attach disallowed` from `bind -a '#I' /net`. + +This is why `putback` was deleted rather than repaired. It meant to +restore the card to the stack as fw exited, which is precisely the +first option: the machine back on the network with no firewall on it. +It never ran, so nothing was ever wrong; had it run, it would have been +wrong every time. + +The cost is that recovery needs an address `fw` no longer knows, and +that is a real cost, paid deliberately. `-a` and `-g` in the service +file buy it back: crash, network down, restart, network up and +filtered, and no moment in between where it is up and unfiltered. + **A file server, so it detaches.** `fw` posts to `/srv`, forks the server and lets the process you started exit — what 42 of the 47 file servers in `/sys/src/cmd` do, and the five that do not are stdio |
