From e03c1bb3df2e70f5fb707f5c7a7081022ed31d8c Mon Sep 17 00:00:00 2001 From: Calvin Morrison Date: Tue, 18 Aug 2026 23:14:48 -0400 Subject: netfs: check the caller when there is one to check An announce names no peer -- at that moment nobody has called -- so matchrule skipped every rule naming an address, and "deny=in ip=1.2.3.4" did nothing at all in namespace mode while doing something real at the packet layer. The code said so and called it right: A rule naming a peer therefore cannot apply to an announce, which is right: at this point there is no peer to name. Right about the announce, wrong about the connection. A rule that silently does nothing is the failure this program refuses to accept from a mistyped attribute -- fw will not start rather than run with "prot=tcp" ignored -- and it should not accept it from itself. So the peer is asked about at listen time, when there is one. The fd that listen yields is the new conversation's ctl file; its number reads out of it at offset 0, so the program's own read, the one listen(2) makes to learn the same number, still sees it. remote and local give the peer and the port announced. If the rules refuse, the connection is hung up and the open fails, and the program never has it. The handshake has already happened by then: the kernel answered before listen returned, and no filter at this altitude can prevent that. That is the difference between a rule that is late and a rule that is decorative, and it is worth the distinction. Two checks, on the same pair of rule sets, differing only in whether the caller is refused; both fail against the previous netfs.c. They read /sys/log/fw as a difference rather than a total: the caller's port is ephemeral, so nothing in the line belongs to this run, and the log keeps what earlier runs put there. The machine's own address stands in for a peer, since this one has no loopback configured -- announcing 127.0.0.1 gets "not a local IP address", and announcing a bare port binds to :: and never sees a v4 call at all. 66 pass. Co-Authored-By: Claude Opus 5 --- fw/src/netfs.c | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++- fw/test/fwtest.rc | 58 ++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 1 deletion(-) 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 @@ -340,6 +340,113 @@ static char *okverbs[] = { nil, }; +/* + * 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 : 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. -- cgit v1.2.3