summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fw/src/netfs.c121
-rwxr-xr-xfw/test/fwtest.rc58
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.