diff options
Diffstat (limited to 'fw')
| -rw-r--r-- | fw/src/netfs.c | 223 | ||||
| -rwxr-xr-x | fw/test/fwtest.rc | 68 |
2 files changed, 237 insertions, 54 deletions
diff --git a/fw/src/netfs.c b/fw/src/netfs.c index f28485f..4aa1909 100644 --- a/fw/src/netfs.c +++ b/fw/src/netfs.c @@ -95,65 +95,123 @@ childpath(char *dir, char *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] == '/'); -} +/* + * 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. + */ /* - * 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. + * 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 -hidden(char *path) +inlist(char **l, char *s) { - if(strncmp(path, "ether", 5) == 0 && path[5] >= '0' && path[5] <= '9') - return 1; - return under(path, "ipmux"); + int i; + + for(i = 0; l[i] != nil; i++) + if(strcmp(l[i], s) == 0) + return 1; + return 0; } -static char* -protect(char *path, int mode) +static int +rootfile(char *name) { - char *p; + int i; - if(hidden(path)) - return "fw: does not exist"; - if(strcmp(path, "ipifc/clone") == 0) - return "fw: interface creation denied"; + for(i = 0; i < nelem(rootfiles); i++) + if(strcmp(rootfiles[i].name, name) == 0) + return i; + return -1; +} - /* - * An interface's data file is a wire. On a machine that is also - * running fw -e it is *the* wire, so reading it hands a filtered - * program the packet stream the filter exists to control. Not - * read-only: not at all. - */ - if(under(path, "ipifc")) - if((p = strrchr(path, '/')) != nil && strcmp(p+1, "data") == 0) - return "fw: denied"; +static int +isnum(char *s) +{ + if(*s == '\0') + return 0; + for(; *s != '\0'; s++) + if(*s < '0' || *s > '9') + return 0; + return 1; +} - /* - * ndb is the machine's configuration and log is a trace of every - * connection on it, both mode 0666. Writing the first - * reconfigures name service for everyone; reading the second - * after enabling it watches the whole machine. - */ - if(under(path, "ipifc") || under(path, "iproute") || under(path, "arp") - || under(path, "ndb") || under(path, "log")) - if((mode & 3) != OREAD) - return "fw: read-only under fw"; - return nil; +/* + * 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 directory in the root is a protocol directory if it has a clone - * file. Asking the filesystem beats hardcoding a list that goes stale. + * 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) @@ -161,7 +219,7 @@ isproto(char *name) char *p; int ok; - if(strchr(name, '/') != nil || *name == '\0') + if(!inlist(protos, name)) return 0; p = smprint("%s/%s/clone", orig, name); ok = access(p, AEXIST) == 0; @@ -169,6 +227,75 @@ isproto(char *name) 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 @@ -341,7 +468,7 @@ slurpdir(Fnode *f, char *rp) sz = 0; for(i = 0; i < n; i++){ cp = childpath(f->path, d[i].name); - if(hidden(cp)){ + if(!served(cp)){ free(cp); continue; } @@ -428,7 +555,7 @@ fswalk1(Fid *fid, char *name, Qid *q) f = fid->aux; np = childpath(f->path, name); - if(hidden(np)){ + if(!served(np)){ free(np); return "fw: does not exist"; } diff --git a/fw/test/fwtest.rc b/fw/test/fwtest.rc index 0b923e9..a7ba4d2 100755 --- a/fw/test/fwtest.rc +++ b/fw/test/fwtest.rc @@ -104,6 +104,19 @@ fn have { echo no } +# Is it gone from the served tree? +# +# A stat, not a read. Reading /net/log, or an interface's data or snoop +# file, blocks until traffic arrives, so a check that read them would +# hang rather than fail on exactly the build that still serves them - +# and a test that hangs on a regression is worse than no test. +fn gone { + if(test -e $1) + echo there + if not + echo gone +} + # Stop the firewalls a packet check started. # # Take the interfaces away and fw follows: the relay's read fails and @@ -139,7 +152,8 @@ cat > $tmp/empty.ndb <<'!' ! echo '== namespace mode: what is refused is there to refuse' -for(p in /net/udp/clone /net/gre/clone /net/ndb /net/log /net/ipifc/0/data){ +for(p in /net/udp/clone /net/gre/clone /net/ndb /net/log /net/ipifc/0/data \ + /net/ipifc/0/snoop /net/tcp/trans /net/ether0/clone /net/ipmux/clone){ r=`{have $p} check 'the real /net has '^$p yes $"r } @@ -164,13 +178,55 @@ echo '== namespace mode' r=`{wr /net/ndb 'x'} check '/net/ndb is not writable' refused $"r - r=`{wr /net/log 'tcp'} - check '/net/log is not writable' refused $"r - - r=`{rd /net/ipifc/0/data} - check 'an interface data file is not readable' refused $"r + # trans installs a kernel address translation and devip gates it + # with iseve() -- which is fw's identity through here, not the + # caller's, so on a machine where fw runs as eve there was no gate + # at all. Opening it with truncation also flushed the table. + r=`{gone /net/tcp/trans} + check 'a protocol trans file is not served' gone $"r + r=`{wr /net/tcp/trans '10.9.9.9 80 10.9.9.8 1234 10.9.9.7 5678'} + check 'and cannot be written' refused $"r + + # reading log is the leak, not writing it: turn it on elsewhere and + # it traces every connection on the machine + r=`{gone /net/log} + check '/net/log is not served at all' gone $"r + + r=`{gone /net/ipifc/0/data} + check 'an interface data file is a wire, and is not served' gone $"r + r=`{gone /net/ipifc/0/snoop} + check 'nor is its snoop file, which is the same wire' gone $"r r=`{rd /net/ipifc/0/status} check 'but its status still is' ok $"r + + r=`{gone /net/ether0/clone} + check 'a card is not served, clone file or no' gone $"r + r=`{gone /net/ipmux/clone} + check 'nor is ipmux' gone $"r + + # The mount driver splits a path before it sends it, so this only + # asks that the obvious way out is shut; a compound name arriving + # as one walk element needs a client speaking 9P straight to the + # server, and splitpath is what refuses that. + r=`{gone '/net/tcp/../../adm/keys'} + check 'nothing outside /net resolves through it' gone $"r + + # what a program still needs + r=`{wr /net/cs 'tcp!10.9.9.9!80'} + check 'cs still translates' ok $"r + r=`{rd /net/arp} + check 'arp is still readable' ok $"r + r=`{rd /net/ndb} + check 'ndb is still readable' ok $"r + r=`{rd /net/iproute} + check 'iproute is still readable' ok $"r + + r=`{ls -p /net | grep -c '^(log|ipmux|ether0|ether1)$'} + check 'none of them are even listed' 0 $"r + r=`{ls -p /net/tcp | grep -c '^trans$'} + check 'and trans is not listed either' 0 $"r + r=`{ls -p /net/tcp | grep -c '^clone$'} + check 'while clone still is' 1 $"r } @{ |
