diff options
| -rw-r--r-- | fw/src/netfs.c | 121 | ||||
| -rwxr-xr-x | fw/test/fwtest.rc | 58 |
2 files changed, 178 insertions, 1 deletions
diff --git a/fw/src/netfs.c b/fw/src/netfs.c index 1300717..f60d5bc 100644 --- a/fw/src/netfs.c +++ b/fw/src/netfs.c @@ -341,6 +341,113 @@ static char *okverbs[] = { }; /* + * 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 "*". */ @@ -590,7 +697,7 @@ static void fsopen(Req *r) { Fnode *f; - char *rp, *e; + char *rp, *e, *proto; int mode; f = r->fid->aux; @@ -615,6 +722,18 @@ fsopen(Req *r) 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); } diff --git a/fw/test/fwtest.rc b/fw/test/fwtest.rc index 8ae3f42..8a5a085 100755 --- a/fw/test/fwtest.rc +++ b/fw/test/fwtest.rc @@ -266,6 +266,64 @@ echo '== namespace mode' check 'a different port is denied' refused $"r } +echo '== a peer is checked when there is one to check' +# An announce names no peer - nobody has called yet - so a rule naming +# one was skipped, and "deny=in ip=..." did nothing here while doing +# something at the packet layer. Both rule sets below permit the +# announce by port; only the second permits the caller. The machine's +# own address stands in for a peer, since it has no loopback. +myip=`{awk '/4u$/ {print $1}' /net/ipselftab | sed 1q} +if(~ $#myip 0) + echo ' skip no IPv4 address on this machine to call' +if not { + { + echo 'deny=in proto=tcp ip='^$myip^' log=yes' + echo 'allow=in proto=tcp lport='^$port + echo 'allow=out proto=tcp ip='^$myip^' port='^$port + } > $tmp/peerno.ndb + { + echo 'allow=in proto=tcp lport='^$port^' log=yes' + echo 'allow=out proto=tcp ip='^$myip^' port='^$port + } > $tmp/peeryes.ndb + + # listentest <rules>: announce, have somebody call, and let fw + # decide. What it decided is in the log; the announce is logged + # too, so the checks look for "listen" and not for the verb alone. + fn listentest { + @{ + rfork n + $fw $1 >[2]/dev/null + @{ + conv=`{cat /fd/0} + echo -n 'announce '^$myip^'!'^$port >[1=0] + @{ cat /net/tcp/$conv/listen } >/dev/null >[2]/dev/null + } <>[0] /net/tcp/clone >/dev/null & + lpid=$apid + sleep 2 + wr /net/tcp/clone 'connect '^$myip^'!'^$port >/dev/null + sleep 3 + @{ echo kill > /proc/$lpid/note } >[2]/dev/null + } + } + + # differences, not totals: the caller's port is ephemeral, so there + # is nothing in the line that belongs to this run, and /sys/log/fw + # keeps what earlier runs put there + before=`{grep -c 'deny tcp listen '^$myip /sys/log/fw} + listentest $tmp/peerno.ndb + sleep 1 + after=`{grep -c 'deny tcp listen '^$myip /sys/log/fw} + r=`{echo $before $after | awk '{print $2 - $1}'} + check 'a rule naming the caller refuses the connection' 1 $"r + + before=`{grep -c 'allow tcp listen '^$myip /sys/log/fw} + listentest $tmp/peeryes.ndb + sleep 1 + after=`{grep -c 'allow tcp listen '^$myip /sys/log/fw} + r=`{echo $before $after | awk '{print $2 - $1}'} + check 'and one permitting it lets the program have it' 1 $"r +} + echo '== logging and accounting' # /sys/log/fw is fw's only durable output. syslog(2) does not create # it, so make it if it is not there and take it away again if we did. |
