/* * 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 #include #include #include #include <9p.h> #include #include #include #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); } /* * What is served, named; not what is hidden, listed. * * Listing what to hide has now been wrong twice. "trans" installs * kernel address translations and devip gates it with iseve(), which is * fw's identity here and not the caller's, so a filtered program could * write it and reach the whole machine. "log" is a trace of every * connection on the machine, and only its write was refused, when * reading it was the leak. Neither was a hard problem: both were * simply missing from a list of things to deny. * * So it is the other way round. Nothing is served unless it is named * below, and the next file devip grows is invisible until someone * decides otherwise - which is the direction a firewall should fail in, * and the same argument the rule parser makes about attributes it does * not recognise. */ /* * devip's protocol directories, by name. Not "any directory with a * clone file": devether has one of those too, and #l bound into /net * would have become a protocol. A protocol missing from this list is * one nobody can reach, which is the safe way to be out of date. * ipmux is left out on purpose - it is raw packet access. */ static char *protos[] = { "tcp", "udp", "il", "icmp", "icmpv6", "rudp", "gre", "esp", "ipifc", /* read-only: see writable() */ nil, }; /* the files a conversation directory has */ static char *convfiles[] = { "ctl", "data", "err", "listen", "local", "remote", "status", nil, }; /* of those, the ones a filtered program may write */ static char *rwconvfiles[] = { "ctl", "data", "err", "listen", nil, }; /* everything else served from the root, and whether writing it is allowed */ static struct { char *name; int rw; } rootfiles[] = { { "cs", 1 }, /* asking for a translation is a write */ { "dns", 1 }, { "arp", 0 }, { "bootp", 0 }, { "iproute", 0 }, { "ipselftab", 0 }, { "ndb", 0 }, }; static int inlist(char **l, char *s) { int i; for(i = 0; l[i] != nil; i++) if(strcmp(l[i], s) == 0) return 1; return 0; } static int rootfile(char *name) { int i; for(i = 0; i < nelem(rootfiles); i++) if(strcmp(rootfiles[i].name, name) == 0) return i; return -1; } static int isnum(char *s) { if(*s == '\0') return 0; for(; *s != '\0'; s++) if(*s < '0' || *s > '9') return 0; return 1; } /* * Split a path into its components, of which a served path has at most * three. A deeper path, an over-long one, or one with an empty * component is not a path this server ever handed out, so it is not one * it will honour: that also disposes of names like "tcp/../.." arriving * as a single walk element from a client speaking 9P directly. */ static int splitpath(char *path, char *buf, int nbuf, char **f) { int i, n; if(strlen(path) >= nbuf) return -1; strcpy(buf, path); n = getfields(buf, f, 4, 0, "/"); if(n < 1 || n > 3) return -1; for(i = 0; i < n; i++) if(*f[i] == '\0') return -1; return n; } /* * A protocol this kernel actually has. The name list says which ones * may be served; the clone file says which ones are there. */ static int isproto(char *name) { char *p; int ok; if(!inlist(protos, name)) return 0; p = smprint("%s/%s/clone", orig, name); ok = access(p, AEXIST) == 0; free(p); return ok; } static int served(char *path) { char buf[128], *f[4]; int n; if(*path == '\0') return 1; /* the root */ if((n = splitpath(path, buf, sizeof buf, f)) < 0) return 0; if(!isproto(f[0])) return n == 1 && rootfile(f[0]) >= 0; if(n == 1) return 1; /* the protocol directory */ if(isnum(f[1])){ if(n == 2) return 1; /* a conversation directory */ /* * An interface's data and snoop files are a wire. On a * machine also running fw -e one of them is *the* wire, so * either hands a filtered program the packet stream the * filter exists to control. */ if(strcmp(f[0], "ipifc") == 0) if(strcmp(f[2], "data") == 0 || strcmp(f[2], "snoop") == 0) return 0; return inlist(convfiles, f[2]); } if(n != 2) return 0; if(strcmp(f[0], "ipifc") == 0) return strcmp(f[1], "stats") == 0; /* no new interfaces */ return strcmp(f[1], "clone") == 0 || strcmp(f[1], "stats") == 0; } static int writable(char *path) { char buf[128], *f[4]; int n, i; if((n = splitpath(path, buf, sizeof buf, f)) < 0) return 0; if(!isproto(f[0])){ if(n != 1 || (i = rootfile(f[0])) < 0) return 0; return rootfiles[i].rw; } if(strcmp(f[0], "ipifc") == 0) return 0; /* the interfaces are not ours to change */ if(n == 2) return strcmp(f[1], "clone") == 0; if(n == 3 && isnum(f[1])) return inlist(rwconvfiles, f[2]); return 0; } static char* protect(char *path, int mode) { if(!served(path)) return "fw: does not exist"; if((mode & 3) != OREAD && !writable(path)) return "fw: read-only under fw"; return nil; } /* * 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); } /* * Control messages that only change how this one conversation behaves, * and so cannot reach the network by themselves. Everything outside * this list is refused: see checkctl. */ static char *okverbs[] = { "bind", /* the local address; announce is what opens */ "ttl", "tos", "ignoreadvice", "close", "hangup", "keepalive", nil, }; /* * If path is a protocol listen file - "tcp/1/listen" - return the * protocol name. Opening one blocks until somebody connects, and the * fd it yields is the ctl file of the connection that arrived. */ static char* listenproto(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'; if((q = strchr(p+1, '/')) == nil || strcmp(q+1, "listen") != 0) return nil; if(!isproto(buf)) return nil; return estrdup(buf); } /* the address side of "10.9.9.1!1234", read out of local or remote */ static int convaddr(char *proto, int conv, char *which, uchar *ip, int *port) { char path[256], buf[128], *p; int fd, n; snprint(path, sizeof path, "%s/%s/%d/%s", orig, proto, conv, which); if((fd = open(path, OREAD)) < 0) return -1; n = read(fd, buf, sizeof buf - 1); close(fd); if(n <= 0) return -1; buf[n] = '\0'; if((p = strchr(buf, '\n')) != nil) *p = '\0'; if((p = strrchr(buf, '!')) == nil) return -1; *p++ = '\0'; *port = atoi(p); return parseip(ip, buf); } /* * A listen has returned a connection. Ask about the peer, now that * there is one. * * The announce could not be matched against a peer - at that moment * nobody had called - so any rule naming an address was skipped, and * "deny=in ip=..." meant nothing here while meaning something at the * packet layer. A rule that silently does nothing is the failure this * program refuses to accept from a mistyped attribute, and it should * not accept it from itself. * * The handshake has already happened: the kernel answered before listen * returned, and no filter at this altitude can prevent that. What it * can do is refuse the connection to the program and hang it up, which * is the difference between a rule that is late and a rule that is * decorative. */ static char* checklisten(Match *m, char *proto, int fd) { uchar ip[IPaddrlen], lip[IPaddrlen]; char buf[64]; int n, conv, lport; /* * fd is the new conversation's ctl file and its number is what * reading it gives. At offset 0, so the program's own read - the * one listen(2) makes to learn the same number - still sees it. */ if((n = pread(fd, buf, sizeof buf - 1, 0)) <= 0) return nil; buf[n] = '\0'; conv = atoi(buf); memset(m, 0, sizeof *m); m->count = 1; m->verb = Vin; m->proto = proto; m->ip = ip; m->port = -1; m->lport = -1; if(convaddr(proto, conv, "remote", ip, &m->port) < 0) return nil; /* gone already; let it be */ if(convaddr(proto, conv, "local", lip, &lport) >= 0) m->lport = lport; /* the port announced, for an lport rule */ if(matchrule(m)){ if(m->log) syslog(0, "fw", "allow %s listen %I!%d", proto, ip, m->port); return nil; } syslog(0, "fw", "deny %s listen %I!%d: %s", proto, ip, m->port, m->err); fprint(fd, "hangup"); snprint(buf, sizeof buf, "%s", m->err); snprint(m->err, sizeof m->err, "fw: %s", buf); return m->err; } /* * connect takes addr!port with optional trailing fields; announce takes * a bare port, or addr!port with addr often "*". */ static char* checkctl(Match *m, char *proto, char *msg, long n) { char buf[512], dest[128], *f[8], *a[4], *addr; uchar ip[IPaddrlen], mask[IPaddrlen]; int nf, na, i; memset(m, 0, sizeof *m); m->count = 1; m->proto = proto; m->ip = ip; 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) m->verb = Vout; else if(strcmp(f[0], "announce") == 0) m->verb = Vin; else{ /* * Anything else is refused unless it is known to be * harmless. Letting unknown control messages through was * a hole, not a convenience: "headers" on a udp * conversation turns it into one that carries its own * destination, and the write queue is live from the * moment it is cloned, so three writes send a datagram * anywhere with no connect for a rule to match. gre has * "raw" and "forward"; rudp and icmpv6 have "headers" * too. * * This is the same argument the rule parser already makes * about attributes it does not recognise, applied to the * place it was not. */ for(i = 0; okverbs[i] != nil; i++) if(strcmp(f[0], okverbs[i]) == 0) return nil; syslog(0, "fw", "deny %s %s: control message not permitted", proto, f[0]); snprint(m->err, sizeof m->err, "fw: %s: not a permitted control message", f[0]); return m->err; } if(nf < 2) return nil; /* malformed; let the kernel say so */ /* * Keep the address before splitting it: getfields writes over the * separators, so f[1] afterwards is only what precedes the first * one, and the log said "connect 127.0.0.2" for a connection to a * port it never named. */ snprint(dest, sizeof dest, "%s", f[1]); na = getfields(f[1], a, nelem(a), 0, "!"); if(na < 1) return nil; if(na == 1){ addr = "*"; /* announce 17019 */ m->port = atoi(a[0]); }else{ addr = a[0]; m->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(m->verb == Vin){ m->lport = m->port; m->port = -1; m->anyip = 1; }else{ m->lport = -1; m->anyip = strcmp(addr, "*") == 0; if(!m->anyip && parseipandmask(ip, mask, addr, nil) == -1){ syslog(0, "fw", "deny %s %s %s: unparseable address", proto, f[0], dest); snprint(m->err, sizeof m->err, "fw: unparseable address"); return m->err; } } if(!matchrule(m)){ if(m->log) syslog(0, "fw", "deny %s %s %s: %s", proto, f[0], dest, m->err); snprint(buf, sizeof buf, "fw: %s", m->err); snprint(m->err, sizeof m->err, "%s", buf); return m->err; } if(m->log) syslog(0, "fw", "allow %s %s %s", proto, f[0], dest); 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(!served(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(!served(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, *proto; 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; } if((proto = listenproto(f->path)) != nil){ Match m; e = checklisten(&m, proto, f->fd); free(proto); if(e != nil){ close(f->fd); f->fd = -1; respond(r, e); /* m outlives the respond, which packs it */ 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){ Match m; e = checkctl(&m, proto, r->ifcall.data, r->ifcall.count); free(proto); if(e != nil){ respond(r, e); /* m outlives the respond, which packs it */ 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); }