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 | |
| 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')
| -rw-r--r-- | fw/src/ether.c | 374 | ||||
| -rw-r--r-- | fw/src/fw.c | 1391 | ||||
| -rw-r--r-- | fw/src/mkfile | 9 | ||||
| -rw-r--r-- | fw/src/netfs.c | 555 | ||||
| -rw-r--r-- | fw/src/rules.c | 440 | ||||
| -rw-r--r-- | fw/src/rules.h | 57 |
6 files changed, 2826 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; +} 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*); |
