summaryrefslogtreecommitdiff
path: root/fw/test/rawether.c
diff options
context:
space:
mode:
authorCalvin Morrison <calvin@pobox.com>2026-08-19 12:54:29 -0400
committerCalvin Morrison <calvin@pobox.com>2026-08-19 12:54:29 -0400
commit841d1b31f575aef87a65e425fdc87b004c53cf0a (patch)
treefdaa03ec138d980341b5b51d28baa17a05d140af /fw/test/rawether.c
parentd1fc2a8047d722f90d289fafe41be0029c6e39ff (diff)
test: put fw on a wire, with another machine on the other end
Everything in fwtest.rc happens on one machine, which is as far as it goes: it can take a spare card and watch what fw does to the interfaces, but it cannot make a neighbour send anything. Four things were listed as never tested for that reason. Three of them now are. run.sh -gw and -lan join two VMs into a point-to-point segment, so the peer is a plain overlay of the base with an address and nothing else. ARP first, since it is the part with no other explanation: the stack that owns 10.9.9.1 has a pkt interface and no ethernet, so nothing else on that segment can answer for it. The peer's arp table: ether OK 10.9.9.1 52540087c8c1 10.9.9.2 which is the firewall's card. The first ping takes about a second and the rest are sub-millisecond -- fw ARPing for the peer before it can reply, and dropping the first one while it asks, which is the drop no rule caused in fw(8) BUGS, until now only reasoned about. Then filtering as against forwarding, which cannot be seen from inside at all. With allow=in proto=icmp alone, a TCP connect from the peer sits for 290 seconds and times out; prepend a rule through ctl and the same connect is refused in 2. Refused is the far stack's RST, so the packet arrived; timed out is fw dropping it in silence. And frames addressed to somebody else, which is the commit that shipped saying it could not be tested without a second machine. A stack only addresses frames to the mac it resolved, so rawether.c forges one. A promiscuous reader on the firewall machine sees both frames, which had to be confirmed first -- a frame that never arrived looks exactly like one that was filtered, and my first attempt at this drew the wrong conclusion from precisely that. Same rig, same frames, same rules: to fw's card to nobody's no check +3 +3 with the check +2 0 test/wire.md is the procedure, including the two ways I wasted time: fw's control files need -s to be reachable from another shell, and anything that leaves fw holding a pipe waits forever for a program that has already detached. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'fw/test/rawether.c')
-rw-r--r--fw/test/rawether.c97
1 files changed, 97 insertions, 0 deletions
diff --git a/fw/test/rawether.c b/fw/test/rawether.c
new file mode 100644
index 0000000..540d0b6
--- /dev/null
+++ b/fw/test/rawether.c
@@ -0,0 +1,97 @@
+/*
+ * rawether - put one ethernet frame on the wire, addressed as told.
+ *
+ * A stack will only ever address a frame to the mac it resolved, so a
+ * frame addressed to somebody else has to be forged. That is the only
+ * way to ask a firewall on a card whether it is looking at frames that
+ * are not for it.
+ *
+ * rawether /net/ether1 <dstmac> <srcip> <dstip>
+ *
+ * The payload is an icmp echo request, which is enough to be counted.
+ */
+#include <u.h>
+#include <libc.h>
+#include <ip.h>
+
+enum { Ehdrlen = 14, Eminlen = 60, Etip4 = 0x0800 };
+
+static ushort
+csum(uchar *p, int n)
+{
+ ulong s;
+ int i;
+
+ s = 0;
+ for(i = 0; i+1 < n; i += 2)
+ s += (p[i]<<8) | p[i+1];
+ if(i < n)
+ s += p[i]<<8;
+ while(s >> 16)
+ s = (s & 0xFFFF) + (s >> 16);
+ return ~s;
+}
+
+void
+main(int argc, char **argv)
+{
+ uchar f[Eminlen], dst[6], src[6], sip[IPaddrlen], dip[IPaddrlen];
+ char path[128], buf[64];
+ int cfd, dfd, n, conn;
+
+ if(argc != 5){
+ fprint(2, "usage: rawether /net/etherN dstmac srcip dstip\n");
+ exits("usage");
+ }
+ if(parseether(dst, argv[2]) < 0)
+ sysfatal("%s: bad ethernet address", argv[2]);
+ if(parseip(sip, argv[3]) == -1 || parseip(dip, argv[4]) == -1)
+ sysfatal("bad ip address");
+
+ snprint(path, sizeof path, "%s/clone", argv[1]);
+ if((cfd = open(path, ORDWR)) < 0)
+ sysfatal("open %s: %r", path);
+ if((n = read(cfd, buf, sizeof buf - 1)) <= 0)
+ sysfatal("read %s: %r", path);
+ buf[n] = '\0';
+ conn = atoi(buf);
+ if(fprint(cfd, "connect -1") < 0)
+ sysfatal("connect -1: %r");
+ snprint(path, sizeof path, "%s/%d/data", argv[1], conn);
+ if((dfd = open(path, ORDWR)) < 0)
+ sysfatal("open %s: %r", path);
+ snprint(path, sizeof path, "%s/addr", argv[1]);
+ if((n = open(path, OREAD)) < 0)
+ sysfatal("open addr: %r");
+ if(read(n, buf, 12) != 12)
+ sysfatal("read addr: %r");
+ close(n);
+ buf[12] = '\0';
+ if(parseether(src, buf) < 0)
+ sysfatal("unparseable card address %s", buf);
+
+ memset(f, 0, sizeof f);
+ memmove(f, dst, 6);
+ memmove(f+6, src, 6);
+ f[12] = Etip4 >> 8;
+ f[13] = Etip4;
+
+ f[14] = 0x45; /* v4, 20 byte header */
+ hnputs(f+16, 28); /* total length */
+ f[22] = 64; /* ttl */
+ f[23] = 1; /* icmp */
+ memmove(f+26, sip+IPv4off, 4);
+ memmove(f+30, dip+IPv4off, 4);
+ hnputs(f+24, csum(f+14, 20));
+
+ f[34] = 8; /* echo request */
+ hnputs(f+38, 0x1234); /* id */
+ hnputs(f+40, 1); /* seq */
+ hnputs(f+36, csum(f+34, 8));
+
+ if(write(dfd, f, sizeof f) != sizeof f)
+ sysfatal("write: %r");
+ fmtinstall('E', eipfmt);
+ print("sent %d bytes to %E\n", (int)sizeof f, dst);
+ exits(nil);
+}