From 0f922552ad8cc73c0c3c3674d484c3d78dd8c557 Mon Sep 17 00:00:00 2001 From: Calvin Morrison Date: Tue, 18 Aug 2026 17:01:49 -0400 Subject: fw: a firewall, at a card, between two networks, or in front of a namespace One program with three modes, sharing one rule engine and one ndb rule language. Which mode it is depends on what you point it at, and it says so at startup rather than choosing silently. fw -e /net/ether0 rules.ndb a card: every packet in or out fw rules.ndb two networks: everything crossing fw rules.ndb one namespace: what programs ask for The first two filter packets on a wire, using the pkt medium: the stack gives up its card and gets a synthetic one with fw on the other end, so nothing reaches it that fw did not pass. Since the stack no longer has ethernet, fw answers ARP for the address it stands in for. The third serves a filtered /net and matches connect and announce before they reach the kernel, so a refusal comes back out of dial(2) with a reason. That is only a boundary if the program also loses #I, which /dev/drivers does and cannot be undone; fw.rc does it in the right order. Rules are ndb, matched top to bottom, first match wins, no match denies. Connections are tracked, so permitting traffic one way permits the replies. A rule change drops connections the new rules forbid rather than letting them finish: a block blocks. Logging is per rule, to /sys/log/fw. Tested on the init-test VM in all three modes: a page fetched through a real card, a TCP handshake across two networks, request filtering with the escape routes closed, live rule changes killing established connections, and one rule file working unchanged at both altitudes. doc/todo.md has what is not done. Item 1 is the one that matters: a fw that dies takes the card's address with it, so the machine loses its network and fw cannot restart unaided. That also blocks svc supervision. Co-Authored-By: Claude Opus 5 --- fw/doc/todo.md | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 fw/doc/todo.md (limited to 'fw/doc/todo.md') diff --git a/fw/doc/todo.md b/fw/doc/todo.md new file mode 100644 index 0000000..97d1d71 --- /dev/null +++ b/fw/doc/todo.md @@ -0,0 +1,103 @@ +# fw: open items + +Ranked. Item 1 is the only thing between this and running it on a +machine you care about. + +## 1. A dead fw takes the network with it + +**Severity: blocks use.** + +Taking a card is destructive and is not undone. The `pkt` interface that +replaces it is `unbindonclose`, so when `fw` stops the interface goes and +the address goes with it. The machine is left with a card bound to +nothing and no network. + +Worse, `fw` cannot restart unaided: the address it would read off the +card is the address that just vanished. So it exits, and a supervisor +with `restart=always` would spin. + +`fw` tries to put the card back as it exits (`putback`, via `atexit` and +`threadnotify`). That covers an orderly stop. **It did not fire on a kill +in testing** and I did not chase it further — a cleanup that works +sometimes is worse than none, because you would trust it. + +The fix is probably not more note handling. Whatever restarts `fw` has to +be able to configure the card first, which means the address has to +survive somewhere `fw` does not own. That is a supervisor's job. + +This is also what blocks `svc` supervision (item 3), so one fix, two +payoffs. + +## 2. Broadcast handling is written but unwitnessed + +Broadcast and multicast are addressed directly rather than resolved: +`255.255.255.255` and the subnet broadcast to `ff:ff:ff:ff:ff:ff`, +`224/4` to `01:00:5e:...`, `ff00::/8` to `33:33:...`. Without this a +DHCP renewal would be ARPed for the gateway and unicast there, and the +lease would quietly never renew. + +The mapping is the standard one and normal traffic is unaffected, but +**I never managed to get a broadcast to cross `fw` to confirm it.** +Reviewed, not observed. + +Related and unfixed: `fw` reads the address once at startup, so a lease +that *changes* the address goes unnoticed until restart. + +## 3. fw daemonizes, so svc cannot supervise it + +The process you exec returns immediately and leaves the server behind. +`svc` would see an instant exit and, with `restart=always`, spin. + +Needs a foreground mode where the process started is the process that +stays. `svc`'s `ready=srv:name` fits: `fw -s fw.ether0` already posts to +`/srv`. + +`fwstart` is the wrong shape and should probably go. `svc` already does +dependency ordering and per-service supervision; `fwstart` re-implements +the loop in rc and then exits, so `svc` would be supervising a process +that has already gone. One service per card is the right shape. + +## 4. One card per fw, and rules cannot name a card + +Two cards means two `fw`s with two rule files. Tested and it works, but +a rule cannot say `ifc=ether0`, so one file cannot express different +policy for different cards. Wants repeatable `-e` and an `ifc=` +attribute, and those go together. + +## 5. Tflush is not implemented + +A request `fw` is blocked on cannot be abandoned, so killing a program +that is waiting for an inbound connection does not reach `fw`. Only +affects the namespace mode; nothing in packet mode blocks indefinitely. + +Doable: record the worker's pid against the Req, post an `interrupt` +note on flush, catch it with `threadnotify` so the syscall returns +`interrupted` rather than killing the proc. Maybe 80 lines. There is a +race that cannot be fully closed — between the syscall returning and the +handler clearing its entry, a note may already be in flight and land on +a worker that has moved on, failing an unrelated request. Rare, +unreproducible, and the reason to do it deliberately. + +## 6. Positional delete renumbers + +`delete n` counts lines of `rules`, so numbers shift after each delete +and `delete 3` twice removes two different rules. Inherent to positional +deletion — iptables has it too — but it should be said out loud. + +## 7. Untested at the edges + +- The gateway has only been tested between two synthetic stacks on one + machine. `run.sh` has `-gw`/`-lan` for a two-VM test; the client VM + was never built. +- No IPv6 traffic has been pushed through any mode. The code paths + exist and parse v6, but nothing has exercised them. +- No test with a real second NIC carrying real traffic. + +## Deliberately not doing + +**NAT.** See design.md. A 9front client imports `/net`; anything else +behind an IPv4 gateway with one address is the only case that needs it, +and that case can wait for someone who actually has it. + +**Rate limiting, fragment logic, ICMP type matching, deep IPv6.** Scope, +not difficulty. The useful firewall is the one that ships. -- cgit v1.2.3