# BST-TIMEOUT: 240 # 64 cases; measured elapsed=56s on an idle hz1, 2026-08-18. # That is inside the 60 s default by four seconds, which is exactly why the marker # is here: 60-tcpslice.test, the same shape next door, measured 68 s, and a loaded # host multiplies both. The bound has to hold on the host's worst day. # # tcpreplay 4.4.4 (meta-networking recipes-support/tcpreplay) -- the capture # replay suite. Six programs ship: tcpreplay and tcpreplay-edit (replay to a live # interface), tcpbridge (bridge two interfaces), tcpliveplay (replay a TCP flow # to a live host), and the two that are file-to-file -- tcpprep, which classifies # every packet of a capture as client-side or server-side and writes that # decision to a cache file, and tcprewrite, which rewrites layer 2/3/4 fields and # writes a new capture. # # WHAT THIS SUITE ESTABLISHES, AND WHAT IT DELIBERATELY DOES NOT. tcpprep and # tcprewrite are the whole of the reachable surface on this target: they open no # interface and need no peer. Everything else in the suite requires injecting a # frame onto a live NIC, and this guest has none -- so the suite drives tcpprep # and tcprewrite exhaustively and attempts injection exactly once, as an # attributed red rather than as a skip. A skip would retire the question; the red # keeps it visible and flips green by itself the day the guest grows an # injectable interface. # # THE FIXTURES ARE BUILT IN THE GUEST by the awk below, not shipped in data/. A # 24-byte little-endian microsecond-magic global header (DLT_EN10MB, snaplen # 65535) followed by fixed-size 76-byte records: a 16-byte packet header and a # 60-byte Ethernet/IPv4/TCP frame (14 + 20 + 20 + 6). Generating them here is # what lets the client/server split below be chosen against known flows instead # of against whatever a shipped capture happened to hold -- and it is the only # way to state the checksum cases at all, because one fixture is generated with # every checksum deliberately WRONG so that --fixcsum has something to repair. # # THE FIXED RECORD SIZE IS LOAD-BEARING. Every header field then sits at a # computable offset, so the assertions read what tcprewrite WROTE rather than # restating what it says it wrote. The whole file is read in ONE `od | awk` pair # that prints one tagged line per packet: an exec is the expensive operation on # this target, and a dd+od pair per field would be ~40 execs per file. # # THE CHECKSUMS ARE VERIFIED, NOT RECOMPUTED. facts() folds the 16-bit words of # the IPv4 header -- and of the TCP pseudo-header, header and payload -- and # checks the result is 0xffff, which is the defining property of a correct # Internet checksum. That is an independent test rather than a second # implementation of the same arithmetic: an assertion that recomputed the value # the way tcprewrite does would agree with tcprewrite however wrong both were. # # EVERY PINNED VALUE WAS MEASURED ON A NATIVE x86-64 BUILD of tcpreplay 4.4.4 -- # the same tarball, with meta-networking's seven CVE patches applied -- against # libpcap 1.10.4, driven by this same shell, before the target ever ran it. The # savefile format is fixed, the frames are wire format and the arithmetic is # integer, so a difference between the two architectures is a real defect rather # than expected variation. # # THE CLIENT/SERVER SPLIT IS 7/5 AND THAT ASYMMETRY IS DELIBERATE. capA holds # three TCP flows whose client sides send 3, 2 and 2 packets and whose server # sides send 2, 1 and 2. A suite pinning an even split would pass just as well # with primary and secondary transposed. . "$BST_LIB" require tcpprep tcpreplay/present # The fixture generator and the byte reader are not the subject. Their absence is # an image regression rather than "tcpreplay 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 od wc; do command -v "$_t" >/dev/null 2>&1 || { bst_notok tcpreplay/harness-$_t "$_t is not on the image; the suite cannot build or read a fixture" exit 0 } done bst_ok tcpreplay/harness-present TMP=${TMPDIR:-/tmp} WORK=$TMP/bst-tcpreplay.$$ mkdir -p "$WORK" 2>/dev/null if [ ! -d "$WORK" ]; then bst_notok tcpreplay/workdir "cannot create $WORK -- the fixtures need somewhere writable" exit 0 fi bst_ok tcpreplay/workdir cd "$WORK" || { bst_notok tcpreplay/workdir-cd "cannot cd $WORK"; exit 0; } # --- fixture generator ------------------------------------------------------- # mkcap ... spec entry: # ........ # src/dst are the last octet of 10.0.0.X; "bad" decrements both checksums by one # so the fixture is a capture tcprewrite --fixcsum has to repair. # 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. mkcap() { _o=$1; shift _e=$(awk -v spec="$*" ' 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 be16(v) { return b(int(v/256)) b(v) } # csum16(n): the Internet checksum of the n bytes held in w[]. function csum16(n, i, s, hi) { s = 0 for (i = 0; i < n; i += 2) s += w[i] * 256 + w[i+1] while (int(s / 65536) > 0) { hi = int(s / 65536); s = s % 65536 + hi } return 65535 - s } function macs(str, i, m, p, r) { r=""; m = split(str, p, "."); for (i=1;i<=m;i++) r = r b(p[i]+0); return r } BEGIN { CLIENT = "0.17.34.51.68.85" # 00:11:22:33:44:55 SERVER = "102.119.136.153.170.187" # 66:77:88:99:aa:bb out = le32(2712847316) le16(2) le16(4) le32(0) le32(0) le32(65535) le32(1) n = split(spec, a, " ") for (k = 1; k <= n; k++) { split(a[k], p, ".") sec = p[1]+0; usec = p[2]+0; id = p[3]+0 sip = 167772160 + (p[4]+0); dip = 167772160 + (p[5]+0) sport = p[6]+0; dport = p[7]+0; fl = p[8]+0; mode = p[9] # A frame from an ephemeral port is the client side of its flow. if (sport >= 1024) eth = macs(SERVER) macs(CLIENT) else eth = macs(CLIENT) macs(SERVER) eth = eth be16(2048) w[0]=69; w[1]=0; w[2]=0; w[3]=46; w[4]=int(id/256); w[5]=id%256 w[6]=64; w[7]=0; w[8]=64; w[9]=6; w[10]=0; w[11]=0 w[12]=int(sip/16777216)%256; w[13]=int(sip/65536)%256; w[14]=int(sip/256)%256; w[15]=sip%256 w[16]=int(dip/16777216)%256; w[17]=int(dip/65536)%256; w[18]=int(dip/256)%256; w[19]=dip%256 c = csum16(20) if (mode == "bad") c = (c == 0) ? 65535 : c - 1 w[10]=int(c/256); w[11]=c%256 ip = "" for (i=0;i<20;i++) ip = ip b(w[i]) t[0]=int(sport/256); t[1]=sport%256; t[2]=int(dport/256); t[3]=dport%256 t[4]=0; t[5]=0; t[6]=int(id/256); t[7]=id%256 t[8]=0; t[9]=0; t[10]=0; t[11]=0 t[12]=80; t[13]=fl; t[14]=32; t[15]=0 t[16]=0; t[17]=0; t[18]=0; t[19]=0 # TCP checksum: pseudo-header (src, dst, 0, proto, tcp length) + segment. w[0]=int(sip/16777216)%256; w[1]=int(sip/65536)%256; w[2]=int(sip/256)%256; w[3]=sip%256 w[4]=int(dip/16777216)%256; w[5]=int(dip/65536)%256; w[6]=int(dip/256)%256; w[7]=dip%256 w[8]=0; w[9]=6; w[10]=0; w[11]=26 for (i=0;i<20;i++) w[12+i] = t[i] w[32]=id%256; w[33]=int(id/256)%256; w[34]=int(id/65536)%256; w[35]=int(id/16777216)%256 w[36]=170; w[37]=187 tc = csum16(38) if (mode == "bad") tc = (tc == 0) ? 65535 : tc - 1 t[16]=int(tc/256); t[17]=tc%256 tcp = "" for (i=0;i<20;i++) tcp = tcp b(t[i]) out = out le32(sec) le32(usec) le32(60) le32(60) eth ip tcp le32(id) b(170) b(187) } printf "%s", out }') || return 1 printf '%b' "$_e" > "$_o" } # --- the byte reader --------------------------------------------------------- # facts : one `NPKT`/`GHDR` line and one `P ...` line per packet, read # straight out of the file. One od + one awk, whatever the packet count. facts() { od -An -tx1 -v "$1" 2>/dev/null | awk ' # Two hex digits to a number, written out rather than via strtonum(), which # busybox awk -- the awk this image resolves first -- does not implement. function h2d(s, i, c, r) { r = 0 for (i = 1; i <= length(s); i++) { c = index("0123456789abcdef", substr(s, i, 1)); r = r * 16 + (c - 1) } return r } function hx(a, b, i, s) { s=""; for (i=a;i<=b;i++) s = s v[i]; return s } function u8(i) { return h2d(v[i]) } function be16v(i) { return h2d(v[i]) * 256 + h2d(v[i+1]) } function le32v(i) { return h2d(v[i+3])*16777216 + h2d(v[i+2])*65536 + h2d(v[i+1])*256 + h2d(v[i]) } function ip(i) { return h2d(v[i]) "." h2d(v[i+1]) "." h2d(v[i+2]) "." h2d(v[i+3]) } # A header verifies when the folded sum of its 16-bit words -- the checksum # field included -- is 0xffff. Verifying is not recomputing: an assertion # that recomputed the value would agree with a wrong writer. function fold(s, hi) { while (int(s/65536) > 0) { hi = int(s/65536); s = s % 65536 + hi } return s } { for (i = 1; i <= NF; i++) v[n++] = $i } END { printf "NPKT %d\n", int((n - 24) / 76) printf "GHDR %s\n", hx(0, 23) for (k = 0; k < int((n - 24) / 76); k++) { o = 24 + k * 76; f = o + 16; p = f + 14; t = p + 20 s = 0; for (i = 0; i < 20; i += 2) s += be16v(p + i) ipok = (fold(s) == 65535) ? "ok" : "bad" s = be16v(p+12) + be16v(p+14) + be16v(p+16) + be16v(p+18) + u8(p+9) + 26 for (i = 0; i < 26; i += 2) s += be16v(t + i) tcpok = (fold(s) == 65535) ? "ok" : "bad" printf "P%d TS %d.%06d DMAC %s SMAC %s ETH %s SIP %s DIP %s SPORT %d DPORT %d FLAGS %d ID %d IPCSUM %s TCPCSUM %s\n", \ k+1, le32v(o), le32v(o+4), hx(f,f+5), hx(f+6,f+11), hx(f+12,f+13), \ ip(p+12), ip(p+16), be16v(t), be16v(t+2), u8(t+13), le32v(t+20), ipok, tcpok } }' } # fact : the value following on the NPKT/GHDR lines. fact() { printf '%s\n' "$1" | awk -v k="$2" '$1==k{print $2; exit}'; } # col : the value of for every packet, space separated -- so # one case can assert a whole column instead of twelve cases asserting a cell. col() { printf '%s\n' "$1" | awk -v k="$2" '/^P[0-9]/{for(i=2;i&1) if bst_execfail "$_v"; then bst_unrun tcpreplay/version-$_p "$_v" else check_eq tcpreplay/version-$_p "$_p version: 4.4.4 (build git:v4.4.4)" \ "$(printf '%s\n' "$_v" | awk -v p="$_p" '$1==p && $2=="version:"{print; exit}')" fi done # tcpreplay names the cache-file version it supports; the value is what tcpprep # writes into the header asserted below, so the two have to agree. check_out tcpreplay/cache-version-supported "Cache file supported: 04" tcpreplay --version # --- tcpprep: classify the capture ------------------------------------------ pout=$(tcpprep --auto=first --pcap=capA.pcap --cachefile=capA.cache 2>&1); prc=$? if bst_execfail "$pout"; then bst_unrun tcpreplay/tcpprep-first "$pout" else check_eq tcpreplay/tcpprep-first-rc 0 "$prc" check tcpreplay/tcpprep-cache-created test -s capA.cache # The cache is a binary file whose first eight bytes are the magic and whose # next three are the format version -- the same "04" tcpreplay reports above. check_eq tcpreplay/tcpprep-cache-magic "tcpprep" \ "$(od -An -c -N 7 capA.cache 2>/dev/null | tr -d ' \n')" check_eq tcpreplay/tcpprep-cache-version "04" \ "$(od -An -c -j 8 -N 2 capA.cache 2>/dev/null | tr -d ' \n')" fi # The counts the ticket asks for, read back from the cache tcpprep wrote. sout=$(tcpprep --print-stats capA.cache 2>&1) if bst_execfail "$sout"; then bst_unrun tcpreplay/tcpprep-stats "$sout" else check_eq tcpreplay/tcpprep-primary 7 "$(printf '%s\n' "$sout" | awk '/^Primary packets:/{print $NF; exit}')" check_eq tcpreplay/tcpprep-secondary 5 "$(printf '%s\n' "$sout" | awk '/^Secondary packets:/{print $NF; exit}')" check_eq tcpreplay/tcpprep-skipped 0 "$(printf '%s\n' "$sout" | awk '/^Skipped packets:/{print $NF; exit}')" check_eq tcpreplay/tcpprep-total 12 "$(printf '%s\n' "$sout" | awk '/^Total packets:/{print $NF; exit}')" fi # The counts alone would pass with the two classes transposed, or with the right # totals distributed over the wrong packets. This is the per-packet decision. iout=$(tcpprep --print-info capA.cache 2>&1) check_eq tcpreplay/tcpprep-per-packet \ "Primary Secondary Primary Primary Secondary Primary Secondary Primary Primary Secondary Secondary Primary" \ "$(printf '%s\n' "$iout" | awk '/^Packet [0-9]+ -> /{printf "%s%s", (n++?" ":""), $4} END{print ""}')" # --port classifies by the destination port instead of by who spoke first. On # this capture the two must agree; they are independent routes to the same split. tcpprep --port --pcap=capA.pcap --cachefile=capA-port.cache >/dev/null 2>&1 check_eq tcpreplay/tcpprep-port-agrees \ "$(tcpprep --print-info capA.cache 2>/dev/null | awk '/-> /{printf "%s ", $4}')" \ "$(tcpprep --print-info capA-port.cache 2>/dev/null | awk '/-> /{printf "%s ", $4}')" # --auto=router infers a client and a server NETWORK, which three flows over six # addresses cannot support -- it refuses rather than guessing. Pinned because it # is upstream's behaviour on a small sample and not this arm's. rout=$(tcpprep --auto=router --pcap=capA.pcap --cachefile=capA-router.cache 2>&1); rrc=$? check_eq tcpreplay/tcpprep-router-rc 255 "$rrc" case "$rout" in *"unable to build a valid list of servers"*) bst_ok tcpreplay/tcpprep-router-refusal ;; *) bst_notok tcpreplay/tcpprep-router-refusal "$(_bst_1line "$rout")" ;; esac check_rc tcpreplay/tcpprep-missing-input 255 tcpprep --auto=first --pcap=nosuch.pcap --cachefile=nosuch.cache # --- tcprewrite: rewrite the endpoints, the MACs and the checksums ----------- # --endpoints maps every primary packet onto ip1->ip2 and every secondary onto # ip2->ip1, so the packet counts per source address ARE the classification, # applied rather than reported. wout=$(tcprewrite --endpoints=10.9.9.1:10.9.9.2 --cachefile=capA.cache \ --enet-smac=00:aa:bb:cc:dd:01,00:aa:bb:cc:dd:02 \ --enet-dmac=00:aa:bb:cc:dd:02,00:aa:bb:cc:dd:01 \ --infile=capA.pcap --outfile=outA.pcap 2>&1); wrc=$? if bst_execfail "$wout"; then bst_unrun tcpreplay/tcprewrite "$wout" else check_eq tcpreplay/tcprewrite-rc 0 "$wrc" FO=$(facts outA.pcap) check_eq tcpreplay/tcprewrite-npkt 12 "$(fact "$FO" NPKT)" check_eq tcpreplay/tcprewrite-ghdr "$(fact "$FA" GHDR)" "$(fact "$FO" GHDR)" check_eq tcpreplay/tcprewrite-size "$(wc -c < capA.pcap | tr -d ' ')" "$(wc -c < outA.pcap | tr -d ' ')" # The rewrite itself, read out of the output file. check_eq tcpreplay/tcprewrite-sip \ "10.9.9.1 10.9.9.2 10.9.9.1 10.9.9.1 10.9.9.2 10.9.9.1 10.9.9.2 10.9.9.1 10.9.9.1 10.9.9.2 10.9.9.2 10.9.9.1" \ "$(col "$FO" SIP)" check_eq tcpreplay/tcprewrite-dip \ "10.9.9.2 10.9.9.1 10.9.9.2 10.9.9.2 10.9.9.1 10.9.9.2 10.9.9.1 10.9.9.2 10.9.9.2 10.9.9.1 10.9.9.1 10.9.9.2" \ "$(col "$FO" DIP)" check_eq tcpreplay/tcprewrite-smac \ "00aabbccdd01 00aabbccdd02 00aabbccdd01 00aabbccdd01 00aabbccdd02 00aabbccdd01 00aabbccdd02 00aabbccdd01 00aabbccdd01 00aabbccdd02 00aabbccdd02 00aabbccdd01" \ "$(col "$FO" SMAC)" check_eq tcpreplay/tcprewrite-dmac \ "00aabbccdd02 00aabbccdd01 00aabbccdd02 00aabbccdd02 00aabbccdd01 00aabbccdd02 00aabbccdd01 00aabbccdd02 00aabbccdd02 00aabbccdd01 00aabbccdd01 00aabbccdd02" \ "$(col "$FO" DMAC)" # The rewritten packets must still verify, with no --fixcsum asked for: # changing an address invalidates both checksums, so a rewriter that moved the # addresses and left the sums alone would satisfy every case above and emit # twelve corrupt packets. check_eq tcpreplay/tcprewrite-ipcsum "ok ok ok ok ok ok ok ok ok ok ok ok" "$(col "$FO" IPCSUM)" check_eq tcpreplay/tcprewrite-tcpcsum "ok ok ok ok ok ok ok ok ok ok ok ok" "$(col "$FO" TCPCSUM)" # ...and what it must NOT have touched. check_eq tcpreplay/tcprewrite-ids-preserved "$(col "$FA" ID)" "$(col "$FO" ID)" check_eq tcpreplay/tcprewrite-ts-preserved "$(col "$FA" TS)" "$(col "$FO" TS)" check_eq tcpreplay/tcprewrite-sport-preserved "$(col "$FA" SPORT)" "$(col "$FO" SPORT)" check_eq tcpreplay/tcprewrite-dport-preserved "$(col "$FA" DPORT)" "$(col "$FO" DPORT)" check_eq tcpreplay/tcprewrite-flags-preserved "$(col "$FA" FLAGS)" "$(col "$FO" FLAGS)" check_eq tcpreplay/tcprewrite-eth-preserved "$(col "$FA" ETH)" "$(col "$FO" ETH)" fi # Repairing a capture whose checksums are all wrong. The endpoints rewrite above # deliberately does NOT pass --fixcsum, and this block explains why: for IPv4 the # flag cannot be distinguished from its absence. tcpedit.c does # # if (ip_hdr != NULL) { fix_ipv4_length(...); needtorecalc = 1; } # ... # if ((tcpedit->fixcsum || needtorecalc > 0)) { fix_ipv4_checksums(...); } # # -- needtorecalc is set unconditionally for every IPv4 packet, so the checksum # branch always fires and the flag only ever changes the IPv6 path (where it is # |= rather than =). Reproduced identically on the native x86-64 build, so it is # upstream's behaviour at 4.4.4 and not this arm's. Pinning it is the point: the # useful property is that every packet tcprewrite writes verifies, and stating it # through the flag alone would credit the flag for something it does not do. cout=$(tcprewrite --infile=capC.pcap --outfile=outC.pcap 2>&1); crc=$? if bst_execfail "$cout"; then bst_unrun tcpreplay/tcprewrite-copy "$cout" else check_eq tcpreplay/tcprewrite-copy-rc 0 "$crc" FX=$(facts outC.pcap) check_eq tcpreplay/tcprewrite-copy-repairs-ip "ok ok ok" "$(col "$FX" IPCSUM)" check_eq tcpreplay/tcprewrite-copy-repairs-tcp "ok ok ok" "$(col "$FX" TCPCSUM)" # A "repair" that rewrote the addresses too would also read ok above. check_eq tcpreplay/tcprewrite-copy-sip "$(col "$FC" SIP)" "$(col "$FX" SIP)" check_eq tcpreplay/tcprewrite-copy-ids "$(col "$FC" ID)" "$(col "$FX" ID)" # ...and the flag adds nothing to it. A version that made --fixcsum # load-bearing for IPv4 would break this equality, which is what we want to # hear about. tcprewrite --fixcsum --infile=capC.pcap --outfile=outCf.pcap >/dev/null 2>&1 check_eq tcpreplay/tcprewrite-fixcsum-adds-nothing "$FX" "$(facts outCf.pcap)" fi # --endpoints has no meaning without a classification, and says so rather than # defaulting to one. eout=$(tcprewrite --endpoints=10.9.9.1:10.9.9.2 --infile=capA.pcap --outfile=noc.pcap 2>&1); erc=$? check_eq tcpreplay/tcprewrite-endpoints-needs-cache-rc 1 "$erc" case "$eout" in *"endpoints option requires the cachefile option"*) bst_ok tcpreplay/tcprewrite-endpoints-needs-cache ;; *) bst_notok tcpreplay/tcprewrite-endpoints-needs-cache "$(_bst_1line "$eout")" ;; esac # --- tcpcapinfo: the output is a pcap libpcap itself will read --------------- # facts() reads the bytes; this reads them back through the library, so a file # that satisfied the offsets but was not a savefile would still be caught. tout=$(tcpcapinfo outA.pcap 2>&1) if bst_execfail "$tout"; then bst_unrun tcpreplay/tcpcapinfo "$tout" else check_eq tcpreplay/tcpcapinfo-linktype "linktype = 0x00000001" \ "$(printf '%s\n' "$tout" | awk '/^linktype/{print; exit}')" check_eq tcpreplay/tcpcapinfo-snaplen "snaplen = 65535" \ "$(printf '%s\n' "$tout" | awk '/^snaplen/{print; exit}')" check_eq tcpreplay/tcpcapinfo-packets 12 \ "$(printf '%s\n' "$tout" | awk '/^[0-9]+\t/{n++} END{print n+0}')" check_eq tcpreplay/tcpcapinfo-all-ok 12 \ "$(printf '%s\n' "$tout" | awk '/\tOK$/{n++} END{print n+0}')" fi # --- injection: attempted once, and red by design --------------------------- # tcpreplay's whole purpose is to put frames on a NIC, and it cannot do that # here. `lo` exists (60-net.test's net/ip-link-lo asserts it), so this is not a # missing interface: libpcap sends on Linux through PF_PACKET, and this kernel is # built `# CONFIG_PACKET is not set`, so socket(AF_PACKET, SOCK_RAW, ETH_P_ALL) # answers EAFNOSUPPORT. That cause is not asserted here -- it is established by # 79-libnet.test's libnet/plat-af-packet, which makes the syscall directly from a # compiled probe; this case records the consequence for tcpreplay and cites it. # # Left FAILING rather than skipped: a skip would retire the question, and this # flips green by itself the day CONFIG_PACKET is turned on -- nothing here needs # changing then. tcpreplay's own message is carried into the detail because WHICH # refusal it is (interface not found vs. cannot open it) is the finding, and only # the target can say. NOIFACE="no packet injection on this kernel: # CONFIG_PACKET is not set, so AF_PACKET is EAFNOSUPPORT (brintos/linux#81)" jout=$(tcpreplay -i lo capA.pcap 2>&1); jrc=$? if [ "$jrc" -eq 0 ]; then bst_ok tcpreplay/inject else bst_notok tcpreplay/inject "$NOIFACE -- rc=$jrc $(_bst_1line "$jout")" fi cd "$TMP" 2>/dev/null && rm -rf "$WORK"