summaryrefslogtreecommitdiff
path: root/fw
diff options
context:
space:
mode:
Diffstat (limited to 'fw')
-rw-r--r--fw/doc/design.md171
-rw-r--r--fw/doc/todo.md103
-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
-rw-r--r--fw/man/fw482
-rw-r--r--fw/man/fwrules224
-rw-r--r--fw/src/ether.c374
-rw-r--r--fw/src/fw.c1391
-rw-r--r--fw/src/mkfile9
-rw-r--r--fw/src/netfs.c555
-rw-r--r--fw/src/rules.c440
-rw-r--r--fw/src/rules.h57
17 files changed, 3966 insertions, 0 deletions
diff --git a/fw/doc/design.md b/fw/doc/design.md
new file mode 100644
index 0000000..d6f3e55
--- /dev/null
+++ b/fw/doc/design.md
@@ -0,0 +1,171 @@
+# fw
+
+**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`, item 1.
+
+## Why
+
+9front has no firewall. The usual answer is that it does not need one:
+few services run, and they are listed in `/rc/bin/service`. That answers
+inbound and says nothing about outbound, which is the question that
+matters now — not "who can reach this machine" but "who can *this
+program* reach".
+
+The manifesto asks for:
+
+> a software-level firewall implementing a /net interface that sits
+> between network interfaces; it can be used as a global firewall or
+> placed in front of a namespace
+
+Both halves of that sentence are now one program, and the same rule file
+drives either.
+
+## The three modes
+
+ 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; the third filters requests before
+any packet exists. The mode is what you point it at, printed at startup,
+never inferred silently.
+
+### Filtering a card
+
+A card cannot stay attached to the IP stack and be filtered — packets
+would reach the stack whatever the rules said. So `fw` takes the card
+and hands the stack a `pkt` interface instead:
+
+ network ---- ether0 [ fw ] stack ---- programs
+
+`pktmedium` is the whole trick, and it is three lines of kernel:
+
+ static void
+ pktbwrite(Ipifc *ifc, Block *bp, int, uchar*, Routehint*)
+ {
+ qpass(ifc->conv->rq, bp);
+ }
+
+Reading `/net/ipifc/N/data` gives packets the stack wants to send;
+writing injects packets as if received. No hardware, no wire — the
+reader *is* the wire. That is why filtering works there and not at
+`/net/ether0`, where reading gives you a copy and the stack gets the
+packet anyway.
+
+Because the stack no longer has ethernet, nothing is doing ARP for its
+address any more, so `fw` does: it answers requests for the address it is
+standing in for and resolves the next hop itself.
+
+An ether `bypass` connection looks like the right primitive and is not:
+it intercepts only the stack's transmissions, and `etheriq` discards what
+arrives from the wire while it is set. Measured, not assumed.
+
+### Filtering a namespace
+
+`fw` serves a filtered view of `/net` and mounts it back. Almost
+everything passes through; a write of `connect` or `announce` to a
+protocol ctl file is matched against the rules first. A refusal fails the
+write, and the text comes back out of `dial(2)` — which is a far better
+diagnostic than a dropped packet.
+
+Because it proxies the *assembled* `/net` rather than synthesising a
+tree, `cs` and `dns` come along for free.
+
+The real `/net` needs no second name and must not have one: any surviving
+path to it is a way around the filter. lib9p forks the server with
+`RFNAMEG` (`postsrv`, `/sys/src/lib9p/post.c`), so the server keeps a
+private copy of the namespace from before the mount. Inside, `/net` is
+real; outside, it is `fw`.
+
+## What makes the namespace mode binding
+
+Filtering `/net` achieves nothing on its own, because
+
+ bind -a '#I' /net
+
+puts the real stack back. 9front already has the answer, and it is
+better than the design needed. Each process group carries a mask of
+permitted device drivers, written through `/dev/drivers`:
+
+ echo chdev '&~' 'Iluσ' >/dev/drivers
+
+Two properties make it right: `devmask` in `pgrp.c` only ever ORs bits
+in, so access can only be removed; and the mask is copied into every new
+process group with the comment *"always inherit devmask
+unconditionally"*. So a process that has lost `#I` cannot regain it and
+cannot fork a child that has it.
+
+`RFNOMNT` is the same mechanism with a fixed whitelist and is too blunt —
+it also forbids all mounting. Note the trap: `chdev &` permits *only*
+what is named, so trying to regain a device with it silently drops
+everything else instead.
+
+## Decisions, and why
+
+**ndb for rules.** The house format, and it deleted a hand-rolled
+parser. Read with `ndbparse`, which returns entries in file order, so it
+stays an ordered list and not a lookup. An unrecognised attribute is
+fatal: silently ignoring `prot=tcp` would leave a rule matching every
+protocol, and a typo that fails open is not something a firewall may do.
+
+**First match wins, default deny.** Firewall matching is a solved
+interface. Being different about it would be cost for its own sake.
+
+**`in` and `out`, not `connect` and `announce`.** Direction, not layer,
+so one rule file works at both altitudes unchanged. Getting this right
+exposed a real bug: `announce 443` was matched by `port=` at the request
+layer and `lport=` at the packet layer, so each spelling silently never
+matched in the other mode.
+
+**Stateful by default.** Without it a rule set is unwritable: permitting
+a connection out would mean permitting every reply back in, which means
+opening every ephemeral port and calling it strict.
+
+**A block blocks.** When the rules change, live connections the new rules
+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.
+
+**Per-rule logging to `/sys/log/fw`.** Global logging either floods a
+disk or tells you nothing. `syslog(2)` does not create its file, so `fw`
+says so at startup rather than dropping the lines silently.
+
+**No NAT.** It is not a firewall feature, it is a workaround for IPv4
+address scarcity, and it is needed in exactly one configuration:
+non-Plan 9 clients behind an IPv4 gateway with one address. 9front
+clients import `/net` instead, which gets NAT's effect with none of its
+code and per-client rules as a bonus. Keeping translation out of
+filtering is also just correct design — it is why iptables has separate
+`filter` and `nat` tables.
+
+## Things learned the hard way
+
+- **`bind '#l' /net` brings in only `ether0`.** A second card is attached
+ by the kernel but invisible until `bind -a '#l1' /net`. The boot
+ messages show `#l0` and `#l1` in a font where `l` looks like `1`.
+- **`@{...}` in rc does not fork the namespace** —
+ `rfork(RFPROC|RFFDG|RFREND)`, no `RFNAMEG`. A mount inside it lands in
+ the calling shell and stays. Over a long-lived shell this accumulates
+ and reads exactly like a regression in whatever you last changed.
+- **IP stacks outlive the programs that configure them.** A stale
+ interface on `#I1` from an earlier run pointed routes at a dead wire
+ and cost an afternoon.
+- **`proccreate` stacks are small.** A 64KB packet buffer on one
+ corrupted the data segment and surfaced as a *string constant* reading
+ back wrong, not as a crash.
+- **Ethernet has a 60-byte minimum** and devether enforces it: 42-byte
+ ARP frames were refused with "read or write too small". Small IP
+ packets would have failed the same way.
+- **Anything backgrounded from the serial shell inherits `/dev/eia0`**
+ and will eat the console's input.
+
+## What it cannot do
+
+It filters connections and packets, not flows over time: no rate limits,
+no fragment logic, no ICMP type matching, and IPv6 extension headers are
+not walked. Forwarded traffic is invisible to the namespace mode by
+construction — it never becomes a ctl write, because no local program
+asked for it.
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.
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
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.
diff --git a/fw/src/ether.c b/fw/src/ether.c
new file mode 100644
index 0000000..54f5068
--- /dev/null
+++ b/fw/src/ether.c
@@ -0,0 +1,374 @@
+/*
+ * fw, the ethernet side.
+ *
+ * To filter a real machine, fw has to be the wire: the card cannot
+ * stay attached to the IP stack, or packets reach it whatever we
+ * decide. So fw opens /net/etherN itself and the protected stack gets
+ * a pkt interface instead - a fake card with fw on the other end.
+ *
+ * ether0 ---- fw ---- pkt ---- the stack ---- programs
+ *
+ * The stack no longer has ethernet, so nobody is doing ARP for it any
+ * more, and fw has to: answer requests for the address we are
+ * protecting, and resolve the next hop for anything we send. That is
+ * all the ethernet fw knows about; everything else it treats as an IP
+ * packet and hands to the rules.
+ *
+ * An ether "bypass" connection looks like it should serve instead, but
+ * it only intercepts the stack's transmissions - etheriq drops what
+ * arrives from the wire while bypass is set - so it cannot filter
+ * inbound at all.
+ */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ndb.h>
+#include <ip.h>
+#include "rules.h"
+
+extern int etherdebug;
+
+enum
+{
+ Eaddrlen = 6,
+ Ehdrlen = 14,
+ Etip4 = 0x0800,
+ Etarp = 0x0806,
+ Etip6 = 0x86DD,
+
+ Arplen = 28,
+ Eminlen = 60, /* ethernet minimum frame, devether enforces it */
+ Arpreq = 1,
+ Arpreply = 2,
+
+ Narp = 64,
+ Arplife = 300,
+};
+
+typedef struct Arpent Arpent;
+struct Arpent
+{
+ uchar ip[IPaddrlen];
+ uchar mac[Eaddrlen];
+ long when;
+};
+
+static Arpent arptab[Narp];
+static Lock arplock;
+
+static uchar ourmac[Eaddrlen];
+static uchar ouraddr[IPaddrlen];
+static uchar gateway[IPaddrlen];
+static int haveg;
+static int efd = -1;
+
+static uchar bcast[Eaddrlen] = { 0xff,0xff,0xff,0xff,0xff,0xff };
+
+/*
+ * Open a card for raw frames of every type. Promiscuous because we
+ * are answering for an address the card does not believe is its own
+ * once the stack has let go of it.
+ */
+int
+etheropen(char *dev, uchar *mac)
+{
+ char path[128], buf[64];
+ int cfd, dfd, n, conn;
+
+ snprint(path, sizeof path, "%s/clone", dev);
+ if((cfd = open(path, ORDWR)) < 0)
+ sysfatal("open %s: %r", path);
+ if((n = read(cfd, buf, sizeof buf - 1)) <= 0)
+ sysfatal("read %s: %r", path);
+ buf[n] = '\0';
+ conn = atoi(buf);
+
+ if(fprint(cfd, "connect -1") < 0)
+ sysfatal("%s: connect -1: %r", dev);
+ if(fprint(cfd, "promiscuous") < 0)
+ sysfatal("%s: promiscuous: %r", dev);
+
+ snprint(path, sizeof path, "%s/%d/data", dev, conn);
+ if((dfd = open(path, ORDWR)) < 0)
+ sysfatal("open %s: %r", path);
+
+ /* the card's own address, which we answer with */
+ snprint(path, sizeof path, "%s/addr", dev);
+ if((n = open(path, OREAD)) < 0)
+ sysfatal("open %s: %r", path);
+ if(read(n, buf, 12) != 12)
+ sysfatal("read %s: %r", path);
+ close(n);
+ buf[12] = '\0';
+ if(parseether(mac, buf) < 0)
+ sysfatal("%s: unparseable address %s", dev, buf);
+
+ efd = dfd;
+ memmove(ourmac, mac, Eaddrlen);
+ return dfd;
+}
+
+void
+ethersetaddr(uchar *ip, uchar *gw, int haveit)
+{
+ ipmove(ouraddr, ip);
+ if(haveit)
+ ipmove(gateway, gw);
+ haveg = haveit;
+}
+
+static void
+arpput(uchar *ip, uchar *mac)
+{
+ Arpent *a, *old;
+ long now;
+ int i;
+
+ now = time(0);
+ lock(&arplock);
+ old = &arptab[0];
+ for(i = 0; i < Narp; i++){
+ a = &arptab[i];
+ if(a->when != 0 && ipcmp(a->ip, ip) == 0){
+ memmove(a->mac, mac, Eaddrlen);
+ a->when = now;
+ unlock(&arplock);
+ return;
+ }
+ if(a->when < old->when)
+ old = a;
+ }
+ ipmove(old->ip, ip);
+ memmove(old->mac, mac, Eaddrlen);
+ old->when = now;
+ unlock(&arplock);
+}
+
+static int
+arpget(uchar *ip, uchar *mac)
+{
+ Arpent *a;
+ long now;
+ int i, r;
+
+ r = 0;
+ now = time(0);
+ lock(&arplock);
+ for(i = 0; i < Narp; i++){
+ a = &arptab[i];
+ if(a->when != 0 && ipcmp(a->ip, ip) == 0){
+ if(now - a->when <= Arplife){
+ memmove(mac, a->mac, Eaddrlen);
+ r = 1;
+ }
+ break;
+ }
+ }
+ unlock(&arplock);
+ return r;
+}
+
+/*
+ * Broadcast and multicast are not resolved, they are addressed by rule.
+ * Without this a DHCP renewal, which goes to 255.255.255.255, would be
+ * sent to whatever the gateway's ethernet address happened to be, and
+ * the lease would quietly never renew.
+ */
+static int
+groupmac(uchar *dst, uchar *mask, uchar *mac)
+{
+ uchar net[IPaddrlen], all[IPaddrlen];
+ int i;
+
+ if(isv4(dst)){
+ /* 255.255.255.255 */
+ for(i = IPv4off; i < IPaddrlen; i++)
+ if(dst[i] != 0xff)
+ break;
+ if(i == IPaddrlen){
+ memmove(mac, bcast, Eaddrlen);
+ return 1;
+ }
+ /* the broadcast address of our own network */
+ maskip(ouraddr, mask, net);
+ for(i = 0; i < IPaddrlen; i++)
+ all[i] = net[i] | ~mask[i];
+ if(ipcmp(dst, all) == 0){
+ memmove(mac, bcast, Eaddrlen);
+ return 1;
+ }
+ /* 224.0.0.0/4 */
+ if(dst[IPv4off] >= 224 && dst[IPv4off] < 240){
+ mac[0] = 0x01;
+ mac[1] = 0x00;
+ mac[2] = 0x5e;
+ mac[3] = dst[IPv4off+1] & 0x7f;
+ mac[4] = dst[IPv4off+2];
+ mac[5] = dst[IPv4off+3];
+ return 1;
+ }
+ return 0;
+ }
+ /* ff00::/8 */
+ if(dst[0] == 0xff){
+ mac[0] = 0x33;
+ mac[1] = 0x33;
+ memmove(mac+2, dst+12, 4);
+ return 1;
+ }
+ return 0;
+}
+
+static void
+puthdr(uchar *f, uchar *dst, int type)
+{
+ memmove(f, dst, Eaddrlen);
+ memmove(f + Eaddrlen, ourmac, Eaddrlen);
+ f[12] = type >> 8;
+ f[13] = type;
+}
+
+/* ask who has ip; the answer arrives later and goes in the cache */
+static void
+arpask(uchar *ip)
+{
+ uchar f[Eminlen];
+
+ if(!isv4(ip))
+ return;
+ memset(f, 0, sizeof f);
+ puthdr(f, bcast, Etarp);
+ hnputs(f + 14, 1); /* ethernet */
+ hnputs(f + 16, Etip4);
+ f[18] = Eaddrlen;
+ f[19] = 4;
+ hnputs(f + 20, Arpreq);
+ memmove(f + 22, ourmac, Eaddrlen);
+ memmove(f + 28, ouraddr + IPv4off, 4);
+ memmove(f + 38, ip + IPv4off, 4);
+ if(etherdebug)
+ fprint(2, "arp: who has %I? (asking)\n", ip);
+ if(write(efd, f, sizeof f) != sizeof f)
+ fprint(2, "arp: write failed: %r\n");
+}
+
+/*
+ * An arp frame from the wire. Learn from it either way, and answer a
+ * request for the address we are standing in for.
+ */
+static void
+arpin(uchar *f, int n)
+{
+ uchar sip[IPaddrlen], tip[IPaddrlen], r[Eminlen];
+ int op;
+
+ if(n < Ehdrlen + Arplen)
+ return;
+ if(nhgets(f + 16) != Etip4 || f[18] != Eaddrlen || f[19] != 4)
+ return;
+ op = nhgets(f + 20);
+ v4tov6(sip, f + 28);
+ v4tov6(tip, f + 38);
+
+ if(ipcmp(sip, IPnoaddr) != 0){
+ arpput(sip, f + 22);
+ if(etherdebug)
+ fprint(2, "arp: learned %I is %E (op %d)\n", sip, f+22, op);
+ }
+ if(op != Arpreq || ipcmp(tip, ouraddr) != 0)
+ return;
+
+ memset(r, 0, sizeof r);
+ puthdr(r, f + 22, Etarp);
+ hnputs(r + 14, 1);
+ hnputs(r + 16, Etip4);
+ r[18] = Eaddrlen;
+ r[19] = 4;
+ hnputs(r + 20, Arpreply);
+ memmove(r + 22, ourmac, Eaddrlen);
+ memmove(r + 28, ouraddr + IPv4off, 4);
+ memmove(r + 32, f + 22, Eaddrlen);
+ memmove(r + 38, f + 28, 4);
+ write(efd, r, sizeof r);
+}
+
+int
+etherisarp(uchar *f, int n)
+{
+ if(n < Ehdrlen)
+ return 0;
+ if(nhgets(f + 12) != Etarp)
+ return 0;
+ arpin(f, n);
+ return 1;
+}
+
+int
+etherisip(uchar *f, int n)
+{
+ int t;
+
+ if(n < Ehdrlen)
+ return 0;
+ t = nhgets(f + 12);
+ return t == Etip4 || t == Etip6;
+}
+
+/*
+ * Send an IP packet out the card. The next hop is the destination if
+ * it is on our own network, otherwise the gateway. If we do not know
+ * its ethernet address yet we ask and drop this one; the sender will
+ * try again, which is what every other stack does too.
+ */
+int
+etherwriteip(uchar *p, int n, uchar *mask)
+{
+ uchar f[Ehdrlen + 64*1024], dst[IPaddrlen], hop[IPaddrlen];
+ uchar net[IPaddrlen], ournet[IPaddrlen], mac[Eaddrlen];
+ int type, len;
+
+ if(n < 20 || n > 64*1024 - Ehdrlen)
+ return -1;
+ switch(p[0] >> 4){
+ case 4:
+ type = Etip4;
+ v4tov6(dst, p + 16);
+ break;
+ case 6:
+ type = Etip6;
+ ipmove(dst, p + 24);
+ break;
+ default:
+ return -1;
+ }
+
+ if(groupmac(dst, mask, mac))
+ goto Send;
+
+ maskip(dst, mask, net);
+ maskip(ouraddr, mask, ournet);
+ if(ipcmp(net, ournet) == 0)
+ ipmove(hop, dst);
+ else if(haveg)
+ ipmove(hop, gateway);
+ else
+ ipmove(hop, dst);
+
+ if(!arpget(hop, mac)){
+ if(etherdebug)
+ fprint(2, "arp: no entry for %I, dropping and asking\n", hop);
+ arpask(hop);
+ return 0;
+ }
+Send:
+ puthdr(f, mac, type);
+ memmove(f + Ehdrlen, p, n);
+ len = Ehdrlen + n;
+ if(len < Eminlen){
+ memset(f + len, 0, Eminlen - len);
+ len = Eminlen;
+ }
+ if((len = write(efd, f, len)) < 0)
+ fprint(2, "ether: write failed: %r\n");
+ return len;
+}
diff --git a/fw/src/fw.c b/fw/src/fw.c
new file mode 100644
index 0000000..4f94090
--- /dev/null
+++ b/fw/src/fw.c
@@ -0,0 +1,1391 @@
+/*
+ * fw - a firewall between two networks.
+ *
+ * Each side is an interface bound to the pkt medium, whose data file is
+ * a raw IP wire: reading gives packets that stack wants to transmit,
+ * writing injects packets as if they had arrived. fw sits between
+ * two of them and copies packets across, or doesn't.
+ *
+ * LAN --ether-- stack --pkt-- fw --pkt-- stack --ether-- WAN
+ *
+ * Because nothing crosses without passing through here, a dropped
+ * packet is dropped before the far stack allocates anything or answers
+ * anything: this is filtering at the gate, in both directions, which is
+ * what the request-filtering half cannot do.
+ *
+ * pktmedium is unbindonclose, so the ctl fd of each interface has to be
+ * held for as long as the interface should exist. That is why the
+ * interfaces are created here rather than by a script.
+ */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ndb.h>
+#include <ip.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+#include "rules.h"
+
+typedef struct Wire Wire;
+struct Wire
+{
+ char *net; /* mountpoint of this side's stack */
+ char *addr;
+ char *mask;
+ char *side; /* "outside" or "inside", for messages */
+ int ifc;
+ int cfd; /* held open: unbindonclose */
+ int dfd; /* the raw IP wire */
+};
+
+typedef struct Pkt Pkt;
+struct Pkt
+{
+ int ok;
+ int proto;
+ uchar src[IPaddrlen];
+ uchar dst[IPaddrlen];
+ int sport;
+ int dport;
+ int verb;
+};
+
+static int debug;
+static int stateless;
+static int nallow, ndeny;
+
+/*
+ * Connection tracking.
+ *
+ * Without this a rule set is unwritable: permitting a connection out
+ * would mean separately permitting every reply back in, which in
+ * practice means opening every ephemeral port and calling it strict.
+ * So a packet the rules permit creates a flow, and anything belonging
+ * to that flow - in either direction - passes without consulting the
+ * rules again. This is what "keep state" means in pf and what every
+ * sysadmin already assumes is happening.
+ *
+ * ICMP has no ports, so its flows are keyed on the addresses alone;
+ * that is enough for ping to work. ICMP errors *about* a flow are not
+ * yet recognised as related to it.
+ */
+typedef struct Flow Flow;
+struct Flow
+{
+ int proto;
+ uchar src[IPaddrlen];
+ uchar dst[IPaddrlen];
+ int sport;
+ int dport;
+ int verb; /* direction that created it */
+ long last;
+ Flow *next;
+};
+
+enum { Nflow = 257, Maxpkt = 16*1024 };
+
+static Flow *flowtab[Nflow];
+static Lock flowlock;
+static int sweep;
+
+static int
+flowtimeout(int proto)
+{
+ switch(proto){
+ case 6: /* tcp */
+ return 300;
+ case 17: /* udp */
+ return 60;
+ default:
+ return 30;
+ }
+}
+
+static uint
+flowhash(int proto, uchar *src, uchar *dst, int sport, int dport)
+{
+ uint h;
+ int i;
+
+ h = proto*31 + sport*7 + dport;
+ for(i = 0; i < IPaddrlen; i++)
+ h = h*33 + src[i]*3 + dst[i];
+ return h % Nflow;
+}
+
+static int
+flowis(Flow *f, int proto, uchar *src, uchar *dst, int sport, int dport)
+{
+ return f->proto == proto && f->sport == sport && f->dport == dport
+ && ipcmp(f->src, src) == 0 && ipcmp(f->dst, dst) == 0;
+}
+
+static int
+flowlook(int proto, uchar *src, uchar *dst, int sport, int dport, long now)
+{
+ Flow *f;
+ uint h;
+
+ h = flowhash(proto, src, dst, sport, dport);
+ for(f = flowtab[h]; f != nil; f = f->next)
+ if(flowis(f, proto, src, dst, sport, dport)){
+ f->last = now;
+ return 1;
+ }
+ return 0;
+}
+
+/* does this packet belong to a flow we already permitted, either way round? */
+static int
+flowseen(Pkt *p)
+{
+ long now;
+ int r;
+
+ if(stateless)
+ return 0;
+ now = time(0);
+ lock(&flowlock);
+ r = flowlook(p->proto, p->src, p->dst, p->sport, p->dport, now)
+ || flowlook(p->proto, p->dst, p->src, p->dport, p->sport, now);
+ unlock(&flowlock);
+ return r;
+}
+
+static void
+reapbucket(int i, long now)
+{
+ Flow *f, **pp;
+
+ for(pp = &flowtab[i]; (f = *pp) != nil; ){
+ if(now - f->last > flowtimeout(f->proto)){
+ *pp = f->next;
+ free(f);
+ }else
+ pp = &f->next;
+ }
+}
+
+/*
+ * Re-check every live flow against the current rules and drop the ones
+ * the rules no longer permit. Called whenever the rule set changes.
+ *
+ * pf and iptables do not do this: there, blocking a port stops new
+ * connections and leaves established ones running until they time out.
+ * That is a wart everybody has to learn. A block should block, so a
+ * rule change kills the traffic it now forbids.
+ */
+int
+revalidate(void)
+{
+ Flow *f, **pp;
+ uchar *peer;
+ int i, port, lport, n;
+
+ n = 0;
+ lock(&flowlock);
+ for(i = 0; i < Nflow; i++)
+ for(pp = &flowtab[i]; (f = *pp) != nil; ){
+ if(f->verb == Vout){
+ peer = f->dst;
+ port = f->dport;
+ lport = f->sport;
+ }else{
+ peer = f->src;
+ port = f->sport;
+ lport = f->dport;
+ }
+ if(matchrule(f->verb, protonum2name(f->proto), peer, 0, port, lport, nil) != nil){
+ *pp = f->next;
+ free(f);
+ n++;
+ }else
+ pp = &f->next;
+ }
+ unlock(&flowlock);
+ return n;
+}
+
+static void
+flowadd(Pkt *p)
+{
+ Flow *f;
+ uint h;
+ long now;
+
+ if(stateless)
+ return;
+ now = time(0);
+ h = flowhash(p->proto, p->src, p->dst, p->sport, p->dport);
+ lock(&flowlock);
+ reapbucket(h, now);
+ /* and sweep one other bucket, so idle flows do not accumulate */
+ sweep = (sweep + 1) % Nflow;
+ reapbucket(sweep, now);
+
+ f = emalloc(sizeof *f);
+ f->proto = p->proto;
+ ipmove(f->src, p->src);
+ ipmove(f->dst, p->dst);
+ f->sport = p->sport;
+ f->dport = p->dport;
+ f->verb = p->verb;
+ f->last = now;
+ f->next = flowtab[h];
+ flowtab[h] = f;
+ unlock(&flowlock);
+}
+
+void servenet(char*, char*, char*);
+int etheropen(char*, uchar*);
+void ethersetaddr(uchar*, uchar*, int);
+int etherisarp(uchar*, int);
+int etherisip(uchar*, int);
+int etherwriteip(uchar*, int, uchar*);
+
+enum { Ehdrlen = 14 };
+
+static int efd = -1;
+int etherdebug;
+static uchar ethermask[IPaddrlen];
+
+static void
+usage(void)
+{
+ fprint(2, "usage: fw [-dSW] [-m ctl] [-s srv] [-n net]\n");
+ fprint(2, " [-e ether -a addr/mask [-g gateway]]\n");
+ fprint(2, " rules.ndb [outside inside]\n");
+ fprint(2, " -e: filter packets on a card, for this machine\n");
+ fprint(2, " two sides: filter packets between them (a gateway)\n");
+ fprint(2, " neither: filter requests on /net (one namespace)\n");
+ fprint(2, " a side is net!addr!mask, eg /net.alt!10.9.9.1!255.255.255.0\n");
+ exits("usage");
+}
+
+static Wire*
+parseside(char *spec, char *side)
+{
+ char *f[4];
+ Wire *w;
+
+ w = emalloc(sizeof *w);
+ w->side = side;
+ if(getfields(estrdup(spec), f, nelem(f), 0, "!") != 3)
+ sysfatal("%s: want net!addr!mask", spec);
+ w->net = f[0];
+ w->addr = f[1];
+ w->mask = f[2];
+ return w;
+}
+
+static void
+wireup(Wire *w)
+{
+ char path[128], buf[64];
+ int n;
+
+ snprint(path, sizeof path, "%s/ipifc/clone", w->net);
+ if((w->cfd = open(path, ORDWR)) < 0)
+ sysfatal("open %s: %r", path);
+ if((n = read(w->cfd, buf, sizeof buf - 1)) <= 0)
+ sysfatal("read %s: %r", path);
+ buf[n] = '\0';
+ w->ifc = atoi(buf);
+
+ if(fprint(w->cfd, "bind pkt") < 0)
+ sysfatal("%s: bind pkt: %r", w->net);
+ if(fprint(w->cfd, "add %s %s", w->addr, w->mask) < 0)
+ sysfatal("%s: add %s %s: %r", w->net, w->addr, w->mask);
+
+ snprint(path, sizeof path, "%s/ipifc/%d/data", w->net, w->ifc);
+ if((w->dfd = open(path, ORDWR)) < 0)
+ sysfatal("open %s: %r", path);
+
+ fprint(2, "fw: %s %s/ipifc/%d addr %s %s\n",
+ w->side, w->net, w->ifc, w->addr, w->mask);
+}
+
+/*
+ * Enough of an IP header to make a decision. IPv6 extension headers
+ * are not walked: a packet carrying them is reported as its next-header
+ * protocol and matched on addresses only, which is conservative given
+ * default deny but is a real gap worth closing.
+ */
+static void
+parsepkt(uchar *b, int n, Pkt *p)
+{
+ uchar *t;
+ int hl;
+
+ memset(p, 0, sizeof *p);
+ p->sport = p->dport = -1;
+ if(n < 1)
+ return;
+
+ switch(b[0] >> 4){
+ case 4:
+ if(n < 20)
+ return;
+ hl = (b[0] & 0xF) * 4;
+ if(hl < 20 || n < hl)
+ return;
+ p->proto = b[9];
+ v4tov6(p->src, b + 12);
+ v4tov6(p->dst, b + 16);
+ t = b + hl;
+ n -= hl;
+ break;
+ case 6:
+ if(n < 40)
+ return;
+ p->proto = b[6];
+ ipmove(p->src, b + 8);
+ ipmove(p->dst, b + 24);
+ t = b + 40;
+ n -= 40;
+ break;
+ default:
+ return;
+ }
+
+ if((p->proto == 6 || p->proto == 17) && n >= 4){
+ p->sport = nhgets(t);
+ p->dport = nhgets(t + 2);
+ }
+ p->ok = 1;
+}
+
+/*
+ * One direction. "verb" says which way this is, and the rule's ip and
+ * port always refer to the peer - the far end - so a rule reads the
+ * same whichever direction it governs.
+ */
+int revalidate(void);
+
+static void
+relay(Wire *from, Wire *to, int verb)
+{
+ uchar *buf;
+ char *e;
+ uchar *peer;
+ Rule *rule;
+ Pkt p;
+ int n, port, lport;
+
+ /*
+ * On the heap, not the stack: these run as libthread procs with a
+ * small stack, and pktmedium's maxtu is 4k anyway.
+ */
+ buf = emalloc(Maxpkt);
+ for(;;){
+ if((n = read(from->dfd, buf, Maxpkt)) <= 0){
+ fprint(2, "fw: %s: read failed: %r\n", from->side);
+ syslog(0, "fw", "%s: read failed: %r", from->side);
+ return;
+ }
+
+ parsepkt(buf, n, &p);
+ if(!p.ok){
+ ndeny++;
+ if(debug)
+ fprint(2, "drop %s: unparseable, %d bytes\n", from->side, n);
+ continue;
+ }
+
+ p.verb = verb;
+ if(verb == Vout){
+ peer = p.dst;
+ port = p.dport;
+ lport = p.sport;
+ }else{
+ peer = p.src;
+ port = p.sport;
+ lport = p.dport;
+ }
+
+ if(flowseen(&p)){
+ nallow++;
+ if(debug)
+ fprint(2, "pass %s %s %I!%d -> %I!%d (state)\n",
+ verb == Vout ? "out" : "in",
+ protonum2name(p.proto),
+ p.src, p.sport, p.dst, p.dport);
+ if(write(to->dfd, buf, n) != n)
+ fprint(2, "fw: write %s wire: %r\n", to->side);
+ continue;
+ }
+
+ e = matchrule(verb, protonum2name(p.proto), peer, 0, port, lport, &rule);
+ if(e != nil){
+ if(rule != nil && rule->log)
+ syslog(0, "fw", "drop %s %s %I!%d -> %I!%d: %s",
+ verb == Vout ? "out" : "in",
+ protonum2name(p.proto),
+ p.src, p.sport, p.dst, p.dport, e);
+ ndeny++;
+ if(debug)
+ fprint(2, "drop %s %s %I!%d -> %I!%d: %s\n",
+ verb == Vout ? "out" : "in",
+ protonum2name(p.proto),
+ p.src, p.sport, p.dst, p.dport, e);
+ continue;
+ }
+
+ nallow++;
+ if(rule != nil && rule->log)
+ syslog(0, "fw", "pass %s %s %I!%d -> %I!%d",
+ verb == Vout ? "out" : "in",
+ protonum2name(p.proto),
+ p.src, p.sport, p.dst, p.dport);
+ flowadd(&p);
+ if(debug)
+ fprint(2, "pass %s %s %I!%d -> %I!%d (new)\n",
+ verb == Vout ? "out" : "in",
+ protonum2name(p.proto),
+ p.src, p.sport, p.dst, p.dport);
+ if(write(to->dfd, buf, n) != n)
+ fprint(2, "fw: write %s wire: %r\n", to->side);
+ }
+}
+
+enum
+{
+ Qctl,
+ Qrules,
+ Qflows,
+ Qstats,
+};
+
+static char *rulepath; /* the file we were started with */
+
+static char *ctltext =
+ "prepend <rule> insert a rule at the top, where it wins\n"
+ "append <rule> add a rule at the bottom\n"
+ "delete <n> remove rule n\n"
+ "flush remove every rule; then nothing is permitted\n"
+ "reload [file] re-read the rule file; no argument means the\n"
+ " one it was started with\n"
+ "save [file] write the current rules back out\n"
+ "flushflows forget tracked connections\n"
+ "\n"
+ "a rule is ndb, eg: deny=out proto=tcp port=80\n"
+ "changes take effect at once, and live connections that the\n"
+ "new rules forbid are dropped rather than left running.\n";
+
+enum { Rulebuf = 64*1024 };
+
+/*
+ * ctl edits are done by writing the rule set back out as ndb, editing
+ * the text, and parsing the whole thing again. It is not the quickest
+ * way, but rule changes are rare, and it means a rule typed at ctl and
+ * a rule in the file go through exactly one parser - they cannot come
+ * to disagree about what a rule means.
+ */
+static Rule*
+rulesfromtext(char *text, char **err)
+{
+ char tmp[64];
+ Rule *r;
+ long n;
+ int fd;
+
+ snprint(tmp, sizeof tmp, "/tmp/fw.%d", getpid());
+ if((fd = create(tmp, OWRITE, 0600)) < 0){
+ *err = "cannot create a temporary file";
+ return nil;
+ }
+ n = strlen(text);
+ if(write(fd, text, n) != n){
+ close(fd);
+ remove(tmp);
+ *err = "cannot write a temporary file";
+ return nil;
+ }
+ close(fd);
+ r = parserules(tmp, err);
+ remove(tmp);
+ return r;
+}
+
+static char*
+replacerules(char *text)
+{
+ Rule *new;
+ char *err;
+
+ new = rulesfromtext(text, &err);
+ if(err != nil)
+ return err;
+ installrules(new);
+ return nil;
+}
+
+static char*
+editrules(char *add, int atfront, int delete)
+{
+ char *cur, *all, *err, *p, *nl;
+ int i;
+
+ if((cur = mallocz(Rulebuf, 1)) == nil)
+ return "out of memory";
+ cur[fmtrules(cur, Rulebuf-1)] = '\0';
+
+ if(delete > 0){
+ /* fmtrules writes one line per rule, so line n is rule n */
+ p = cur;
+ for(i = 1; i < delete && p != nil; i++)
+ if((p = strchr(p, '\n')) != nil)
+ p++;
+ if(p == nil || *p == '\0'){
+ free(cur);
+ return "no such rule";
+ }
+ if((nl = strchr(p, '\n')) != nil)
+ memmove(p, nl+1, strlen(nl+1)+1);
+ else
+ *p = '\0';
+ all = strdup(cur);
+ }else if(atfront)
+ all = smprint("%s\n%s", add, cur);
+ else
+ all = smprint("%s%s\n", cur, add);
+ free(cur);
+ if(all == nil)
+ return "out of memory";
+
+ err = replacerules(all);
+ free(all);
+ return err;
+}
+
+static char*
+reloadrules(char *file)
+{
+ Rule *new;
+ char *err;
+
+ if(file == nil)
+ return "no rule file to reload";
+ if((new = parserules(file, &err)) == nil && err != nil)
+ return err;
+ installrules(new);
+ return nil;
+}
+
+/* the other direction: keep what was typed at ctl across a restart */
+static char*
+saverules(char *file)
+{
+ char *buf;
+ long n;
+ int fd;
+
+ if(file == nil)
+ return "no rule file to save to";
+ if((buf = mallocz(Rulebuf, 1)) == nil)
+ return "out of memory";
+ n = fmtrules(buf, Rulebuf-1);
+ if((fd = create(file, OWRITE, 0644)) < 0){
+ free(buf);
+ return "cannot create the rule file";
+ }
+ if(write(fd, buf, n) != n){
+ close(fd);
+ free(buf);
+ return "cannot write the rule file";
+ }
+ close(fd);
+ free(buf);
+ return nil;
+}
+
+static char*
+flushflows(void)
+{
+ Flow *f, *next;
+ int i;
+
+ lock(&flowlock);
+ for(i = 0; i < Nflow; i++){
+ for(f = flowtab[i]; f != nil; f = next){
+ next = f->next;
+ free(f);
+ }
+ flowtab[i] = nil;
+ }
+ unlock(&flowlock);
+ return nil;
+}
+
+static char*
+flowtext(void)
+{
+ char *buf, *p, *e;
+ Flow *f;
+ long now;
+ int i;
+
+ if((buf = mallocz(Rulebuf, 1)) == nil)
+ return nil;
+ p = buf;
+ e = buf + Rulebuf;
+ now = time(0);
+ lock(&flowlock);
+ for(i = 0; i < Nflow; i++)
+ for(f = flowtab[i]; f != nil; f = f->next)
+ p = seprint(p, e, "%s %s %I!%d -> %I!%d idle %ld\n",
+ f->verb == Vout ? "out" : "in",
+ protonum2name(f->proto),
+ f->src, f->sport, f->dst, f->dport,
+ now - f->last);
+ unlock(&flowlock);
+ USED(p);
+ return buf;
+}
+
+/*
+ * A rule set written to "rules" may not arrive in one 9P message, and
+ * treating each write as a complete set would install fragments. So
+ * writes accumulate on the fid and the set is replaced when the fid is
+ * clunked, which is the transaction boundary the protocol already has.
+ *
+ * A clunk cannot fail, so a set that will not parse is reported to the
+ * log and the running rules are kept. Use ctl, whose writes do report
+ * errors, when you want to be told.
+ */
+typedef struct Wbuf Wbuf;
+struct Wbuf
+{
+ char *b;
+ long n;
+ long max;
+};
+
+static void
+wbufput(Wbuf *w, char *p, long n, vlong off)
+{
+ long need;
+
+ need = off + n;
+ if(need + 1 > w->max){
+ w->max = need + 8192;
+ if((w->b = realloc(w->b, w->max)) == nil)
+ sysfatal("out of memory");
+ }
+ if(off > w->n)
+ memset(w->b + w->n, 0, off - w->n);
+ memmove(w->b + off, p, n);
+ if(need > w->n)
+ w->n = need;
+}
+
+static void
+fsdestroyfid(Fid *fid)
+{
+ Wbuf *w;
+ char *err;
+
+ if((w = fid->aux) == nil)
+ return;
+ fid->aux = nil;
+ if(w->n > 0){
+ w->b[w->n] = '\0';
+ if((err = replacerules(w->b)) != nil)
+ syslog(0, "fw", "rules rejected, keeping the old ones: %s", err);
+ else
+ syslog(0, "fw", "rule set replaced");
+ }
+ free(w->b);
+ free(w);
+}
+
+static void
+fsread(Req *r)
+{
+ char buf[256], *s;
+ long n;
+
+ switch((int)(uintptr)r->fid->file->aux){
+ case Qctl:
+ readstr(r, ctltext);
+ break;
+ case Qrules:
+ if((s = mallocz(Rulebuf, 1)) == nil){
+ respond(r, "out of memory");
+ return;
+ }
+ n = fmtrules(s, Rulebuf-1);
+ s[n] = '\0';
+ readstr(r, s);
+ free(s);
+ break;
+ case Qflows:
+ if((s = flowtext()) == nil){
+ respond(r, "out of memory");
+ return;
+ }
+ readstr(r, s);
+ free(s);
+ break;
+ case Qstats:
+ if((s = mallocz(Rulebuf, 1)) == nil){
+ respond(r, "out of memory");
+ return;
+ }
+ n = snprint(s, Rulebuf-1, "passed %d\ndropped %d\n\n", nallow, ndeny);
+ fmthits(s+n, Rulebuf-1-n);
+ readstr(r, s);
+ free(s);
+ break;
+ default:
+ respond(r, "not a readable file");
+ return;
+ }
+ respond(r, nil);
+}
+
+static void
+fswrite(Req *r)
+{
+ char *buf, *err, *arg, *p;
+ int type;
+ long n;
+
+ type = (int)(uintptr)r->fid->file->aux;
+ if(type != Qctl && type != Qrules){
+ respond(r, "not a writable file");
+ return;
+ }
+ if((buf = mallocz(r->ifcall.count+1, 1)) == nil){
+ respond(r, "out of memory");
+ return;
+ }
+ memmove(buf, r->ifcall.data, r->ifcall.count);
+ buf[r->ifcall.count] = '\0';
+
+ if(type == Qrules){
+ Wbuf *w;
+
+ if((w = r->fid->aux) == nil){
+ w = emalloc(sizeof *w);
+ r->fid->aux = w;
+ }
+ wbufput(w, r->ifcall.data, r->ifcall.count, r->ifcall.offset);
+ err = nil;
+ goto Done;
+ }
+
+ /*
+ * A write may or may not carry a trailing newline, and the
+ * argument of prepend is a whole ndb rule with spaces and tabs
+ * in it, so split off the first word by hand rather than
+ * tokenizing the lot.
+ */
+ for(n = strlen(buf); n > 0; n--){
+ if(buf[n-1] != '\n' && buf[n-1] != '\r'
+ && buf[n-1] != ' ' && buf[n-1] != '\t')
+ break;
+ buf[n-1] = '\0';
+ }
+ if(*buf == '\0'){
+ err = "no command";
+ goto Done;
+ }
+ arg = nil;
+ for(p = buf; *p != '\0' && *p != ' ' && *p != '\t'; p++)
+ ;
+ if(*p != '\0'){
+ *p++ = '\0';
+ while(*p == ' ' || *p == '\t')
+ p++;
+ if(*p != '\0')
+ arg = p;
+ }
+
+ if(strcmp(buf, "flush") == 0)
+ err = replacerules("");
+ else if(strcmp(buf, "flushflows") == 0)
+ err = flushflows();
+ else if(strcmp(buf, "reload") == 0)
+ err = reloadrules(arg != nil ? arg : rulepath);
+ else if(strcmp(buf, "save") == 0)
+ err = saverules(arg != nil ? arg : rulepath);
+ else if(arg == nil)
+ err = "usage: prepend, append or delete <rule>, "
+ "flush, reload, save or flushflows";
+ else if(strcmp(buf, "prepend") == 0)
+ err = editrules(arg, 1, 0);
+ else if(strcmp(buf, "append") == 0)
+ err = editrules(arg, 0, 0);
+ else if(strcmp(buf, "delete") == 0)
+ err = editrules(nil, 0, atoi(arg));
+ else
+ err = "unknown command; read ctl for the list";
+
+Done:
+ free(buf);
+ if(err != nil){
+ respond(r, err);
+ return;
+ }
+ r->ofcall.count = r->ifcall.count;
+ respond(r, nil);
+}
+
+static Srv fs =
+{
+.read= fsread,
+.write= fswrite,
+.destroyfid= fsdestroyfid,
+};
+
+/* a rule change must not leave traffic running that the rules now forbid */
+static void
+rulesdidchange(void)
+{
+ int n;
+
+ if((n = revalidate()) > 0)
+ syslog(0, "fw", "rules changed; dropped %d flow%s they forbid",
+ n, n == 1 ? "" : "s");
+}
+
+static void
+servectl(char *mtpt, char *srvname)
+{
+ File *root;
+
+ fs.tree = alloctree("fw", "fw", DMDIR|0555, nil);
+ root = fs.tree->root;
+ closefile(createfile(root, "ctl", "fw", 0666, (void*)Qctl));
+ closefile(createfile(root, "rules", "fw", 0666, (void*)Qrules));
+ closefile(createfile(root, "flows", "fw", 0444, (void*)Qflows));
+ closefile(createfile(root, "stats", "fw", 0444, (void*)Qstats));
+ threadpostmountsrv(&fs, srvname, mtpt, MREPL);
+}
+
+static void
+relayproc(void *a)
+{
+ Wire **w;
+
+ w = a;
+ relay(w[0], w[1], (int)(uintptr)w[2]);
+}
+
+/*
+ * Decide on one packet. Shared by every relay: the wire it came from
+ * only changes which way "in" and "out" mean.
+ */
+static int
+permitted(uchar *buf, int n, int verb, Pkt *p)
+{
+ Rule *rule;
+ uchar *peer;
+ char *e;
+ int port, lport;
+
+ parsepkt(buf, n, p);
+ if(!p->ok){
+ ndeny++;
+ if(debug)
+ fprint(2, "drop %s: unparseable, %d bytes\n",
+ verb == Vout ? "out" : "in", n);
+ return 0;
+ }
+ p->verb = verb;
+ if(verb == Vout){
+ peer = p->dst;
+ port = p->dport;
+ lport = p->sport;
+ }else{
+ peer = p->src;
+ port = p->sport;
+ lport = p->dport;
+ }
+ if(flowseen(p)){
+ nallow++;
+ return 1;
+ }
+ e = matchrule(verb, protonum2name(p->proto), peer, 0, port, lport, &rule);
+ if(e != nil){
+ ndeny++;
+ if(rule != nil && rule->log)
+ syslog(0, "fw", "drop %s %s %I!%d -> %I!%d: %s",
+ verb == Vout ? "out" : "in", protonum2name(p->proto),
+ p->src, p->sport, p->dst, p->dport, e);
+ if(debug)
+ fprint(2, "drop %s %s %I!%d -> %I!%d: %s\n",
+ verb == Vout ? "out" : "in", protonum2name(p->proto),
+ p->src, p->sport, p->dst, p->dport, e);
+ return 0;
+ }
+ nallow++;
+ if(rule != nil && rule->log)
+ syslog(0, "fw", "pass %s %s %I!%d -> %I!%d",
+ verb == Vout ? "out" : "in", protonum2name(p->proto),
+ p->src, p->sport, p->dst, p->dport);
+ if(debug)
+ fprint(2, "pass %s %s %I!%d -> %I!%d\n",
+ verb == Vout ? "out" : "in", protonum2name(p->proto),
+ p->src, p->sport, p->dst, p->dport);
+ flowadd(p);
+ return 1;
+}
+
+/* the wire -> the protected stack */
+static void
+etherin(void *a)
+{
+ uchar *buf;
+ Wire *w;
+ Pkt p;
+ int n;
+
+ w = a;
+ buf = emalloc(Maxpkt);
+ for(;;){
+ if((n = read(efd, buf, Maxpkt)) <= 0){
+ fprint(2, "fw: %s: read the card: %r\n", w->side);
+ syslog(0, "fw", "stopped reading the card: %r");
+ return;
+ }
+ if(debug > 1)
+ fprint(2, "wire: %d bytes type %.4ux\n", n, (buf[12]<<8)|buf[13]);
+ if(etherisarp(buf, n))
+ continue;
+ if(!etherisip(buf, n))
+ continue;
+ if(permitted(buf+Ehdrlen, n-Ehdrlen, Vin, &p))
+ write(w->dfd, buf+Ehdrlen, n-Ehdrlen);
+ }
+}
+
+/* the protected stack -> the wire */
+static void
+etherout(void *a)
+{
+ uchar *buf;
+ Wire *w;
+ Pkt p;
+ int n;
+
+ w = a;
+ buf = emalloc(Maxpkt);
+ for(;;){
+ if((n = read(w->dfd, buf, Maxpkt)) <= 0){
+ fprint(2, "fw: %s: read the stack: %r\n", w->side);
+ syslog(0, "fw", "stopped reading the stack: %r");
+ return;
+ }
+ if(permitted(buf, n, Vout, &p))
+ etherwriteip(buf, n, ethermask);
+ }
+}
+
+/*
+ * The address, mask and gateway the stack is already using. Asking the
+ * administrator to repeat what ipconfig(8) was told is a way of getting
+ * the two out of step, so read them instead: the interface status names
+ * the address and mask, and the routing table names the gateway.
+ */
+static int
+learnaddr(char *net, char *dev, char *addr, int naddr, char *gw, int ngw)
+{
+ char path[128], buf[1024], *lines[8], *f[8], *p;
+ int i, fd, n, nl, nf, found;
+
+ found = 0;
+ for(i = 0; i < 16 && !found; i++){
+ snprint(path, sizeof path, "%s/ipifc/%d/status", net, i);
+ if((fd = open(path, OREAD)) < 0)
+ continue;
+ n = read(fd, buf, sizeof buf - 1);
+ close(fd);
+ if(n <= 0)
+ continue;
+ buf[n] = '\0';
+ if((p = strstr(buf, dev)) == nil)
+ continue;
+ USED(p);
+ nl = getfields(buf, lines, nelem(lines), 0, "\n");
+ if(nl < 2)
+ continue;
+ /* the address line: address, then the mask as a prefix */
+ nf = tokenize(lines[1], f, nelem(f));
+ if(nf < 2)
+ continue;
+ snprint(addr, naddr, "%s%s", f[0], f[1]);
+ found = 1;
+ }
+ if(!found)
+ return -1;
+
+ *gw = '\0';
+ snprint(path, sizeof path, "%s/iproute", net);
+ if((fd = open(path, OREAD)) >= 0){
+ n = read(fd, buf, sizeof buf - 1);
+ close(fd);
+ if(n > 0){
+ buf[n] = '\0';
+ nl = getfields(buf, lines, nelem(lines), 0, "\n");
+ for(i = 0; i < nl; i++){
+ nf = tokenize(lines[i], f, nelem(f));
+ if(nf >= 3 && strcmp(f[0], "0.0.0.0") == 0
+ && strcmp(f[1], "/96") == 0){
+ snprint(gw, ngw, "%s", f[2]);
+ break;
+ }
+ }
+ }
+ }
+ return 0;
+}
+
+/*
+ * Undo a previous fw that died.
+ *
+ * An IP stack outlives the program that configured it, so a fw that is
+ * killed leaves its pkt interface behind, holding the address, with
+ * nothing on the other end of it. The machine has no network until
+ * someone unpicks that by hand, and the next fw to start makes a second
+ * interface with the same address and routes that could go to either.
+ *
+ * So before taking anything, throw away any pkt interface already
+ * carrying the address we are about to use. Nothing else can have made
+ * it: a live fw would still be holding the card we are about to take.
+ */
+/*
+ * Put the card back.
+ *
+ * Taking a card is destructive: the stack loses it, and the pkt
+ * interface that replaced it is unbindonclose, so when fw stops the
+ * address goes with it and the machine is left with a card bound to
+ * nothing. Restore it on the way out, so that everything short of an
+ * uncatchable kill leaves the machine as we found it.
+ */
+static char *backdev, *backaddr, *backmask, *backnet, *backgw;
+
+static void
+putback(void)
+{
+ char path[128], buf[64];
+ int cfd, n, ifc;
+
+ if(backdev == nil)
+ return;
+ snprint(path, sizeof path, "%s/ipifc/clone", backnet);
+ if((cfd = open(path, ORDWR)) < 0)
+ return;
+ if((n = read(cfd, buf, sizeof buf - 1)) <= 0){
+ close(cfd);
+ return;
+ }
+ buf[n] = '\0';
+ ifc = atoi(buf);
+ if(fprint(cfd, "bind ether %s", backdev) > 0
+ && fprint(cfd, "add %s %s", backaddr, backmask) > 0){
+ if(backgw != nil){
+ snprint(path, sizeof path, "%s/iproute", backnet);
+ if((n = open(path, OWRITE)) >= 0){
+ fprint(n, "add 0.0.0.0 0.0.0.0 %s", backgw);
+ close(n);
+ }
+ }
+ fprint(2, "fw: put %s back on %s/ipifc/%d\n", backdev, backnet, ifc);
+ }
+ /* the ctl fd must stay open for the binding to last */
+ backdev = nil;
+}
+
+/* clean up, then let the note do what it was going to do */
+static int
+notehandler(void*, char*)
+{
+ putback();
+ return 0;
+}
+
+static void
+reclaim(char *net, char *addr)
+{
+ char path[128], buf[1024], *lines[8], *f[8];
+ int i, fd, n, nl, nf;
+
+ for(i = 0; i < 16; i++){
+ snprint(path, sizeof path, "%s/ipifc/%d/status", net, i);
+ if((fd = open(path, OREAD)) < 0)
+ continue;
+ n = read(fd, buf, sizeof buf - 1);
+ close(fd);
+ if(n <= 0)
+ continue;
+ buf[n] = '\0';
+ nl = getfields(buf, lines, nelem(lines), 0, "\n");
+ if(nl < 2)
+ continue;
+ nf = tokenize(lines[0], f, nelem(f));
+ if(nf < 2 || strncmp(f[1], "pkt", 3) != 0)
+ continue;
+ nf = tokenize(lines[1], f, nelem(f));
+ if(nf < 1 || strcmp(f[0], addr) != 0)
+ continue;
+
+ snprint(path, sizeof path, "%s/ipifc/%d/ctl", net, i);
+ if((fd = open(path, OWRITE)) < 0)
+ continue;
+ if(fprint(fd, "unbind") > 0){
+ fprint(2, "fw: cleared %s/ipifc/%d, left behind by an earlier fw\n",
+ net, i);
+ syslog(0, "fw", "cleared a stale interface holding %s", addr);
+ }
+ close(fd);
+ }
+}
+
+/*
+ * The card cannot stay attached to the stack we are protecting, or
+ * packets reach it whatever the rules say. Find whichever interface
+ * has it and take it away.
+ */
+static void
+takecard(char *net, char *dev)
+{
+ char path[128], buf[512], *p;
+ int i, fd, n, found;
+
+ found = 0;
+ for(i = 0; i < 16; i++){
+ snprint(path, sizeof path, "%s/ipifc/%d/status", net, i);
+ if((fd = open(path, OREAD)) < 0)
+ continue;
+ n = read(fd, buf, sizeof buf - 1);
+ close(fd);
+ if(n <= 0)
+ continue;
+ buf[n] = '\0';
+ if((p = strchr(buf, ' ')) == nil)
+ continue;
+ *p = '\0';
+ if(strcmp(buf, "device") != 0 && strstr(buf, dev) == nil)
+ continue;
+ if(strstr(p+1, dev) == nil && strstr(buf, dev) == nil)
+ continue;
+ snprint(path, sizeof path, "%s/ipifc/%d/ctl", net, i);
+ if((fd = open(path, OWRITE)) < 0)
+ continue;
+ if(fprint(fd, "unbind") > 0){
+ fprint(2, "fw: took %s away from %s/ipifc/%d\n", dev, net, i);
+ found = 1;
+ }
+ close(fd);
+ }
+ if(!found)
+ fprint(2, "fw: warning: %s was not bound to %s; "
+ "is something else still using it?\n", dev, net);
+}
+
+void
+threadmain(int argc, char **argv)
+{
+ static Wire *outargs[3], *inargs[3];
+ Wire *out, *in, *prot;
+ char *mtpt, *srvname, *netmtpt, *etherdev, *etheraddr, *ethergw;
+ uchar ip[IPaddrlen], gw[IPaddrlen];
+ int wireonly;
+
+ mtpt = "/mnt/fw";
+ netmtpt = "/net";
+ srvname = nil;
+ etherdev = nil;
+ etheraddr = nil;
+ ethergw = nil;
+ wireonly = 0;
+ ARGBEGIN{
+ case 'd':
+ debug++;
+ break;
+ case 'S':
+ stateless++;
+ break;
+ case 'm':
+ mtpt = EARGF(usage());
+ break;
+ case 's':
+ srvname = EARGF(usage());
+ break;
+ case 'n':
+ netmtpt = EARGF(usage());
+ break;
+ case 'W':
+ wireonly++;
+ break;
+ case 'e':
+ /*
+ * One card per fw. Taking two would mean two pkt
+ * interfaces, two ARP caches and one rule set that could
+ * not say which card it meant; run one fw per card until
+ * the rules can name one.
+ */
+ if(etherdev != nil)
+ sysfatal("one -e at a time; run one fw per card");
+ etherdev = EARGF(usage());
+ break;
+ case 'a':
+ etheraddr = EARGF(usage());
+ break;
+ case 'g':
+ ethergw = EARGF(usage());
+ break;
+ default:
+ usage();
+ }ARGEND
+
+ if(argc != 1 && argc != 3)
+ usage();
+
+ fmtinstall('I', eipfmt);
+ fmtinstall('V', eipfmt);
+ fmtinstall('E', eipfmt);
+
+ rulepath = argv[0];
+ readrules(rulepath);
+ checklogging();
+ if(debug)
+ dumprules();
+
+ /*
+ * Naming two sides means there are wires to sit between, so we
+ * filter packets. Naming none means there is one namespace to
+ * protect, so we filter the requests it makes. Same rules
+ * either way; the mode is what you pointed it at, and it is
+ * never chosen silently.
+ */
+ if(etherdev != nil){
+ char abuf[64], rpath[128], *m;
+ uchar mac[6];
+ int haveg, k;
+
+ if(etheraddr == nil){
+ static char abuf2[64], gbuf[64];
+
+ if(learnaddr(netmtpt, etherdev, abuf2, sizeof abuf2,
+ gbuf, sizeof gbuf) < 0)
+ sysfatal("%s has no address on %s; give -a addr/mask",
+ etherdev, netmtpt);
+ etheraddr = abuf2;
+ if(ethergw == nil && *gbuf != '\0')
+ ethergw = gbuf;
+ fprint(2, "fw: %s has %s%s%s\n", etherdev, etheraddr,
+ ethergw != nil ? ", gateway " : "",
+ ethergw != nil ? ethergw : "");
+ }
+ m = nil;
+ if((m = strchr(etheraddr, '/')) != nil){
+ k = m - etheraddr;
+ if(k >= sizeof abuf)
+ sysfatal("%s: address too long", etheraddr);
+ memmove(abuf, etheraddr, k);
+ abuf[k] = '\0';
+ }else
+ sysfatal("-a wants addr/mask, eg 10.0.2.15/24");
+ if(parseipandmask(ip, ethermask, abuf, m) == -1)
+ sysfatal("%s: unparseable address or mask", etheraddr);
+ haveg = 0;
+ if(ethergw != nil){
+ if(parseip(gw, ethergw) == -1)
+ sysfatal("%s: unparseable gateway", ethergw);
+ haveg = 1;
+ }
+
+ /*
+ * Check we can mount before touching the card. Taking it
+ * and then failing leaves the machine with an interface
+ * that has no card behind it and no network at all, which
+ * is a bad way to discover a typo in -m.
+ */
+ if(access(mtpt, AEXIST) < 0)
+ sysfatal("%s: %r; not touching %s until it exists",
+ mtpt, etherdev);
+
+ fprint(2, "fw: filtering packets on %s\n", etherdev);
+ syslog(0, "fw", "started, filtering %s for %s", etherdev, etheraddr);
+
+ etherdebug = debug;
+ efd = etheropen(etherdev, mac);
+ ethersetaddr(ip, gw, haveg);
+ reclaim(netmtpt, abuf);
+
+ backdev = etherdev;
+ backaddr = abuf;
+ backmask = m;
+ backnet = netmtpt;
+ backgw = ethergw;
+ atexit(putback);
+ threadnotify(notehandler, 1);
+
+ takecard(netmtpt, etherdev);
+
+ prot = emalloc(sizeof *prot);
+ prot->net = netmtpt;
+ prot->addr = abuf;
+ prot->mask = m;
+ prot->side = "protected";
+ wireup(prot);
+
+ /*
+ * Taking the card away took the routes with it. The stack
+ * still needs to know how to leave its own network, and we
+ * are the only thing that knows where the gateway is.
+ */
+ if(haveg){
+ snprint(rpath, sizeof rpath, "%s/iproute", netmtpt);
+ if((k = open(rpath, OWRITE)) < 0)
+ fprint(2, "fw: cannot open %s: %r\n", rpath);
+ else{
+ if(fprint(k, "add 0.0.0.0 0.0.0.0 %s", ethergw) < 0)
+ fprint(2, "fw: cannot add default route: %r\n");
+ else
+ fprint(2, "fw: default route via %s\n", ethergw);
+ close(k);
+ }
+ }
+
+ rulechanged = rulesdidchange;
+ proccreate(etherin, prot, 32*1024);
+ proccreate(etherout, prot, 32*1024);
+ servectl(mtpt, srvname);
+ threadexits(nil);
+ }
+
+ if(argc == 1){
+ if(wireonly)
+ sysfatal("-W given, but no interfaces to filter between");
+ servenet(netmtpt, srvname, netmtpt);
+ threadexits(nil);
+ }
+
+ fprint(2, "fw: filtering packets\n");
+ syslog(0, "fw", "started, filtering packets, rules %s", rulepath);
+ out = parseside(argv[1], "outside");
+ in = parseside(argv[2], "inside");
+ wireup(out);
+ wireup(in);
+
+ rulechanged = rulesdidchange;
+
+ /* inside -> outside is "out"; outside -> inside is "in" */
+ outargs[0] = in;
+ outargs[1] = out;
+ outargs[2] = (Wire*)Vout;
+ proccreate(relayproc, outargs, 32*1024);
+
+ inargs[0] = out;
+ inargs[1] = in;
+ inargs[2] = (Wire*)Vin;
+ proccreate(relayproc, inargs, 32*1024);
+
+ servectl(mtpt, srvname);
+ threadexits(nil);
+}
diff --git a/fw/src/mkfile b/fw/src/mkfile
new file mode 100644
index 0000000..25ba677
--- /dev/null
+++ b/fw/src/mkfile
@@ -0,0 +1,9 @@
+</$objtype/mkfile
+
+TARG=fw
+OFILES=fw.$O rules.$O netfs.$O ether.$O
+HFILES=rules.h
+LIB=/$objtype/lib/lib9p.a /$objtype/lib/libthread.a /$objtype/lib/libndb.a /$objtype/lib/libip.a /$objtype/lib/libbio.a
+BIN=/$objtype/bin
+
+</sys/src/cmd/mkone
diff --git a/fw/src/netfs.c b/fw/src/netfs.c
new file mode 100644
index 0000000..c87412d
--- /dev/null
+++ b/fw/src/netfs.c
@@ -0,0 +1,555 @@
+/*
+ * fw, request-filtering half - a per-namespace connection firewall.
+ *
+ * Serves a mirror of /net and mounts it back over /net. Almost
+ * everything passes straight through; the interesting part is a write
+ * of "connect" or "announce" to a protocol ctl file, which is matched
+ * against a rule list before it reaches the kernel. A denial fails
+ * the write, and dial(2) hands the text to whoever called it.
+ *
+ * The real /net needs no second name, and must not have one: any path
+ * that still reaches it is a way around this. lib9p forks the server
+ * proc with RFNAMEG (see postsrv in /sys/src/lib9p/post.c), so the
+ * server keeps a private copy of the namespace as it was before the
+ * mount. "/net" in here is the real one; "/net" out there is us.
+ *
+ * None of this holds unless the sandboxed process is also denied #I,
+ * or it can bind the IP stack back in and ignore us. The wrapper does
+ * that with a write to /dev/drivers; see fw(8).
+ */
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+#include <bio.h>
+#include <ndb.h>
+#include <ip.h>
+#include "rules.h"
+
+/*
+ * One per fid. fd is the host file behind it, and is closed only when
+ * the fid is clunked: for a ctl file that close is what tears down the
+ * connection, so the two lifetimes have to be the same one.
+ */
+typedef struct Fnode Fnode;
+struct Fnode
+{
+ char *path; /* relative to the root, "" is the root */
+ int fd;
+ char *dbuf; /* directory, encoded at open */
+ long dlen;
+};
+
+static char *orig = "/net";
+
+/*
+ * The real /net is a union of devip and the cs and dns mounts, whose
+ * qids are allocated by different servers and can collide. Hash the
+ * path instead: stable across walks, unique across servers.
+ */
+static uvlong
+hashpath(char *s)
+{
+ uvlong h;
+
+ h = 14695981039346656037ULL;
+ while(*s != '\0'){
+ h ^= (uchar)*s++;
+ h *= 1099511628211ULL;
+ }
+ return h;
+}
+
+static void
+mkqid(Qid *q, char *path, Qid *real)
+{
+ q->path = hashpath(path);
+ q->vers = real->vers;
+ q->type = real->type;
+}
+
+static char*
+realpath(char *path)
+{
+ if(*path == '\0')
+ return estrdup(orig);
+ return smprint("%s/%s", orig, path);
+}
+
+static char*
+childpath(char *dir, char *name)
+{
+ char *p, *q;
+
+ if(strcmp(name, "..") == 0){
+ p = estrdup(dir);
+ if((q = strrchr(p, '/')) != nil)
+ *q = '\0';
+ else
+ *p = '\0';
+ return p;
+ }
+ if(*dir == '\0')
+ return estrdup(name);
+ return smprint("%s/%s", dir, name);
+}
+
+/* does path name this top-level entry, or something under it? */
+static int
+under(char *path, char *name)
+{
+ int n;
+
+ n = strlen(name);
+ return strncmp(path, name, n) == 0 && (path[n] == '\0' || path[n] == '/');
+}
+
+/*
+ * Raw packet access. Kept out of the served tree entirely rather than
+ * made unopenable, so that a program probing for a way out does not
+ * even see one.
+ */
+static int
+hidden(char *path)
+{
+ if(strncmp(path, "ether", 5) == 0 && path[5] >= '0' && path[5] <= '9')
+ return 1;
+ return under(path, "ipmux");
+}
+
+static char*
+protect(char *path, int mode)
+{
+ if(hidden(path))
+ return "fw: does not exist";
+ if(strcmp(path, "ipifc/clone") == 0)
+ return "fw: interface creation denied";
+ if(under(path, "ipifc") || under(path, "iproute") || under(path, "arp"))
+ if((mode & 3) != OREAD)
+ return "fw: read-only under fw";
+ return nil;
+}
+
+/*
+ * A directory in the root is a protocol directory if it has a clone
+ * file. Asking the filesystem beats hardcoding a list that goes stale.
+ */
+static int
+isproto(char *name)
+{
+ char *p;
+ int ok;
+
+ if(strchr(name, '/') != nil || *name == '\0')
+ return 0;
+ p = smprint("%s/%s/clone", orig, name);
+ ok = access(p, AEXIST) == 0;
+ free(p);
+ return ok;
+}
+
+/*
+ * If path is a protocol ctl file - "tcp/clone" or "tcp/1/ctl" - return
+ * the protocol name. Opening clone yields an fd that is itself the new
+ * connection's ctl file, so both spellings take a connect write.
+ */
+static char*
+ctlproto(char *path)
+{
+ char buf[64], *p, *q;
+ int n;
+
+ if((p = strchr(path, '/')) == nil)
+ return nil;
+ n = p - path;
+ if(n <= 0 || n >= sizeof buf)
+ return nil;
+ memmove(buf, path, n);
+ buf[n] = '\0';
+ p++;
+ if(strcmp(p, "clone") != 0){
+ if((q = strchr(p, '/')) == nil || strcmp(q+1, "ctl") != 0)
+ return nil;
+ }
+ if(!isproto(buf))
+ return nil;
+ return estrdup(buf);
+}
+
+/*
+ * connect takes addr!port with optional trailing fields; announce takes
+ * a bare port, or addr!port with addr often "*".
+ */
+static char*
+checkctl(char *proto, char *msg, long n)
+{
+ char buf[512], *f[8], *a[4], *addr, *e;
+ static char err[128];
+ uchar ip[IPaddrlen], mask[IPaddrlen];
+ Rule *rule;
+ int nf, na, verb, anyip, port, lport;
+
+ if(n <= 0)
+ return nil;
+ if(n >= sizeof buf)
+ n = sizeof buf - 1;
+ memmove(buf, msg, n);
+ buf[n] = '\0';
+
+ if((nf = tokenize(buf, f, nelem(f))) < 1)
+ return nil;
+ if(strcmp(f[0], "connect") == 0)
+ verb = Vout;
+ else if(strcmp(f[0], "announce") == 0)
+ verb = Vin;
+ else
+ return nil; /* hangup, ttl, keepalive: not policy */
+ if(nf < 2)
+ return nil; /* malformed; let the kernel say so */
+
+ na = getfields(f[1], a, nelem(a), 0, "!");
+ if(na < 1)
+ return nil;
+ if(na == 1){
+ addr = "*"; /* announce 17019 */
+ port = atoi(a[0]);
+ }else{
+ addr = a[0];
+ port = strcmp(a[1], "*") == 0 ? -1 : atoi(a[1]);
+ }
+
+ /*
+ * Which end the port and the address belong to, so that a rule
+ * means the same here as it does against a packet.
+ *
+ * connect names the far end: its port is the peer's, and the
+ * local port is whatever the kernel picks, so unknown.
+ *
+ * announce names this end: its port is ours, its address is a
+ * local address to listen on, and the peer is nobody yet - we
+ * find out who connected only at listen time. A rule naming a
+ * peer therefore cannot apply to an announce, which is right:
+ * at this point there is no peer to name.
+ */
+ if(verb == Vin){
+ lport = port;
+ port = -1;
+ anyip = 1;
+ }else{
+ lport = -1;
+ anyip = strcmp(addr, "*") == 0;
+ if(!anyip && parseipandmask(ip, mask, addr, nil) == -1){
+ syslog(0, "fw", "deny %s %s %s: unparseable address",
+ proto, f[0], f[1]);
+ return "fw: unparseable address";
+ }
+ }
+ if((e = matchrule(verb, proto, ip, anyip, port, lport, &rule)) != nil){
+ if(rule != nil && rule->log)
+ syslog(0, "fw", "deny %s %s %s: %s", proto, f[0], f[1], e);
+ snprint(err, sizeof err, "fw: %s", e);
+ return err;
+ }
+ if(rule != nil && rule->log)
+ syslog(0, "fw", "allow %s %s %s", proto, f[0], f[1]);
+ return nil;
+}
+
+/*
+ * A directory is read once at open, filtered, and re-encoded; reads
+ * then slice that buffer at entry boundaries. This gets the offset
+ * rules right without a gen function, and an open directory is a
+ * snapshot on Plan 9 anyway.
+ */
+static char*
+slurpdir(Fnode *f, char *rp)
+{
+ char *buf, *cp;
+ Dir *d;
+ Qid q;
+ long sz;
+ int fd, i, n, m;
+
+ if((fd = open(rp, OREAD)) < 0)
+ return "fw: cannot open directory";
+ n = dirreadall(fd, &d);
+ close(fd);
+ if(n < 0)
+ return "fw: cannot read directory";
+
+ buf = nil;
+ sz = 0;
+ for(i = 0; i < n; i++){
+ cp = childpath(f->path, d[i].name);
+ if(hidden(cp)){
+ free(cp);
+ continue;
+ }
+ q = d[i].qid;
+ mkqid(&d[i].qid, cp, &q);
+ free(cp);
+ m = sizeD2M(&d[i]);
+ if((buf = realloc(buf, sz + m)) == nil)
+ sysfatal("out of memory");
+ convD2M(&d[i], (uchar*)buf + sz, m);
+ sz += m;
+ }
+ free(d);
+ f->dbuf = buf;
+ f->dlen = sz;
+ return nil;
+}
+
+static void
+dirslice(Req *r, Fnode *f)
+{
+ long o, e, m;
+
+ for(o = 0; o < f->dlen && o != r->ifcall.offset; o += m)
+ m = GBIT16((uchar*)f->dbuf + o) + BIT16SZ;
+ if(o != r->ifcall.offset || o >= f->dlen){
+ r->ofcall.count = 0;
+ return;
+ }
+ for(e = o; e < f->dlen; e += m){
+ m = GBIT16((uchar*)f->dbuf + e) + BIT16SZ;
+ if(e + m - o > r->ifcall.count)
+ break;
+ }
+ memmove(r->ofcall.data, f->dbuf + o, e - o);
+ r->ofcall.count = e - o;
+}
+
+static Fnode*
+newfnode(char *path)
+{
+ Fnode *f;
+
+ f = emalloc(sizeof *f);
+ f->path = estrdup(path);
+ f->fd = -1;
+ return f;
+}
+
+static void
+fsattach(Req *r)
+{
+ Fnode *f;
+ Dir *d;
+
+ if((d = dirstat(orig)) == nil){
+ responderror(r);
+ return;
+ }
+ f = newfnode("");
+ mkqid(&r->fid->qid, "", &d->qid);
+ free(d);
+ r->fid->aux = f;
+ r->ofcall.qid = r->fid->qid;
+ respond(r, nil);
+}
+
+static char*
+fsclone(Fid *old, Fid *new)
+{
+ Fnode *f;
+
+ f = old->aux;
+ new->aux = newfnode(f->path);
+ return nil;
+}
+
+static char*
+fswalk1(Fid *fid, char *name, Qid *q)
+{
+ Fnode *f;
+ Dir *d;
+ char *np, *rp;
+
+ f = fid->aux;
+ np = childpath(f->path, name);
+ if(hidden(np)){
+ free(np);
+ return "fw: does not exist";
+ }
+ rp = realpath(np);
+ d = dirstat(rp);
+ free(rp);
+ if(d == nil){
+ free(np);
+ return "fw: does not exist";
+ }
+ mkqid(q, np, &d->qid);
+ free(d);
+ free(f->path);
+ f->path = np;
+ fid->qid = *q;
+ return nil;
+}
+
+static void
+fsopen(Req *r)
+{
+ Fnode *f;
+ char *rp, *e;
+ int mode;
+
+ f = r->fid->aux;
+ mode = r->ifcall.mode;
+ if((e = protect(f->path, mode)) != nil){
+ respond(r, e);
+ return;
+ }
+ rp = realpath(f->path);
+ if(r->fid->qid.type & QTDIR){
+ e = slurpdir(f, rp);
+ free(rp);
+ respond(r, e);
+ return;
+ }
+ /* opening listen blocks until someone connects */
+ srvrelease(r->srv);
+ f->fd = open(rp, mode & ~ORCLOSE);
+ srvacquire(r->srv);
+ free(rp);
+ if(f->fd < 0){
+ responderror(r);
+ return;
+ }
+ respond(r, nil);
+}
+
+static void
+fsread(Req *r)
+{
+ Fnode *f;
+ long n;
+
+ f = r->fid->aux;
+ if(r->fid->qid.type & QTDIR){
+ dirslice(r, f);
+ respond(r, nil);
+ return;
+ }
+ if(f->fd < 0){
+ respond(r, "fw: not open");
+ return;
+ }
+ srvrelease(r->srv);
+ n = pread(f->fd, r->ofcall.data, r->ifcall.count, r->ifcall.offset);
+ srvacquire(r->srv);
+ if(n < 0){
+ responderror(r);
+ return;
+ }
+ r->ofcall.count = n;
+ respond(r, nil);
+}
+
+static void
+fswrite(Req *r)
+{
+ Fnode *f;
+ char *proto, *e;
+ long n;
+
+ f = r->fid->aux;
+ if(f->fd < 0){
+ respond(r, "fw: not open");
+ return;
+ }
+ if((proto = ctlproto(f->path)) != nil){
+ e = checkctl(proto, r->ifcall.data, r->ifcall.count);
+ free(proto);
+ if(e != nil){
+ respond(r, e);
+ return;
+ }
+ }
+ srvrelease(r->srv);
+ n = pwrite(f->fd, r->ifcall.data, r->ifcall.count, r->ifcall.offset);
+ srvacquire(r->srv);
+ if(n < 0){
+ responderror(r);
+ return;
+ }
+ r->ofcall.count = n;
+ respond(r, nil);
+}
+
+static void
+fsstat(Req *r)
+{
+ Fnode *f;
+ Dir *d;
+ char *rp, *name;
+ Qid q;
+
+ f = r->fid->aux;
+ rp = realpath(f->path);
+ d = dirstat(rp);
+ free(rp);
+ if(d == nil){
+ responderror(r);
+ return;
+ }
+ q = d->qid;
+ mkqid(&d->qid, f->path, &q);
+ if((name = strrchr(f->path, '/')) != nil)
+ name++;
+ else if(*f->path != '\0')
+ name = f->path;
+ else
+ name = "/";
+ r->d = *d;
+ r->d.name = estrdup(name);
+ r->d.uid = estrdup(d->uid);
+ r->d.gid = estrdup(d->gid);
+ r->d.muid = estrdup(d->muid);
+ free(d);
+ respond(r, nil);
+}
+
+static void
+fsdestroyfid(Fid *fid)
+{
+ Fnode *f;
+
+ if((f = fid->aux) == nil)
+ return;
+ fid->aux = nil;
+ if(f->fd >= 0)
+ close(f->fd);
+ free(f->dbuf);
+ free(f->path);
+ free(f);
+}
+
+static Srv fs =
+{
+ .attach = fsattach,
+ .clone = fsclone,
+ .walk1 = fswalk1,
+ .open = fsopen,
+ .read = fsread,
+ .write = fswrite,
+ .stat = fsstat,
+ .destroyfid = fsdestroyfid,
+};
+
+/*
+ * Serve a filtered view of "orig" at "mtpt". Rules have already been
+ * read; this is the request-filtering half of fw, kept in its own file
+ * only because it is a different mechanism, not a different program.
+ */
+void
+servenet(char *mtpt, char *srvname, char *net)
+{
+ orig = net;
+ if(access(orig, AEXIST) < 0)
+ sysfatal("%s: %r", orig);
+ fprint(2, "fw: filtering requests on %s\n", mtpt);
+ syslog(0, "fw", "started, filtering requests on %s", mtpt);
+ threadpostmountsrv(&fs, srvname, mtpt, MREPL);
+}
diff --git a/fw/src/rules.c b/fw/src/rules.c
new file mode 100644
index 0000000..c0df82d
--- /dev/null
+++ b/fw/src/rules.c
@@ -0,0 +1,440 @@
+/*
+ * The shared rule engine.
+ *
+ * Rules are an ndb file: one entry is one rule, matched top to bottom,
+ * first match wins, no match denies. ndbparse hands entries back in
+ * file order, which is what keeps this a list rather than a lookup.
+ * Firewall matching is a solved interface and being different about it
+ * would be cost for its own sake.
+ *
+ * allow=out proto=tcp port=443
+ * deny=in ip=1.1.1.1
+ * allow=out ip=10.0.2.0/24
+ *
+ * connect and announce are accepted as spellings of out and in, since
+ * that is what they mean at the ctl layer.
+ *
+ * ip and port always mean the *peer* - the far end of the traffic,
+ * whichever direction it is going - so one rule reads the same whether
+ * it is enforced against a connect string or against a packet header.
+ * lport is the local side, and is only meaningful at the packet layer.
+ */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ndb.h>
+#include <ip.h>
+#include "rules.h"
+
+Rule *rules;
+void (*rulechanged)(void);
+
+static Lock rulelock;
+static Rule *lastrule;
+static char *rulefile;
+static char *parseerr;
+static jmp_buf parsejmp;
+static int parsing;
+
+/*
+ * A bad rule typed at a ctl file must fail the write, not the firewall,
+ * so parsing longjmps out instead of calling sysfatal when it is being
+ * driven from there.
+ */
+static void
+rulefail(char *fmt, ...)
+{
+ static char buf[256];
+ va_list arg;
+
+ va_start(arg, fmt);
+ vsnprint(buf, sizeof buf, fmt, arg);
+ va_end(arg);
+ if(parsing){
+ parseerr = buf;
+ longjmp(parsejmp, 1);
+ }
+ sysfatal("%s", buf);
+}
+
+static struct {
+ char *name;
+ int num;
+} protos[] = {
+ { "icmp", 1 },
+ { "igmp", 2 },
+ { "tcp", 6 },
+ { "udp", 17 },
+ { "gre", 47 },
+ { "icmpv6", 58 },
+ { "il", 40 },
+ { nil, 0 },
+};
+
+int
+protoname2num(char *name)
+{
+ int i;
+
+ for(i = 0; protos[i].name != nil; i++)
+ if(strcmp(protos[i].name, name) == 0)
+ return protos[i].num;
+ return -1;
+}
+
+char*
+protonum2name(int num)
+{
+ static char buf[16];
+ int i;
+
+ for(i = 0; protos[i].name != nil; i++)
+ if(protos[i].num == num)
+ return protos[i].name;
+ snprint(buf, sizeof buf, "%d", num);
+ return buf;
+}
+
+void*
+emalloc(ulong n)
+{
+ void *p;
+
+ if((p = mallocz(n, 1)) == nil)
+ sysfatal("out of memory");
+ return p;
+}
+
+char*
+estrdup(char *s)
+{
+ char *p;
+
+ if((p = strdup(s)) == nil)
+ sysfatal("out of memory");
+ return p;
+}
+
+static int
+verbof(char *s)
+{
+ if(*s == '\0' || strcmp(s, "*") == 0)
+ return Vany;
+ if(strcmp(s, "out") == 0 || strcmp(s, "connect") == 0)
+ return Vout;
+ if(strcmp(s, "in") == 0 || strcmp(s, "announce") == 0)
+ return Vin;
+ return -1;
+}
+
+/*
+ * An attribute we do not recognise is fatal rather than ignored. In a
+ * lookup database ignoring it would be the friendly thing; here,
+ * quietly dropping "prot=tcp" would leave a rule matching every
+ * protocol instead of one, and a typo that fails open is not something
+ * a firewall gets to do.
+ */
+static void
+addrule(Ndbtuple *t, int nr)
+{
+ char *ip, *mask, *p, abuf[64];
+ Rule *r;
+ int n, act;
+
+ act = -1;
+ ip = mask = nil;
+ r = emalloc(sizeof *r);
+ r->nr = nr;
+ r->verb = Vany;
+ r->port = -1;
+ r->lport = -1;
+ r->anyip = 1;
+
+ for(; t != nil; t = t->entry){
+ if(strcmp(t->attr, "allow") == 0 || strcmp(t->attr, "deny") == 0){
+ if(act >= 0)
+ rulefail("%s: rule %d: two actions in one rule", rulefile, nr);
+ act = strcmp(t->attr, "allow") == 0;
+ if((r->verb = verbof(t->val)) < 0)
+ rulefail("%s: rule %d: %s: want in, out or *",
+ rulefile, nr, t->val);
+ }else if(strcmp(t->attr, "proto") == 0){
+ if(strcmp(t->val, "*") != 0)
+ r->proto = estrdup(t->val);
+ }else if(strcmp(t->attr, "port") == 0){
+ if(strcmp(t->val, "*") != 0)
+ r->port = atoi(t->val);
+ }else if(strcmp(t->attr, "lport") == 0){
+ if(strcmp(t->val, "*") != 0)
+ r->lport = atoi(t->val);
+ }else if(strcmp(t->attr, "log") == 0){
+ if(strcmp(t->val, "no") == 0 || strcmp(t->val, "0") == 0)
+ r->log = 0;
+ else
+ r->log = 1;
+ }else if(strcmp(t->attr, "ip") == 0)
+ ip = t->val;
+ else if(strcmp(t->attr, "ipmask") == 0)
+ mask = t->val;
+ else
+ rulefail("%s: rule %d: %s: unknown attribute", rulefile, nr, t->attr);
+ }
+ if(act < 0)
+ rulefail("%s: rule %d: needs allow= or deny=", rulefile, nr);
+ r->allow = act;
+
+ if(ip != nil && strcmp(ip, "*") != 0){
+ /*
+ * ip=10.0.2.0/24 is taken as well as ip=10.0.2.0 ipmask=/24.
+ * parseipmask tells a prefix length from a dotted mask by the
+ * leading slash, so the slash has to survive the split.
+ */
+ if((p = strchr(ip, '/')) != nil){
+ if(mask != nil)
+ rulefail("%s: rule %d: mask given twice", rulefile, nr);
+ n = p - ip;
+ if(n >= sizeof abuf)
+ rulefail("%s: rule %d: address too long", rulefile, nr);
+ memmove(abuf, ip, n);
+ abuf[n] = '\0';
+ mask = p;
+ ip = abuf;
+ }
+ if(parseipandmask(r->ip, r->mask, ip, mask) == -1)
+ rulefail("%s: rule %d: %s: unparseable address or mask",
+ rulefile, nr, ip);
+ r->anyip = 0;
+ }
+
+ if(lastrule == nil)
+ rules = r;
+ else
+ lastrule->next = r;
+ lastrule = r;
+}
+
+/*
+ * Parse without installing. Returns the new list, or nil with *err set.
+ * An empty file is a valid rule set: it denies everything.
+ */
+Rule*
+parserules(char *file, char **err)
+{
+ Rule *new, *save, *savelast;
+ Ndbtuple *t;
+ Ndb *db;
+ int nr;
+
+ save = rules;
+ savelast = lastrule;
+ rules = lastrule = nil;
+ rulefile = file;
+ parseerr = nil;
+
+ if((db = ndbopen(file)) == nil){
+ static char eb[128];
+
+ snprint(eb, sizeof eb, "%s: %r", file);
+ *err = eb;
+ rules = save;
+ lastrule = savelast;
+ return nil;
+ }
+ parsing = 1;
+ if(setjmp(parsejmp) == 0){
+ for(nr = 1; (t = ndbparse(db)) != nil; nr++){
+ addrule(t, nr);
+ ndbfree(t);
+ }
+ }
+ parsing = 0;
+ ndbclose(db);
+
+ new = rules;
+ rules = save;
+ lastrule = savelast;
+ if(parseerr != nil){
+ freerules(new);
+ *err = parseerr;
+ return nil;
+ }
+ *err = nil;
+ return new;
+}
+
+void
+freerules(Rule *r)
+{
+ Rule *next;
+
+ for(; r != nil; r = next){
+ next = r->next;
+ free(r->proto);
+ free(r);
+ }
+}
+
+void
+installrules(Rule *new)
+{
+ Rule *old;
+
+ lock(&rulelock);
+ old = rules;
+ rules = new;
+ unlock(&rulelock);
+ freerules(old);
+ if(rulechanged != nil)
+ (*rulechanged)();
+}
+
+void
+readrules(char *file)
+{
+ Rule *new;
+ char *err;
+
+ if((new = parserules(file, &err)) == nil && err != nil)
+ sysfatal("%s", err);
+ installrules(new);
+}
+
+/*
+ * The current set, written back out as ndb. What comes out here must
+ * parse back in unchanged; it is what gets persisted.
+ */
+long
+fmtrules(char *buf, long nbuf)
+{
+ char *p, *e;
+ Rule *r;
+
+ p = buf;
+ e = buf + nbuf;
+ lock(&rulelock);
+ for(r = rules; r != nil; r = r->next){
+ p = seprint(p, e, "%s=%s", r->allow ? "allow" : "deny",
+ r->verb == Vin ? "in" : r->verb == Vout ? "out" : "*");
+ if(r->proto != nil)
+ p = seprint(p, e, "\tproto=%s", r->proto);
+ if(!r->anyip)
+ p = seprint(p, e, "\tip=%I\tipmask=%M", r->ip, r->mask);
+ if(r->port >= 0)
+ p = seprint(p, e, "\tport=%d", r->port);
+ if(r->lport >= 0)
+ p = seprint(p, e, "\tlport=%d", r->lport);
+ if(r->log)
+ p = seprint(p, e, "\tlog=yes");
+ p = seprint(p, e, "\n");
+ }
+ unlock(&rulelock);
+ return p - buf;
+}
+
+/*
+ * A firewall that was told to log and silently cannot is worse than one
+ * that never logged: you would believe you had an audit trail. syslog
+ * does not create its file, so say so plainly at startup rather than
+ * dropping the lines on the floor.
+ */
+void
+checklogging(void)
+{
+ Rule *r;
+ int fd;
+
+ for(r = rules; r != nil; r = r->next)
+ if(r->log)
+ break;
+ if(r == nil)
+ return;
+ if((fd = open("/sys/log/fw", OWRITE)) < 0){
+ fprint(2, "fw: rules ask for logging, but /sys/log/fw cannot be "
+ "written: %r\n");
+ fprint(2, "fw: make it once with: touch /sys/log/fw; chmod +a /sys/log/fw\n");
+ fprint(2, "fw: filtering anyway, but nothing will be logged\n");
+ return;
+ }
+ close(fd);
+}
+
+void
+dumprules(void)
+{
+ Rule *r;
+
+ for(r = rules; r != nil; r = r->next)
+ fprint(2, "rule %d: %s %s proto %s port %d lport %d anyip %d ip %I mask %I\n",
+ r->nr, r->allow ? "allow" : "deny",
+ r->verb == Vin ? "in" : r->verb == Vout ? "out" : "*",
+ r->proto != nil ? r->proto : "*", r->port, r->lport,
+ r->anyip, r->ip, r->mask);
+}
+
+/*
+ * The rules with a count of how often each has decided something. A
+ * rule that has never fired is either dead or protecting you from
+ * something that has not happened yet, and it is worth being able to
+ * tell which. Kept out of fmtrules so that what "rules" prints stays
+ * a rule set that can be written straight back.
+ */
+long
+fmthits(char *buf, long nbuf)
+{
+ char *p, *e;
+ Rule *r;
+
+ p = buf;
+ e = buf + nbuf;
+ lock(&rulelock);
+ for(r = rules; r != nil; r = r->next)
+ p = seprint(p, e, "%-8ld %s=%s%s%s\n", r->hits,
+ r->allow ? "allow" : "deny",
+ r->verb == Vin ? "in" : r->verb == Vout ? "out" : "*",
+ r->proto != nil ? "\tproto=" : "",
+ r->proto != nil ? r->proto : "");
+ unlock(&rulelock);
+ return p - buf;
+}
+
+char*
+matchrule(int verb, char *proto, uchar *ip, int anyip, int port, int lport, Rule **rp)
+{
+ uchar net[IPaddrlen], rnet[IPaddrlen];
+ static char err[128];
+ Rule *r;
+
+ if(rp != nil)
+ *rp = nil;
+ lock(&rulelock);
+ for(r = rules; r != nil; r = r->next){
+ if(r->verb != Vany && r->verb != verb)
+ continue;
+ if(r->proto != nil && (proto == nil || strcmp(r->proto, proto) != 0))
+ continue;
+ if(r->port >= 0 && r->port != port)
+ continue;
+ if(r->lport >= 0 && r->lport != lport)
+ continue;
+ if(!r->anyip){
+ if(anyip) /* a wildcard request cannot match a specific rule */
+ continue;
+ maskip(ip, r->mask, net);
+ maskip(r->ip, r->mask, rnet);
+ if(ipcmp(net, rnet) != 0)
+ continue;
+ }
+ if(rp != nil)
+ *rp = r;
+ r->hits++;
+ if(r->allow){
+ unlock(&rulelock);
+ return nil;
+ }
+ snprint(err, sizeof err, "denied by rule %d", r->nr);
+ unlock(&rulelock);
+ return err;
+ }
+ unlock(&rulelock);
+ return "denied, no rule matched";
+}
diff --git a/fw/src/rules.h b/fw/src/rules.h
new file mode 100644
index 0000000..6aad0ae
--- /dev/null
+++ b/fw/src/rules.h
@@ -0,0 +1,57 @@
+/*
+ * The rule engine, shared by both halves of fw: request filtering (the
+ * ctl layer) and packet filtering (the wire). One rule language, two enforcement points: a rule says what
+ * may happen, and the caller decides where it is enforced.
+ */
+typedef struct Rule Rule;
+
+/*
+ * Direction, not layer. A "connect" written to a ctl file and an
+ * egress packet are the same intent seen from two places, so they are
+ * the same verb and one rule file serves both enforcement points.
+ */
+enum
+{
+ Vin, /* inbound: announce, or a packet arriving */
+ Vout, /* outbound: connect, or a packet leaving */
+ Vany,
+};
+
+struct Rule
+{
+ int allow;
+ int verb;
+ char *proto; /* nil: any */
+ int anyip;
+ uchar ip[IPaddrlen]; /* the peer, whichever end that is */
+ uchar mask[IPaddrlen];
+ int port; /* peer port, -1: any */
+ int lport; /* local port, -1: any */
+ int log; /* note matches in /sys/log/fw */
+ long hits; /* how often it has decided something */
+ int nr;
+ Rule *next;
+};
+
+/*
+ * Rules are swapped wholesale, never mutated in place, so no packet is
+ * ever judged against a half-applied rule set. rulechanged, if set, is
+ * called after a swap - the wire half uses it to re-check live flows.
+ */
+extern Rule *rules;
+extern void (*rulechanged)(void);
+
+void readrules(char*); /* parse and install, fatal on error */
+Rule* parserules(char*, char**); /* parse only; nil + reason on error */
+void installrules(Rule*); /* swap in, then call rulechanged */
+void freerules(Rule*);
+long fmtrules(char*, long); /* current set, back in ndb form */
+long fmthits(char*, long); /* the same, with hit counts */
+void dumprules(void);
+void checklogging(void);
+char* matchrule(int verb, char *proto, uchar *ip, int anyip, int port, int lport, Rule**);
+int protoname2num(char*);
+char* protonum2name(int);
+
+void* emalloc(ulong);
+char* estrdup(char*);