diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-08-18 17:01:49 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-08-18 17:01:49 -0400 |
| commit | 0f922552ad8cc73c0c3c3674d484c3d78dd8c557 (patch) | |
| tree | 4bfc4222ccc202daa2d79d6a782727fac1fb9ae0 /fw/src/ether.c | |
| parent | 441c64d81594bb521350dbee6348f552401a1a2e (diff) | |
fw: a firewall, at a card, between two networks, or in front of a namespace
One program with three modes, sharing one rule engine and one ndb rule
language. Which mode it is depends on what you point it at, and it says
so at startup rather than choosing silently.
fw -e /net/ether0 rules.ndb a card: every packet in or out
fw rules.ndb <side> <side> two networks: everything crossing
fw rules.ndb one namespace: what programs ask for
The first two filter packets on a wire, using the pkt medium: the stack
gives up its card and gets a synthetic one with fw on the other end, so
nothing reaches it that fw did not pass. Since the stack no longer has
ethernet, fw answers ARP for the address it stands in for.
The third serves a filtered /net and matches connect and announce before
they reach the kernel, so a refusal comes back out of dial(2) with a
reason. That is only a boundary if the program also loses #I, which
/dev/drivers does and cannot be undone; fw.rc does it in the right order.
Rules are ndb, matched top to bottom, first match wins, no match denies.
Connections are tracked, so permitting traffic one way permits the
replies. A rule change drops connections the new rules forbid rather
than letting them finish: a block blocks. Logging is per rule, to
/sys/log/fw.
Tested on the init-test VM in all three modes: a page fetched through a
real card, a TCP handshake across two networks, request filtering with
the escape routes closed, live rule changes killing established
connections, and one rule file working unchanged at both altitudes.
doc/todo.md has what is not done. Item 1 is the one that matters: a fw
that dies takes the card's address with it, so the machine loses its
network and fw cannot restart unaided. That also blocks svc supervision.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'fw/src/ether.c')
| -rw-r--r-- | fw/src/ether.c | 374 |
1 files changed, 374 insertions, 0 deletions
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; +} |
