# BST-TIMEOUT: 300 # 34 cases, but only 7 of them exec netsniff-ng -- and each # exec re-parses /etc/netsniff-ng/oui.conf, 836 KB of IEEE vendor prefixes that # cost ~16 MB of heap (measured natively, `/usr/bin/time -v`: 17.9 MB max RSS # with the tables, versus a program that is 2.6 MB on disk). Seven of those on a # guest that is short of memory is the expense here, not the assertions. # # netsniff-ng 0.6.8 (meta-networking recipes-support) -- the zero-copy sniffer, # its protocol dissector, and its pcap reader/writer. # # WHAT THIS SUITE ESTABLISHES, AND WHAT IT CANNOT. netsniff-ng has two halves # and only one of them exists on this image. The OFFLINE half -- read a savefile, # dissect it, convert it, filter it -- is fully reachable and is what almost # every case below measures. The LIVE half needs AF_PACKET, and this kernel is # built without it: arch/wasm32/configs/wasm32_defconfig carries # `# CONFIG_PACKET is not set` beside CONFIG_NET/INET/UNIX, which are on. So the # socket layer exists and the packet family deliberately does not, there being no # netdev on the browser model to capture from. # # That is a kernel CONFIG decision, not a defect and not an unwired syscall, so # it is attributed and NOT filed. The live cases below assert the REFUSAL rather # than skipping it: an honest EAFNOSUPPORT is the correct behaviour of a correct # port on this kernel, and pinning it means the day a netdev arrives the case # changes rather than staying quietly unmeasured. # # THE FIXTURE IS BUILT IN THE GUEST, by the awk below, and not shipped in data/. # Three frames chosen so that every table the port installs has to be consulted # to produce the expected text: # # #1 ARP request, who-has 192.168.1.5 tell 192.168.1.1 # #2 IPv4/UDP from port 53, 8 bytes of payload # #3 IPv4/TCP SYN to port 80 # # The IPv4 and TCP checksums are REAL, computed when the fixture was designed, so # `CSum (...) is ok` is a statement about netsniff-ng's checksum verifier rather # than about whether it echoes a field back. # # EVERY PINNED VALUE COMES FROM A NATIVE x86-64 BUILD of netsniff-ng 0.6.8 at the # same SRCREV, reading the same four tables, driven by this same fixture, with # mlockall(2) stubbed to ENOSYS so the reference sees what the target sees. The # two must agree: the savefile format is fixed, the frames are wire format, and # the dissector's arithmetic is integer -- a difference between the two # architectures is a real defect here, not an expected variation. # # THE OUTPUT IS COMPARED WITH ALL WHITESPACE REMOVED, which is not laziness. # tprintf wraps to `ioctl(0, TIOCGWINSZ)`'s answer and falls back to a built-in # width when that fails, and it wraps MID-TOKEN -- the native reference contains # `Proto (0x0800, IPv\n 4)`. So the line breaks are a property of whatever # stdin happened to be, and the only architecture-independent comparison is one # that does not see them. Stdin is redirected from /dev/null below for the same # reason, which pins the fallback width and makes the wrap deterministic even # though nothing here depends on it any more. # # ONE RED IS EXPECTED AND ATTRIBUTED: mlockall(2) is ENOSYS on this target # (syscall 151 is reserved in the wasm32 uapi and has no row in # arch/wasm32/kernel/syscall_table.c, so the dispatcher's NULL check answers # -ENOSYS -- brintos/linux#24). Upstream PANICS on that, which would make every # invocation on this image exit having done nothing, so the recipe patches it to # warn. The case below is left FAILING against the warning rather than asserting # the warning as expected, so it flips green by itself the day 151 is wired -- # nothing here needs changing then. . "$BST_LIB" # netsniff-ng installs to ${sbindir} (upstream's Template puts every tool in # SBINDIR), not ${bindir}. `require` resolves through PATH, so a PATH without # /usr/sbin would report this port as legitimately absent -- a skip that reads as # coverage. PATH=$PATH:/usr/sbin:/sbin export PATH require netsniff-ng netsniff-ng/present # The fixture generator and the comparators are not the subject. Their absence is # an image regression rather than "netsniff-ng is not installed", so it is a red # here and not a skip -- a skip would retire the whole port's coverage silently. for _t in awk sed tr wc cmp; do command -v "$_t" >/dev/null 2>&1 || { bst_notok netsniff-ng/harness-$_t "$_t is not on the image; the suite cannot build or read a fixture" exit 0 } done bst_ok netsniff-ng/harness-present MLOCK="mlockall(2) ENOSYS on this target (brintos/linux#24)" TMP=${TMPDIR:-/tmp} WORK=$TMP/bst-netsniff-ng.$$ rm -rf "$WORK"; mkdir -p "$WORK" 2>/dev/null if [ ! -d "$WORK" ]; then bst_notok netsniff-ng/workdir "cannot create $WORK -- the fixture needs somewhere writable" exit 0 fi bst_ok netsniff-ng/workdir cd "$WORK" || { bst_notok netsniff-ng/workdir-cd "cannot cd $WORK"; exit 0; } ESC=$(printf '\033') # --- the fixture ------------------------------------------------------------- # Frame bytes, wire order, as hex. Written out rather than computed so that what # the dissector is asked about is auditable next to what it is expected to say. # # F1 ff:ff:ff:ff:ff:ff <- 00:11:22:33:44:55, ethertype 0806 # ARP hw=1 proto=0800 hlen=6 plen=4 op=1 (request) # sender 00:11:22:33:44:55 / 192.168.1.1, target 00:00:00:00:00:00 / 192.168.1.5 # F2 00:aa:bb:cc:dd:ee <- 00:11:22:33:44:55, ethertype 0800 # IPv4 tot_len 36 id 0x1234 DF ttl 64 proto 17 csum 0xa53e 192.168.1.1 -> 192.168.1.5 # UDP 53 -> 4096 len 16 csum 0 payload "abcdefgh" # F3 00:11:22:33:44:55 <- 00:aa:bb:cc:dd:ee, ethertype 0800 # IPv4 tot_len 40 id 0x5678 DF ttl 64 proto 6 csum 0x129b 192.168.1.5 -> 8.8.8.8 # TCP 50000 -> 80 seq 0x11223344 SYN win 65535 csum 0xd61e F1=ffffffffffff00112233445508060001080006040001001122334455c0a80101000000000000c0a80105 F2=00aabbccddee001122334455080045000024123440004011a53ec0a80101c0a8010500351000001000006162636465666768 F3=00112233445500aabbccddee080045000028567840004006129bc0a8010508080808c350005011223344000000005002ffffd61e0000 # mkpcap : the 218-byte little-endian microsecond-magic savefile holding # F1, F2 and F3 at 1700000000.000100, 1700000001.000200 and 1700000002.000300. # # No exponentiation and no printf %c: `d = d*256` and \0ddd octal escapes through # printf %b are the forms busybox awk and busybox printf both implement. mkpcap() { _e=$(awk -v f1="$F1" -v f2="$F2" -v f3="$F3" ' function b(x) { return sprintf("\\0%03o", x % 256) } function le32(v, i, s, d) { s=""; d=1; for (i=0;i<4;i++) { s = s b(int(v/d)); d = d*256 } return s } function le16(v, i, s, d) { s=""; d=1; for (i=0;i<2;i++) { s = s b(int(v/d)); d = d*256 } return s } function hx(ch) { return index("0123456789abcdef", ch) - 1 } function raw(h, i, s) { s=""; for (i=1;i<=length(h);i+=2) s = s b(hx(substr(h,i,1))*16 + hx(substr(h,i+1,1))); return s } function rec(sec, usec, h, n) { n = length(h)/2 return le32(sec) le32(usec) le32(n) le32(n) raw(h) } BEGIN { # magic 0xa1b2c3d4, version 2.4, thiszone 0, sigfigs 0, snaplen 65535, # network 1 (DLT_EN10MB) printf "%s", le32(2712847316) le16(2) le16(4) le32(0) le32(0) le32(65535) le32(1) \ rec(1700000000, 100, f1) \ rec(1700000001, 200, f2) \ rec(1700000002, 300, f3) }') || return 1 printf '%b' "$_e" > "$1" } mkpcap cap.pcap # NULL CONTROLS ON THE FIXTURE ITSELF. Everything below reads netsniff-ng against # these bytes, so a generator that emitted the wrong thing would show up as a # dissector defect. These three cases are what separate the two. check_eq netsniff-ng/fixture-size 218 "$(wc -c < cap.pcap | tr -d ' ')" check_eq netsniff-ng/fixture-magic d4c3b2a1 \ "$(dd if=cap.pcap bs=1 count=4 2>/dev/null | od -An -tx1 | tr -d ' \n')" check_eq netsniff-ng/fixture-frame1 "$F1" \ "$(dd if=cap.pcap bs=1 skip=40 count=42 2>/dev/null | od -An -tx1 | tr -d ' \n')" # flat : stdin with the dissector's colour escapes and ALL whitespace removed. # See the header -- tprintf wraps mid-token at a terminal-derived width, so this # is the only comparison that means the same thing on two architectures. # [:space:] rather than ' \t\n': the dissector's line handling emits CARRIAGE # RETURNS -- four of them in this fixture's transcript -- and a three-character # delete list silently leaves them in, so a comparison that looks character-for- # character identical fails by one byte at a segment boundary. Measured against # the native reference before this was [:space:]. flat() { sed "s/${ESC}\[[0-9;]*m//g" | tr -d '[:space:]'; } # --- the program ------------------------------------------------------------- check netsniff-ng/installed-in-sbindir test -x /usr/sbin/netsniff-ng v=$(netsniff-ng --version &1); vrc=$? if bst_execfail "$v"; then bst_unrun netsniff-ng/version "$v" else # UPSTREAM BEHAVIOUR, PINNED AS OBSERVED: --version prints and then calls # die(), so it exits 1. Reproduced identically on the native x86-64 build, so # there is nothing here for brintOS to fix and nothing to attribute -- but a # port that started exiting 0 would mean something changed, so it is asserted # rather than ignored. check_eq netsniff-ng/version-rc 1 "$vrc" # The build stamps a git describe into the version, so the suffix is not # pinned -- only that this is 0.6.8 and that it is the sniffer. case "$(printf '%s\n' "$v" | flat)" in netsniff-ng0.6.8*) bst_ok netsniff-ng/version ;; *) bst_notok netsniff-ng/version "$(_bst_1line "$v")" ;; esac case "$v" in *"the packet sniffing beast"*) bst_ok netsniff-ng/version-banner ;; *) bst_notok netsniff-ng/version-banner "$(_bst_1line "$v")" ;; esac fi # --- the mlockall gap, pinned ------------------------------------------------ # The recipe patches xlockme() from a panic to a warning, so this is what a # working port looks like today. A CLEAN stderr here is the statement that # syscall 151 got wired; that is what the case is for, so it stays red until it # is, and nothing in this file needs editing on the day it flips. netsniff-ng --in cap.pcap diss.out 2>diss.err drc=$? derr=$(cat diss.err) if [ -z "$derr" ]; then bst_ok netsniff-ng/no-mlockall-warning else bst_notok netsniff-ng/no-mlockall-warning "$MLOCK -- $(_bst_1line "$derr")" fi # ...and the other half of that: whatever stderr says, the decode must still have # run. This is the case the patch exists for -- unpatched, rc is 1 and diss.out # is empty. check_eq netsniff-ng/decode-rc 0 "$drc" # --- the dissection ---------------------------------------------------------- D=$(flat < diss.out) # Per-packet compares against the native x86-64 reference. Three of them rather # than one whole-stream compare so that a red names the packet whose dissection # changed instead of handing back 1168 characters. P1='ff:ff:ff:ff:ff:ff),Proto(0x0806,ARP)][Vendor(CIMSYSInc=>Broadcast)][ARPFormatHA(1=>Ethernet),FormatProto(0x0800=>IPv4),HALen(6)ProtoLen(4),Opcode(1=>ARPrequest),SenderMAC(00:11:22:33:44:55)SenderIP(192.168.1.1),TargetMAC(00:00:00:00:00:00),TargetIP(192.168.1.5)]' P2='00:aa:bb:cc:dd:ee),Proto(0x0800,IPv4)][Vendor(CIMSYSInc=>Unknown)][IPv4Addr(192.168.1.1=>192.168.1.5),Proto(17),TTL(64),TOS(0),Ver(4),IHL(5),Tlen(36),ID(4660),Res(0),NoFrag(1),MoreFrag(0)FragOff(0),CSum(0xa53e)isok][UDPPort(53(domain)=>4096),Len(16Bytes,8BytesData),CSum(0x0000)][Chrabcdefgh][Hex6162636465666768]' P3='00:11:22:33:44:55),Proto(0x0800,IPv4)][Vendor(Unknown=>CIMSYSInc)][IPv4Addr(192.168.1.5=>8.8.8.8),Proto(6),TTL(64),TOS(0),Ver(4),IHL(5),Tlen(40),ID(22136),Res(0),NoFrag(1),MoreFrag(0),FragOff(0),CSum(0x129b)isok][TCPPort(50000(iiimsf)=>80(http)),SN(0x11223344),AN(0x0),DataOff(5),Res(0),Flags(SYN),Window(65535),CSum(0xd61e),UrgPtr(0)]' # seg : the flattened stream from one packet header to the next. # Written with ${VAR#...}/${VAR%...} rather than awk so the whole comparison # stays in the shell that already holds $D. p1=${D#*"Broadcast)"*) bst_ok netsniff-ng/table-oui ;; *) bst_notok netsniff-ng/table-oui "oui.conf did not resolve 00:11:22:33 to its vendor" ;; esac case "$D" in *"UDPPort(53(domain)"*) bst_ok netsniff-ng/table-udp ;; *) bst_notok netsniff-ng/table-udp "udp.conf did not resolve port 53 to 'domain'" ;; esac case "$D" in *"=>80(http))"*) bst_ok netsniff-ng/table-tcp ;; *) bst_notok netsniff-ng/table-tcp "tcp.conf did not resolve port 80 to 'http'" ;; esac case "$D" in *"Proto(0x0806,ARP)"*) bst_ok netsniff-ng/table-ether ;; *) bst_notok netsniff-ng/table-ether "ether.conf did not resolve 0x0806 to 'ARP'" ;; esac # The checksum VERIFIER, which is the one part of the dissection that is a # computation rather than a field echo. Both frames carry a correct checksum, so # both must read `is ok`. case "$D" in *"CSum(0xa53e)isok"*) bst_ok netsniff-ng/csum-ipv4-udp ;; *) bst_notok netsniff-ng/csum-ipv4-udp "IPv4 checksum of frame 2 not verified ok" ;; esac case "$D" in *"CSum(0x129b)isok"*) bst_ok netsniff-ng/csum-ipv4-tcp ;; *) bst_notok netsniff-ng/csum-ipv4-tcp "IPv4 checksum of frame 3 not verified ok" ;; esac # NULL CONTROL for the checksum verifier: it must be able to say no. Without # this, `is ok` is equally consistent with a dissector that never checks. # Offset 120 is frame 2's IPv4 TTL: 24-byte global header, 16-byte packet # header, 42-byte frame 1, 16-byte packet header, then 14 bytes of Ethernet and # 8 bytes into the IPv4 header. Changing a header byte WITHOUT changing the # stored checksum is what makes the checksum wrong -- an earlier draft flipped # offset 64, which is inside frame 1's ARP payload and carries no checksum at # all, so the control passed while proving nothing. cp cap.pcap bad.pcap printf '%b' '\0377' | dd of=bad.pcap bs=1 seek=120 count=1 conv=notrunc 2>/dev/null BD=$(netsniff-ng --in bad.pcap /dev/null | flat) case "$BD" in *"isok"*"isok"*) bst_notok netsniff-ng/csum-control-finds-bad "a corrupted IPv4 header still verified ok" ;; *) bst_ok netsniff-ng/csum-control-finds-bad ;; esac # The run summary -- the counters, which are what say the reader reached the end # of the file rather than stopping early with a clean-looking transcript. case "$D" in *"3packetsoutgoing"*) bst_ok netsniff-ng/summary-count ;; *) bst_notok netsniff-ng/summary-count "$(_bst_1line "$D")" ;; esac case "$D" in *"0packetstruncatedinfile"*) bst_ok netsniff-ng/summary-untruncated ;; *) bst_notok netsniff-ng/summary-untruncated "the reader reported truncated packets" ;; esac case "$D" in *"146bytesoutgoing"*) bst_ok netsniff-ng/summary-bytes ;; *) bst_notok netsniff-ng/summary-bytes "expected 42+50+54 = 146 frame bytes" ;; esac # --- pcap -> pcap, byte exact ------------------------------------------------ # THE TICKET'S SECOND NAMED ASSERTION. `--out .pcap` re-writes every packet # through the writer; the output being byte-identical to the input is the # strongest single statement about the reader and the writer together. # # The `.pcap` suffix is load-bearing and not cosmetic: read_pcap() decides what # --out means with `strstr(device_out, ".pcap")`, so a differently-named file # would silently take the trafgen-config branch below instead. netsniff-ng --in cap.pcap --out copy.pcap /dev/null 2>&1 check_eq netsniff-ng/roundtrip-rc 0 "$?" check netsniff-ng/roundtrip-byte-exact cmp -s copy.pcap cap.pcap # NULL CONTROL for the comparator: it must be able to say no. if cmp -s bad.pcap cap.pcap; then bst_notok netsniff-ng/cmp-control-finds-diff "cmp called a one-byte-flipped copy identical" else bst_ok netsniff-ng/cmp-control-finds-diff fi # --- pcap -> trafgen config -------------------------------------------------- # `--out -` is NOT "dissect to stdout", which is the natural reading and the # wrong one: read_pcap() takes the `.pcap` branch only for a name containing # .pcap, and otherwise calls translate_pcap_to_txf() -- so this emits the # capture as a trafgen packet configuration. It also dup()s stdout and closes # fd 1, which is why the dissection does not appear alongside it. netsniff-ng --in cap.pcap --out - txf.out 2>/dev/null check_eq netsniff-ng/txf-rc 0 "$?" T=$(flat < txf.out) # One brace-delimited block per packet, and the first block is frame 1's bytes. check_eq netsniff-ng/txf-blocks 3 "$(tr -cd '{' < txf.out | wc -c | tr -d ' ')" case "$T" in "{0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x11,0x22,0x33,0x44,0x55,0x08,0x06,"*) bst_ok netsniff-ng/txf-frame1 ;; *) bst_notok netsniff-ng/txf-frame1 "$(_bst_1line "$T")" ;; esac # Every byte of every frame is present exactly once: 42+50+54 = 146 `0x` tokens. check_eq netsniff-ng/txf-byte-count 146 \ "$(sed 's/0x/\n0x/g' txf.out | grep -c '^0x')" # --- the BPF filter arm ------------------------------------------------------ # This is what makes libpcap a real dependency rather than a declared one: # HAVE_TCPDUMP_LIKE_FILTER and bpf_comp.o are compiled in only when configure # found libpcap, and without them netsniff-ng answers `No libpcap support, # cannot compile filter` and filters nothing. `tcp` selects frame 3 alone. # stderr is folded in deliberately: `No libpcap support, cannot compile filter` # is written THERE, so a 2>/dev/null capture sees an empty string and the # support case below passes by observing nothing. That is how it read before # this line, on a reference build that genuinely had no libpcap. netsniff-ng --in cap.pcap -f tcp filt.out 2>&1 frc=$? F=$(flat < filt.out) case "$F" in *"Nolibpcapsupport"*) bst_notok netsniff-ng/filter-supported "built without libpcap; -f compiles no filter" ;; *) bst_ok netsniff-ng/filter-supported ;; esac check_eq netsniff-ng/filter-rc 0 "$frc" case "$F" in *"1packetsoutgoing"*) bst_ok netsniff-ng/filter-selects-one ;; *) bst_notok netsniff-ng/filter-selects-one "expected 'tcp' to select frame 3 alone -- $(_bst_1line "$F")" ;; esac case "$F" in *"TCPPort(50000"*) bst_ok netsniff-ng/filter-selects-the-tcp-frame ;; *) bst_notok netsniff-ng/filter-selects-the-tcp-frame "the surviving packet is not the TCP one" ;; esac # --- inputs the reader must refuse ------------------------------------------- # A tool that answered "no packets, no error" to these would pass every case # above and be useless. e1=$(netsniff-ng --in nosuch.pcap &1) case "$e1" in *"Cannot open file"*"No such file or directory"*) bst_ok netsniff-ng/absent-input ;; *) bst_notok netsniff-ng/absent-input "$(_bst_1line "$e1")" ;; esac printf 'this is not a capture file at all, not even close\n' > junk.bin e2=$(netsniff-ng --in junk.bin &1) case "$e2" in *"unsupported pcap magic number"*) bst_ok netsniff-ng/junk-input ;; *) bst_notok netsniff-ng/junk-input "$(_bst_1line "$e2")" ;; esac # --- the live arm: AF_PACKET is not in this kernel --------------------------- # With no -i, device_in defaults to "any", main() takes the LIVE branch, and # pf_socket() -> socket(PF_PACKET, SOCK_RAW, 0) is what has to be refused. # Attributed to the defconfig, not filed: `# CONFIG_PACKET is not set`. e3=$(netsniff-ng &1) case "$e3" in *"Creation of PF socket failed"*) bst_ok netsniff-ng/live-refused ;; *) bst_notok netsniff-ng/live-refused \ "expected PF_PACKET to be refused (CONFIG_PACKET unset) -- $(_bst_1line "$e3")" ;; esac # ...and the boundary of that: a NAMED device that does not exist never reaches # AF_PACKET at all. device_mtu() returns 0 rather than failing, so "eth0" falls # to the offline branch and is opened as a capture file. Pinned because the two # invocations fail for entirely different reasons and only this tells them apart. e4=$(netsniff-ng -i eth0 &1) case "$e4" in *"Cannot open file eth0"*) bst_ok netsniff-ng/named-device-is-read-as-a-file ;; *) bst_notok netsniff-ng/named-device-is-read-as-a-file "$(_bst_1line "$e4")" ;; esac cd "$TMP" 2>/dev/null rm -rf "$WORK"