/* * 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 #include #include #include #include #include #include #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 frag; /* Fragno, Fragfirst or Fraglater */ int id; /* which datagram, if it is fragmented */ int verb; }; enum { Fragno, /* a whole datagram */ Fragfirst, /* the piece with the transport header */ Fraglater, /* a piece without one */ }; 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)){ /* * An expired flow must not match. Reaping only * happens when a flow is added, so on a quiet * firewall nothing is ever reaped and a flow that * timed out long ago would keep passing traffic, * refreshing itself on every packet. */ if(now - f->last > flowtimeout(f->proto)) return 0; 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; Match m; int i, n; n = 0; memset(&m, 0, sizeof m); m.count = 0; /* re-checking a flow is not traffic */ lock(&flowlock); for(i = 0; i < Nflow; i++) for(pp = &flowtab[i]; (f = *pp) != nil; ){ m.verb = f->verb; m.proto = protonum2name(f->proto); if(f->verb == Vout){ m.ip = f->dst; m.port = f->dport; m.lport = f->sport; }else{ m.ip = f->src; m.port = f->sport; m.lport = f->dport; } if(!matchrule(&m)){ *pp = f->next; free(f); n++; }else pp = &f->next; } unlock(&flowlock); return n; } /* * Fragment trains. * * Only the first fragment of a datagram carries the transport header, * so every later one matches no port, and a rule set written in ports - * which is every rule set worth writing - denies it. The first * fragment crosses and the receiver waits for the rest until it gives * up. Reading ports out of a later fragment, which is what this did * before that, is worse: a fragment whose payload bytes happen to look * like an open connection is let through. * * So the first fragment decides and the rest of the train inherits. * The train is what the receiving stack will reassemble on - protocol, * addresses and identification - and it lasts about as long as that * stack will hold the pieces. A train we never saw the head of is * judged on its addresses alone, and so is normally denied, which is * the right way round: it is either an attack or a datagram whose * first fragment we already refused. * * IPv6 fragments are carried in an extension header, which fw does not * walk, so none of this reaches them. */ enum { Nfrag = 61, Fragtime = 30 }; typedef struct Frag Frag; struct Frag { int proto; int id; uchar src[IPaddrlen]; uchar dst[IPaddrlen]; long last; Frag *next; }; static Frag *fragtab[Nfrag]; static Lock fraglock; static int fragsweep; static uint fraghash(int proto, uchar *src, uchar *dst, int id) { uint h; int i; h = proto*31 + id; for(i = 0; i < IPaddrlen; i++) h = h*33 + src[i]*3 + dst[i]; return h % Nfrag; } static void reapfrags(int i, long now) { Frag *f, **pp; for(pp = &fragtab[i]; (f = *pp) != nil; ){ if(now - f->last > Fragtime){ *pp = f->next; free(f); }else pp = &f->next; } } /* is this a later piece of a datagram whose head we let through? */ static int fragseen(Pkt *p) { Frag *f; long now; uint h; int r; r = 0; now = time(0); h = fraghash(p->proto, p->src, p->dst, p->id); lock(&fraglock); for(f = fragtab[h]; f != nil; f = f->next) if(f->proto == p->proto && f->id == p->id && ipcmp(f->src, p->src) == 0 && ipcmp(f->dst, p->dst) == 0){ if(now - f->last <= Fragtime){ f->last = now; r = 1; } break; } unlock(&fraglock); return r; } static void fragadd(Pkt *p) { Frag *f; long now; uint h; now = time(0); h = fraghash(p->proto, p->src, p->dst, p->id); lock(&fraglock); reapfrags(h, now); fragsweep = (fragsweep + 1) % Nfrag; reapfrags(fragsweep, now); f = emalloc(sizeof *f); f->proto = p->proto; f->id = p->id; ipmove(f->src, p->src); ipmove(f->dst, p->dst); f->last = now; f->next = fragtab[h]; fragtab[h] = f; unlock(&fraglock); } 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*, 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); /* * pktmedium claims 4096 with no link header, where a card is 1514 * with 14, so a stack behind one of these thinks it may send 4096 * bytes. In card mode devether then refuses the frame outright; * between two stacks the packet only has further to go before an * ether interface refuses it. Either way 1500 is what the traffic * will meet in the end, so say so here. Left alone, the only * thing hiding it is remote peers capping the MSS. */ if(fprint(w->cfd, "mtu 1500") < 0) fprint(2, "fw: %s: cannot set mtu: %r\n", 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); /* * Which piece of a datagram this is. A later one has no * transport header, so reading ports out of it gives * payload bytes; the first one of a train has to be * remembered so the rest can inherit its verdict. */ p->id = nhgets(b + 4); if((nhgets(b + 6) & 0x1FFF) != 0) p->frag = Fraglater; else if((b[6] & 0x20) != 0) p->frag = Fragfirst; 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->frag != Fraglater && (p->proto == 6 || p->proto == 17) && n >= 4){ p->sport = nhgets(t); p->dport = nhgets(t + 2); } p->ok = 1; } /* * 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) { Match m; 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; memset(&m, 0, sizeof m); m.count = 1; m.verb = verb; m.proto = protonum2name(p->proto); if(verb == Vout){ m.ip = p->dst; m.port = p->dport; m.lport = p->sport; }else{ m.ip = p->src; m.port = p->sport; m.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(p->frag == Fragfirst) fragadd(p); return 1; } if(p->frag == Fraglater && fragseen(p)){ nallow++; if(debug) fprint(2, "pass %s %s %I -> %I id %d (fragment)\n", verb == Vout ? "out" : "in", protonum2name(p->proto), p->src, p->dst, p->id); return 1; } if(!matchrule(&m)){ ndeny++; if(m.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, m.err); 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, m.err); return 0; } nallow++; if(p->frag == Fragfirst) fragadd(p); if(m.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); if(p->frag != Fraglater) /* it has no ports to key a flow on */ flowadd(p); return 1; } /* * One direction. "verb" says which way this is; permitted() takes it * from there, so the wire the packet came from only decides which way * "in" and "out" mean and where the packet goes if it may go at all. */ int revalidate(void); static void relay(Wire *from, Wire *to, int verb) { uchar *buf; Pkt p; int n; /* * 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); threadexitsall("wire"); } if(!permitted(buf, n, verb, &p)) continue; 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 insert a rule at the top, where it wins\n" "append add a rule at the bottom\n" "delete 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"; /* * 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 = rulestext()) == nil) return "out of memory"; if(delete > 0){ /* rulestext 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 = rulestext()) == nil) return "out of memory"; n = strlen(buf); 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; } /* allocated to fit, for the reason rulestext is */ static char* flowtext(void) { char *buf, *p, *e; Flow *f; long now, sz; int i; now = time(0); lock(&flowlock); sz = 1; for(i = 0; i < Nflow; i++) for(f = flowtab[i]; f != nil; f = f->next) sz += 160; if((buf = malloc(sz)) == nil){ unlock(&flowlock); return nil; } p = buf; e = buf + sz; 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 *s, *t; switch((int)(uintptr)r->fid->file->aux){ case Qctl: readstr(r, ctltext); break; case Qrules: if((s = rulestext()) == nil){ respond(r, "out of memory"); return; } 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 = hitstext()) == nil){ respond(r, "out of memory"); return; } if((t = smprint("passed %d\ndropped %d\n\n%s", nallow, ndeny, s)) == nil){ free(s); respond(r, "out of memory"); return; } free(s); readstr(r, t); free(t); 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 , " "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){ int nr; nr = atoi(arg); if(nr < 1) err = "delete wants a rule number, from 1"; else err = editrules(nil, 0, nr); } 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) { char *user; File *root; /* * Owned by whoever is running fw, not by a user called "fw" that * does not exist: with 0600 that locked out the administrator as * effectively as everyone else. */ user = getuser(); fs.tree = alloctree(user, user, DMDIR|0555, nil); root = fs.tree->root; closefile(createfile(root, "ctl", user, 0600, (void*)Qctl)); closefile(createfile(root, "rules", user, 0600, (void*)Qrules)); closefile(createfile(root, "flows", user, 0440, (void*)Qflows)); closefile(createfile(root, "stats", user, 0440, (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]); } /* 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){ /* * One relay stopping would leave the other running * and that direction unfiltered, with nothing to * notice. Take the whole firewall down instead. */ fprint(2, "fw: %s: read the card: %r\n", w->side); syslog(0, "fw", "stopped reading the card: %r"); threadexitsall("card"); } 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, *frame; Wire *w; Pkt p; int n; w = a; buf = emalloc(Maxpkt); frame = emalloc(Ehdrlen + Maxpkt); /* not the proc stack */ 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"); threadexitsall("stack"); } if(permitted(buf, n, Vout, &p)) etherwriteip(frame, Ehdrlen + Maxpkt, buf, n, ethermask); } } /* * The first n lines of a Biobuf, copied out, since Brdline's buffer is * only good until the next one. Returns how many there were. */ static int rdline(Biobuf *b, char **lines, int n) { char *p; int i; for(i = 0; i < n; i++){ if((p = Brdline(b, '\n')) == nil) break; p[Blinelen(b)-1] = '\0'; lines[i] = estrdup(p); } return i; } static void freelines(char **lines, int n) { int i; for(i = 0; i < n; i++){ free(lines[i]); lines[i] = nil; } } /* * 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], *lines[2], *f[8], *p; Biobuf *b; int i, nf, found; found = 0; for(i = 0; i < 16 && !found; i++){ snprint(path, sizeof path, "%s/ipifc/%d/status", net, i); if((b = Bopen(path, OREAD)) == nil) continue; /* * The device line, then the first address line. Read, not * searched: the whole status text used to be scanned for the * device name, which an address could satisfy, and it was * read into a fixed buffer that a few addresses would fill. */ if(rdline(b, lines, 2) == 2 && strstr(lines[0], dev) != nil){ nf = tokenize(lines[1], f, nelem(f)); if(nf >= 2){ snprint(addr, naddr, "%s%s", f[0], f[1]); found = 1; } } freelines(lines, 2); Bterm(b); } if(!found) return -1; /* * The default route names the gateway. A line at a time: a * routing table is as long as it is, and reading 1024 bytes of it * meant the default route could be off the end and the gateway * silently unknown. */ *gw = '\0'; snprint(path, sizeof path, "%s/iproute", net); if((b = Bopen(path, OREAD)) != nil){ while((p = Brdline(b, '\n')) != nil){ p[Blinelen(b)-1] = '\0'; nf = tokenize(p, 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; } } Bterm(b); } 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. * * This is the only cleanup there is. There was once a putback() that * meant to restore the card on the way out, registered with atexit and * threadnotify; both match on the pid that registered them, and that * proc exits as soon as the server is posted, so neither could ever * run. Nothing puts the card back - see fw(8) and todo.md, not a * function that reads as though it does. */ static void reclaim(char *net, char *addr) { char path[128], *lines[2], *f[8]; Biobuf *b; int i, fd, nf; for(i = 0; i < 16; i++){ snprint(path, sizeof path, "%s/ipifc/%d/status", net, i); if((b = Bopen(path, OREAD)) == nil) continue; nf = rdline(b, lines, 2); Bterm(b); if(nf != 2){ freelines(lines, 2); continue; } nf = tokenize(lines[0], f, nelem(f)); if(nf < 2 || strncmp(f[1], "pkt", 3) != 0){ freelines(lines, 2); continue; } nf = tokenize(lines[1], f, nelem(f)); if(nf < 1 || strcmp(f[0], addr) != 0){ freelines(lines, 2); continue; } freelines(lines, 2); 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], *lines[1], *f[8]; Biobuf *b; int i, fd, nf, found; found = 0; for(i = 0; i < 16; i++){ snprint(path, sizeof path, "%s/ipifc/%d/status", net, i); if((b = Bopen(path, OREAD)) == nil) continue; nf = rdline(b, lines, 1); Bterm(b); if(nf != 1) continue; /* * "device maxtu ...". The name, not the rest of the * status: an address that happened to contain the device's * name used to match it. */ nf = tokenize(lines[0], f, nelem(f)); if(nf < 2 || strstr(f[1], dev) == nil){ freelines(lines, 1); continue; } freelines(lines, 1); 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); fmtinstall('M', eipfmt); /* rulestext prints masks with it */ 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 : ""); } 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); 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); }