summaryrefslogtreecommitdiff
path: root/fw/src/netfs.c
diff options
context:
space:
mode:
Diffstat (limited to 'fw/src/netfs.c')
-rw-r--r--fw/src/netfs.c555
1 files changed, 555 insertions, 0 deletions
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);
+}