summaryrefslogtreecommitdiff
path: root/fw/lib
diff options
context:
space:
mode:
authorCalvin Morrison <calvin@pobox.com>2026-08-18 17:01:49 -0400
committerCalvin Morrison <calvin@pobox.com>2026-08-18 17:01:49 -0400
commit0f922552ad8cc73c0c3c3674d484c3d78dd8c557 (patch)
tree4bfc4222ccc202daa2d79d6a782727fac1fb9ae0 /fw/lib
parent441c64d81594bb521350dbee6348f552401a1a2e (diff)
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 <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>
Diffstat (limited to 'fw/lib')
-rw-r--r--fw/lib/example.ndb42
-rw-r--r--fw/lib/fw.ndb13
-rwxr-xr-xfw/lib/fw.rc27
-rwxr-xr-xfw/lib/fwstart61
-rw-r--r--fw/lib/local.ndb7
-rw-r--r--fw/lib/none.ndb3
-rw-r--r--fw/lib/web.ndb7
7 files changed, 160 insertions, 0 deletions
diff --git a/fw/lib/example.ndb b/fw/lib/example.ndb
new file mode 100644
index 0000000..60f5119
--- /dev/null
+++ b/fw/lib/example.ndb
@@ -0,0 +1,42 @@
+#
+# every attribute fw understands. an attribute that is absent does
+# not constrain, so there is never a "*" to write; it is accepted, but
+# leaving the attribute out says the same thing more quietly.
+#
+# allow=<dir> the rule permits. dir is in, out or *
+# deny=<dir> the rule refuses
+# proto=<name> a protocol: tcp, udp, icmp, ...
+# port=<n> the port at the far end
+# lport=<n> the port at this end
+# ip=<addr> the address at the far end, optionally with a /mask
+# ipmask=<mask> the mask, if you would rather write it separately
+#
+# in and out are directions, not layers: the same rule means the same
+# thing whether fw is matching it against a packet on a wire or against
+# a connection a program asked for. connect and announce are accepted
+# as older spellings of out and in.
+#
+# rules are matched top to bottom and the first one that matches wins.
+# nothing matching means deny, so a file with no rules permits nothing.
+#
+
+# deny before allow, since the first match wins
+deny=out ip=1.1.1.1
+deny=out ip=8.8.8.8
+
+# the two spellings of a masked address are the same rule
+allow=out ip=10.0.2.0/24
+allow=out ip=192.168.0.0 ipmask=/16
+
+# v6 needs no distinguishing: it is the same attribute
+allow=out ip=2001:db8::/32
+
+# an entry may also be spread over indented continuation lines
+allow=out
+ proto=tcp
+ port=443
+
+# answer on one port, but never call out on it. lport is our end, so
+# this is the port we serve; port would be the caller's, which we do not
+# get to know until they call.
+allow=in proto=tcp lport=17019
diff --git a/fw/lib/fw.ndb b/fw/lib/fw.ndb
new file mode 100644
index 0000000..0292f53
--- /dev/null
+++ b/fw/lib/fw.ndb
@@ -0,0 +1,13 @@
+#
+# Which cards are firewalled, and with what. Read by fwstart(8) at
+# boot. The address is not named here: fw reads it from the card it
+# takes over, so this file and ipconfig(8) cannot drift apart.
+#
+# One entry per card. Everything the card sends or receives is
+# filtered; see fwrules(6) for the rule files themselves.
+#
+
+fw=ether0 rules=/lib/fw/host.ndb
+
+# a second card, if this machine has one
+#fw=ether1 rules=/lib/fw/lan.ndb
diff --git a/fw/lib/fw.rc b/fw/lib/fw.rc
new file mode 100755
index 0000000..09f2960
--- /dev/null
+++ b/fw/lib/fw.rc
@@ -0,0 +1,27 @@
+#!/bin/rc
+# fw.rc - run a command behind a firewall of its own.
+#
+# fw.rc rules.ndb cmd [arg ...]
+#
+# Order matters. The namespace is made private first, the firewall is
+# mounted second, and the devices are dropped last: once dropped they
+# cannot be regained by this process or any child, so fw has to
+# already be serving by then.
+#
+# The devices dropped are the ones that reach a network without going
+# through /net:
+# I the IP stack itself - bind -a '#I' /net undoes everything
+# l ethernet
+# u usb, which can carry an ether device
+# σ shr, where nusb publishes usbnet
+# Relax that set only if you know the machine has no other way out.
+rfork ne
+if(~ $#* 0 1){
+ echo usage: fw.rc rules.ndb cmd [arg ...] >[1=2]
+ exit usage
+}
+rules=$1
+shift
+fw $rules || exit 'fw failed'
+echo chdev '&~' 'Iluσ' >/dev/drivers || exit 'cannot drop devices'
+exec $*
diff --git a/fw/lib/fwstart b/fw/lib/fwstart
new file mode 100755
index 0000000..00b4d3a
--- /dev/null
+++ b/fw/lib/fwstart
@@ -0,0 +1,61 @@
+#!/bin/rc
+# fwstart [cfg] - start a firewall for each card named in /lib/ndb/fw.
+#
+# Run this after the network is configured and before anything dials.
+# fw reads each card's address from the card itself, so the addresses
+# have to be there already; and a program that connects before fw is up
+# is a program that was never filtered.
+rfork e
+
+cfg=/lib/ndb/fw
+if(! ~ $#* 0)
+ cfg=$1
+if(! test -f $cfg){
+ echo fwstart: no $cfg, nothing to do >[1=2]
+ exit
+}
+
+# the cards named in the config
+fn cards {
+ awk '
+ /^[ \t]*#/ { next }
+ { for(i = 1; i <= NF; i++) if($i ~ /^fw=/) print substr($i, 4) }
+ ' $cfg
+}
+
+# the rule file for one card
+fn rulesfor {
+ awk -v 'want='^$1 '
+ /^[ \t]*#/ { next }
+ {
+ dev = ""; rules = ""
+ for(i = 1; i <= NF; i++){
+ if($i ~ /^fw=/) dev = substr($i, 4)
+ if($i ~ /^rules=/) rules = substr($i, 7)
+ }
+ if(dev == want && rules != ""){ print rules; exit }
+ }
+ ' $cfg
+}
+
+# each card gets its own control directory; mntgen makes them appear
+if(! test -d /mnt/fw)
+ mntgen /mnt/fw
+
+for(name in `{cards}){
+ dev=/net/$name
+ rules=`{rulesfor $name}
+ if(! test -e $dev)
+ echo fwstart: no $dev, skipped >[1=2]
+ if not if(~ $#rules 0)
+ echo fwstart: no rules given for $name, skipped >[1=2]
+ if not if(! test -f $rules)
+ echo fwstart: $rules missing, $name skipped >[1=2]
+ if not {
+ fw -m /mnt/fw/$name -e $dev $rules
+ if(~ $status '')
+ echo fwstart: $name filtered by $rules
+ if not
+ echo fwstart: $name failed to start >[1=2]
+ }
+}
diff --git a/fw/lib/local.ndb b/fw/lib/local.ndb
new file mode 100644
index 0000000..2b846c4
--- /dev/null
+++ b/fw/lib/local.ndb
@@ -0,0 +1,7 @@
+#
+# the local network, and one port to answer on.
+#
+allow=out ip=10.0.2.0/24
+allow=out ip=192.168.0.0/16
+allow=out ip=fe80::/10
+allow=in proto=tcp lport=17019
diff --git a/fw/lib/none.ndb b/fw/lib/none.ndb
new file mode 100644
index 0000000..c12c0aa
--- /dev/null
+++ b/fw/lib/none.ndb
@@ -0,0 +1,3 @@
+#
+# no rules. the default is deny, so this program gets no network at all.
+#
diff --git a/fw/lib/web.ndb b/fw/lib/web.ndb
new file mode 100644
index 0000000..709df98
--- /dev/null
+++ b/fw/lib/web.ndb
@@ -0,0 +1,7 @@
+#
+# a browser: name resolution and the web, nothing else.
+#
+allow=out proto=udp port=53
+allow=out proto=tcp port=53
+allow=out proto=tcp port=80
+allow=out proto=tcp port=443