| Age | Commit message (Collapse) | Author |
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
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>
|
|
todo.md said fw daemonizing meant svc could not supervise it, and that
it needed a foreground mode. Both wrong.
svc has had the shape from the start. ready=srv: watches the /srv name
rather than the pid, and its own manual says why: "Use this for a
service that posts to /srv and lets the process you started exit, which
many Plan 9 file servers do on purpose. For these the /srv file is
watched and the process is not, so the process exiting is normal and
never causes a restart." init.c agrees -- reap() returns early for
Ksrv with that comment on it.
Nor is detaching unusual. 42 commands under /sys/src/cmd use
postmountsrv or threadpostmountsrv; five call srv() directly, and every
one of those is a stdio server (ramfs -i, ext4srv -s, skelfs, hjfs,
wacom) speaking 9P on file descriptors it was handed. That is not a
foreground service, it is a pipe server: no /srv, no mount, nothing to
supervise. A foreground mode for fw would buy a pid to watch, and the
/srv name is the better signal -- it survives the process that made it.
What was true underneath the wrong diagnosis: the name was not honest.
Taking the control filesystem away ends the server proc, and the relays
carried on filtering:
procs: 3 ... take the ctl filesystem away ... procs after: 2
still filtering? pkt interface: pkt0
A firewall nobody can reach, stop, or notice, and the /srv name gone
while it runs. Srv.end now takes the whole thing down, which is the
answer the relays already gave when their wire failed. Same test after:
three procs become none and the interface goes with them.
So: no flag, an example service file in lib/svc, and a SUPERVISION
section in fw(8) that says what init should watch and what restarting
will and will not fix. Restart=always brings fw back; it does not undo
taking the card, so the new fw has no address to read. Supervision
works, recovery does not, and that stays open as item 1.
Two checks. Against the previous fw.c they fail with two orphaned
procs and a pkt interface still bound -- and so do the leak checks at
the end of the run, which is what they were built for. 77 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
man/fw said a fragmented datagram does not cross. IPv4 ones now do:
the first piece decides and the train inherits. IPv6 fragments still
do not, because the extension header they live in is not walked, and
that is what the paragraph says now.
The altitude paragraph said request filtering cannot stop an inbound
connection before the handshake. Still true, and still worth saying,
but it now refuses the connection to the program and hangs it up, which
is the difference between a rule that is late and one that does
nothing. fwrules(6) gains the other half of that: an in rule naming an
ip cannot decide an announce, and decides the connection instead.
todo.md loses the six items that are now fixed and gains a third round.
One of them is a correction rather than a fix: the interface tables
being read into fixed buffers was reported as losing the default route,
and it was not -- routes come out sorted and 0.0.0.0 sorts first, so it
was always on the first line. Worth removing the limits anyway; not
worth having claimed it broke something.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
todo.md had three items in both halves of itself. Control files being
world-writable, delete 0, and a dead relay leaving half a firewall were
all in "Fixed since the review" as items 10, 11 and 12, and all three
were still sitting in "Worth fixing" underneath. Anyone working the
open list would have redone them. Now an item appears in one half or
the other, and the first round's entries are one line each, since the
detail is in the commit and the value of this file is the part that is
still true.
Fragments got a sentence that described the mechanism and not the
consequence. "Later fragments now match on addresses and protocol
only" reads as though they would pass under an address rule; what
actually happens is that every rule set in fwrules(6) is written in
ports, later fragments match no port, and the datagram does not cross.
That is the right trade against reading ports out of them, which let a
crafted fragment through, but it is a hole in what works and belongs in
BUGS rather than in a changelog line.
man/fw also now says what the served /net contains, which changed
underneath it and was never written down; and it still said the card
was "not undone reliably", when nothing undoes it at all.
design.md gains the whitelist decision next to the others, because the
argument for it is the same one the rule parser already makes and the
next person to add a file to the served tree should meet it.
The mtu comment claimed a card as its reason while sitting in code both
modes use. Between two stacks there is no card; 1500 is still right,
for a reason worth one sentence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The serious one is that the ctl filter was a blacklist. checkctl looked
at connect and announce and passed everything else, but udpctl takes
"headers", and udpcreate gives a conversation a live write queue at clone
time:
c->wq = qbypass(udpkick, c);
So three writes -- clone, "headers", a header-prefixed datagram to data --
sent a packet anywhere, with no connect for a rule to match. rudp and
icmpv6 have the same verb, gre has raw and forward. none.ndb did not
mean "no network at all", though the manual said it did. It is now a
whitelist of control messages that cannot reach the network by
themselves, which is the argument this code already made about ndb
attributes it does not recognise, applied where it was not.
Also blocking: %M was never installed, so fmtrules emitted ipmask=%M% and
every ctl edit on a rule set containing ip= failed, while save wrote a
file reload would reject. Tests had exercised the ctl path and the ip=
path but never together.
parserules built the new list in the globals with no lock, so for the
length of a reload the relay procs walked a list that was empty and then
half built -- exactly what installrules' comment promised could not
happen. etherwriteip put a 64KB frame on a 32KB proc stack, the same bug
design.md records learning and fixing in relay(). putback and
notehandler were dead code: atexit matches on the registering pid and
_exits never runs the handlers, which is why the cleanup "did not fire"
rather than being flaky. Nothing puts the card back, and the docs that
said otherwise are corrected.
The rest: expired flows kept matching and refreshing themselves; the pkt
interface claimed a 4096 MTU from a 1514-byte card; ports were read out
of non-first fragments; /net/ndb and /net/log were writable and
ipifc/*/data readable through the filter; control files were
world-writable, and owning them as a user called "fw" locked out the
administrator instead; delete 0 appended a rule reading <nil>; a dead
relay left one direction unfiltered with nothing to notice; and IPv6
unicast under -e was dropped in silence when it is simply not
implemented.
All three modes regression tested after: a namespace refusing headers and
port 22 while allowing 443, a card passing https and then blocking it
live, and the machine's network restored afterwards.
doc/todo.md says which of these were reproduced and which were read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
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 <side> <side> 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 <noreply@anthropic.com>
|