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/man/fw | 482 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ fw/man/fwrules | 224 +++++++++++++++++++++++++++ 2 files changed, 706 insertions(+) create mode 100644 fw/man/fw create mode 100644 fw/man/fwrules (limited to 'fw/man') diff --git a/fw/man/fw b/fw/man/fw new file mode 100644 index 0000000..34a9d19 --- /dev/null +++ b/fw/man/fw @@ -0,0 +1,482 @@ +.TH FW 8 +.SH NAME +fw \- firewall +.SH SYNOPSIS +.B fw +.RB [ -dSW ] +.RB [ -m +.IR ctlmtpt ] +.RB [ -n +.IR net ] +.RB [ -s +.IR srv ] +.br +.RB [ -e +.I ether +.B -a +.IR addr / mask +.RB [ -g +.IR gateway ]] +.br +.I rules +.RI [ outside +.IR inside ] +.SH DESCRIPTION +.I Fw +is a network firewall. +It can filter a network card, the traffic crossing between two networks, +or the traffic of a single namespace. +Rules are written in an +.IR ndb (6) +file; see +.B RULES +below. +.PP +The three ways of running it follow. +The first two filter +.IR packets , +as they cross a wire; the third filters +.IR requests , +before any packet exists. +.SS Filtering a network card +.IP +.EX +fw -e /net/ether0 -a 10.0.2.15/24 -g 10.0.2.2 /lib/fw/host.ndb +.EE +.PP +Everything sent or received through that card is filtered, whoever sent +it. +One card to one +.IR fw , +so a machine with two cards to filter runs two of them. +.PP +The card cannot stay attached to the IP stack, or packets would reach it +whatever the rules said. +So +.I fw +takes the card and hands the stack a +.I pkt +interface instead - a card with +.I fw +on the other end of it: +.IP +.EX +network ---- ether0 [ fw ] stack ---- your programs +.EE +.PP +The stack keeps the address it had, given as +.BI -a " addr/mask"\f1, +and +.B -g +names the gateway. +Nothing is left doing ARP for an address whose card has been taken away, +so +.I fw +answers for it and resolves the next hop itself. +.SS Gateway, between two networks +.IP +.EX +fw /lib/fw/gate.ndb /net.wan!203.0.113.2!/24 /net.lan!10.0.0.1!/24 +.EE +.PP +.I Fw +sits between two networks and decides, packet by packet, what may cross: +.IP +.EX +internet ---- ether0 [ fw ] ether1 ---- your machines + outside inside +.EE +.PP +The side facing the untrusted network is the +.IR outside , +the side facing the machines you are protecting is the +.IR inside . +Rules are written in those terms, so which side is which matters. +.PP +Every packet between the two networks goes through +.IR fw , +so one the rules forbid is thrown away before the far side ever sees it. +Nothing is opened, nothing replies, and the sender is told nothing. +A side is written +.IB net ! addr ! mask\f1, +naming the mountpoint of an IP stack and the address to give the +interface +.I fw +creates on it. +.SS Filtering one namespace +.IP +.EX +fw.rc /lib/fw/web.ndb mothra +.EE +.PP +Given neither of those, +.I fw +serves a synthetic +.B /net +in place of +.IR ip (3), +and every program in that namespace reaches the network through it. +A write of +.B connect +or +.B announce +to a protocol ctl file is matched against the rules before it reaches +the kernel. +A refused write fails, and the diagnostic is what +.IR dial (2) +reports to the program that tried it. +.PP +Because the policy lives in a namespace, each program can have its own. +Filtering a program's requests is not a boundary by itself - see +.B CONTAINMENT +below - so to sandbox one, run +.B /lib/fw/fw.rc +rather than +.I fw +directly: it builds the namespace, mounts the filter, drops the devices +that would go around it, and only then runs the program. +.PP +The difference is one of altitude. +Packet filtering sees everything, including traffic no local program +asked for, but cannot tell one program from another. +Request filtering knows exactly who asked, but only ever sees intentions, +so it cannot stop an inbound connection before the handshake and cannot +see traffic that is merely passing through. +.PP +The options are: +.TP +.B -W +require wire (packet) filtering; exit rather than fall back to filtering +requests. +.TP +.B -S +do not track connections. +Every packet is then matched against the rules, and a rule permitting +traffic one way does not permit the replies. +.TP +.B -d +report every packet and every decision on standard error. +.TP +.BI -m " ctlmtpt" +mount the control files here, default +.BR /mnt/fw . +Packet filtering only. +.TP +.BI -e " ether" +take this card and filter the packets of this machine, eg +.BR /net/ether0 . +.TP +.BI -a " addr/mask" +the address the stack should keep once its card has been taken. +Without it +.I fw +reads the address and mask off the card it is taking, which is almost +always what you want; give it only to use something other than the +address the card already has. +.TP +.BI -g " gateway" +the next hop for anything off this network. +Without it +.I fw +takes the default route the stack was already using; if there was none, +only the local network is reachable. +.TP +.BI -n " net" +filter this stack rather than +.BR /net . +.TP +.BI -s " srv" +post the served filesystem on +.BI /srv/ srv +as well as mounting it. +Which filesystem that is depends on the mode, and so does the risk. +Filtering packets it is the control files, and anyone who can open them +can rewrite the rules; filtering requests it is the synthetic +.BR /net , +and anyone who can mount it has the network the rules were meant to +ration. +Either way, do not post it anywhere the traffic being filtered can reach +.BR /srv . +.SH RULES +Rules live in an +.IR ndb (6) +file, matched from the top, first match deciding, and traffic matching +nothing denied. +Connections are tracked, so permitting traffic one way permits the +replies. +The file format, the attributes, and worked rule sets are in +.IR fwrules (6). +.SH CONTROL +When filtering packets, +.I fw +serves four files, by default under +.BR /mnt/fw . +There are none when filtering requests: that policy is chosen when the +namespace is built and lasts as long as it does, so to change it, build +the namespace again. +.TP +.B ctl +Read it for the commands it takes. +Writing a rule installs it at once. +A rule that will not parse fails the write and leaves the running rules +alone. +.TP +.B rules +The current rules, as ndb, one rule per line and numbered from one in +the order shown - so rule +.I n +in a diagnostic is line +.I n +here, which is what +.B delete +counts. +What it prints can be written back unchanged. +.IP +Writing it replaces the whole set. +Writes accumulate and are applied when the file is closed, so a set +arriving in several messages is still installed in one step and no packet +is matched against half of it. +A clunk cannot report an error, so a set that will not parse is noted in +.B /sys/log/fw +and the running rules are kept; +.B ctl +is the way in if you want to be told. +.TP +.B flows +The connections being tracked. +.TP +.B stats +Packets passed and dropped, then each rule with the number of decisions +it has made. +A rule at zero is either dead or waiting for something that has not +happened, and it is worth knowing which. +.PP +The commands accepted by +.B ctl +are +.BR prepend , +.BR append , +.B delete +.IR n , +.BR flush , +.B reload +.RI [ file ], +.B save +.RI [ file ], +and +.BR flushflows . +.B Reload +and +.B save +with no argument use the file +.I fw +was started with. +.PP +There are three things called the rules: the file, what is running, and +what has been typed at +.BR ctl . +.B Reload +makes the running rules match the file, discarding anything typed; +.B save +makes the file match the running rules, keeping it. +Neither happens on its own. +.SH CONTAINMENT +Filtering +.B /net +achieves nothing on its own, because +.IP +.EX +bind -a '#I' /net +.EE +.PP +puts the real stack back. +A filtered program must also be denied the devices that reach a network +directly, through +.B /dev/drivers +(see +.IR cons (3)): +.IP +.EX +echo chdev '&~' 'Iluσ' >/dev/drivers +.EE +.PP +That mask can only ever have bits added, and every new process group +inherits it, so nothing below can undo it. +Note that the +.B & +form permits +.I only +the devices named, so using it to try to regain one silently drops +everything else instead. +.PP +.B /lib/fw/fw.rc +does this in the right order: private namespace, mount, drop, exec. +.SH BOOT +.B /lib/fw/fwstart +starts a firewall for each card named in +.BR /lib/ndb/fw , +which is an +.IR ndb (6) +file of one entry per card: +.IP +.EX +fw=ether0 rules=/lib/fw/wan.ndb +fw=ether1 rules=/lib/fw/lan.ndb +.EE +.PP +No address appears there. +.I Fw +reads each card's address, mask and gateway from the card it is taking +over, so this file cannot drift out of step with +.IR ipconfig (8). +Control files land under +.BI /mnt/fw/ ether0 +and so on, one directory per card. +.PP +Run it after the network is configured and before anything dials. +Too early and there is no address to read; too late and something has +already connected unfiltered. +A card named here that does not exist, or a rule file that is missing, +is reported and skipped rather than stopping the rest. +.SH EXAMPLES +Filter this machine's card, keeping the address it already has: +.IP +.EX +fw -e /net/ether0 -a 10.0.2.15/24 -g 10.0.2.2 /lib/fw/host.ndb +.EE +.PP +Filter two cards, one +.I fw +each: +.IP +.EX +fw -m /mnt/fw/ether0 -e /net/ether0 -a 198.51.100.7/24 -g 198.51.100.1 \ + /lib/fw/wan.ndb +fw -m /mnt/fw/ether1 -e /net/ether1 -a 10.0.0.1/24 /lib/fw/lan.ndb +.EE +.PP +Sit between two networks: +.IP +.EX +fw /lib/fw/gate.ndb /net.wan!203.0.113.2!/24 /net.lan!10.0.0.1!/24 +.EE +.PP +Sandbox one program, which may resolve names and speak https: +.IP +.EX +fw.rc /lib/fw/web.ndb mothra +.EE +.PP +Give a program no network whatever. +An empty rule file permits nothing: +.IP +.EX +fw.rc /lib/fw/none.ndb troff -ms doc +.EE +.PP +Block a port on a running firewall, at once, including anything already +connected: +.IP +.EX +echo prepend deny=in proto=tcp lport=80 >/mnt/fw/ctl +.EE +.PP +Edit the rule file and apply it, or keep what was typed at +.BR ctl : +.IP +.EX +echo reload >/mnt/fw/ctl +echo save >/mnt/fw/ctl +.EE +.PP +See what a rule has actually done: +.IP +.EX +cat /mnt/fw/stats +.EE +.SH FILES +.TP +.B /lib/fw +rule sets +.TP +.B /lib/fw/fwstart +starts one firewall per card at boot +.TP +.B /lib/ndb/fw +which cards are filtered, and with what +.TP +.B /mnt/fw +control files +.TP +.B /sys/log/fw +where rules marked +.B log=yes +are recorded +.SH SOURCE +.B /sys/src/cmd/fw +.SH "SEE ALSO" +.IR ip (3), +.IR cons (3), +.IR fwrules (6), +.IR ndb (6), +.IR dial (2), +.IR syslog (2), +.IR fork (2), +.IR ipconfig (8) +.SH BUGS +There is no address translation. +A machine behind a +.I fw +gateway needs a routable address, or must import +.B /net +rather than route through it. +.PP +Packet filtering drops silently, so a refused connection is discovered +by timing out. +Request filtering fails +.IR dial (2) +immediately with a reason, which is much easier to diagnose. +.PP +IPv6 extension headers are not walked; such packets are matched on their +addresses and next-header protocol alone. +ICMP has no type or code matching. +.PP +Taking a card is destructive and is not undone reliably. +The interface that replaces it is unbound when +.I fw +stops, and the address goes with it, so a +.I fw +that is killed leaves the card bound to nothing and the machine with no +network. +It cannot be restarted unaided either: the address it would have read +off the card is the address that has just been lost, so it must be told +one with +.BR -a , +or the card configured again with +.IR ipconfig (8) +first. +.I Fw +tries to put the card back as it exits, which covers an orderly stop but +not a kill. +Whatever restarts +.I fw +should be prepared to configure the card first. +.PP +The first packet to an unresolved next hop is dropped while +.I fw +asks for its ethernet address, exactly as any other stack would, so a +run of +.I fw +begins with one drop that no rule caused. +.PP +.I Fw +does not implement +.BR Tflush , +so a request it is blocked on cannot be abandoned. +While filtering requests this affects waiting for an inbound connection, +which is the one operation that blocks indefinitely: giving up on it does +not reach +.IR fw , +which stays waiting until a connection arrives. + + diff --git a/fw/man/fwrules b/fw/man/fwrules new file mode 100644 index 0000000..143ac08 --- /dev/null +++ b/fw/man/fwrules @@ -0,0 +1,224 @@ +.TH FWRULES 6 +.SH NAME +fwrules \- firewall rule files +.SH DESCRIPTION +.IR Fw (8) +decides what may cross a network by matching traffic against a file of +rules. +The file is an +.IR ndb (6) +file: one entry is one rule. +.PP +A rule set for a machine that may look up names and fetch pages, and do +nothing else: +.IP +.EX +allow=out proto=udp port=53 +allow=out proto=tcp port=53 +allow=out proto=tcp port=80 +allow=out proto=tcp port=443 + +deny=* log=yes +.EE +.PP +Rules are matched from the top, the first one that matches decides, and +traffic matching none of them is denied. +A file with no rules therefore permits nothing, and the last rule above +changes nothing about what is allowed - it exists so that the refusals +are written down instead of happening silently. +.PP +There is no rule permitting the replies to any of this, and none is +needed: see +.B STATE +below. +.SH ACTION +Every rule begins with an action, whose value is the direction it +governs: +.TP +.BI allow= dir +permit. +.TP +.BI deny= dir +refuse. +.PP +.I Dir +is +.BR out , +.BR in , +or +.B * +for either. +.B Out +is traffic begun from the side being protected; +.B in +is traffic begun toward it. +That holds wherever the rule is enforced: on a gateway the protected +side is the inside, and in a namespace it is the program, whose +.B connect +is +.B out +and whose +.B announce +is +.BR in . +.B Connect +and +.B announce +are accepted as older spellings of +.B out +and +.BR in . +.SH ATTRIBUTES +The rest of a rule says what it matches. +An attribute that is absent does not constrain, so there is never a +.B * +to write: +.TP +.BI proto= name +a protocol: +.BR tcp , +.BR udp , +.BR icmp , +and so on. +.TP +.BI port= n +the port at the far end. +.TP +.BI lport= n +the port at this end. +A program announcing a port is naming this end, so +.B announce 17019 +is matched by +.BR lport=17019 , +and by +.B port= +never - at that moment nobody has called, so there is no far end. +.TP +.BI ip= address +the address at the far end, optionally carrying a +.BI / mask +suffix. +IPv4 and IPv6 are written the usual way and need no distinguishing. +.TP +.BI ipmask= mask +the mask, written separately, as +.B /24 +or in full. +.TP +.B log=yes +note every match of this rule in +.BR /sys/log/fw . +.PP +An unrecognised attribute is an error, and +.I fw +refuses to start rather than run with it ignored: a mistyped constraint +would otherwise silently widen the rule it was meant to narrow. +.PP +An entry may be spread over indented continuation lines, as any +.IR ndb (6) +entry may: +.IP +.EX +allow=out + proto=tcp + port=443 +.EE +.SH ORDER +The first match decides, so a rule carving an exception out of a +broader rule must come above it. +This is right: +.IP +.EX +deny=out ip=1.1.1.1 +allow=out proto=tcp port=443 +.EE +.PP +and this is not, because 1.1.1.1:443 matches the allow first and the +deny is never reached: +.IP +.EX +allow=out proto=tcp port=443 +deny=out ip=1.1.1.1 +.EE +.PP +Rules are numbered from one in the order they appear, and that is the +number a refusal names in +.BR /sys/log/fw . +.SH STATE +Connections are tracked, so a rule permitting traffic one way permits +the replies without a second rule. +A permitted packet records the protocol and both addresses and ports; +anything matching that, either way round, passes without consulting the +rules again. +.PP +UDP has no connections, so a flow is that same tuple and lasts 60 +seconds after the last packet. +TCP lasts 300 seconds, everything else 30. +ICMP has no ports, so its flows are the two addresses alone, which is +enough for a reply to an echo to be recognised, but does not tie an +ICMP error to the connection it is about. +.PP +When the rules change, connections the new rules forbid are dropped +rather than left to finish: a block blocks. +.SH EXAMPLES +A gateway. +The machines behind it may reach the web, one host is refused outright, +and the only thing the internet may reach is a web server: +.IP +.EX +# the exception first, or the allows below would match +# 1.1.1.1:443 before this was ever reached +deny=out ip=1.1.1.1 + +# out: what the machines behind me may reach +allow=out proto=udp port=53 +allow=out proto=tcp port=53 +allow=out proto=tcp port=80 +allow=out proto=tcp port=443 + +# in: what the internet may reach here +allow=in proto=tcp lport=443 + +# and note anything else that tries, either way +deny=* log=yes +.EE +.PP +On a gateway facing the internet that last rule will log a great deal, +since the internet knocks on every door constantly. +Narrow it to +.B deny=out +if only the traffic from your own machines is worth recording. +.PP +A program that may resolve names and fetch pages over TLS, and nothing +else: +.IP +.EX +allow=out proto=udp port=53 +allow=out proto=tcp port=443 +.EE +.PP +A service that answers on one port and never calls out: +.IP +.EX +allow=in proto=tcp lport=17019 +.EE +.PP +No network at all. +An empty file permits nothing, so this is a complete rule set: +.IP +.EX +# nothing +.EE +.SH FILES +.TP +.B /lib/fw +rule sets +.TP +.B /lib/fw/example.ndb +every attribute, with comments +.SH "SEE ALSO" +.IR fw (8), +.IR ndb (6) +.SH BUGS +A rule cannot name which network card it applies to, so a machine +filtering two cards needs a file for each. -- cgit v1.2.3