summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fw/src/netfs.c223
-rwxr-xr-xfw/test/fwtest.rc68
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
}
@{