/* * 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); } /* 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); }