# BST-TIMEOUT: 600 # MEASURED: elapsed=42s, 60 cases (==BST-SUITE, local verify, # # 2026-08-22), so 600 is ~14x. The bound that actually # # governs this suite is not the clock but the harness's # # 2048 MiB/min growth rate, which is why it makes ONE # # front-end run and not four -- see section 4. . "$BST_LIB" # arno-iptables-firewall 2.1.1a at SRCREV a4569b76 (meta-networking # recipes-filter) -- a shell front-end that turns one declarative firewall.conf # into a complete iptables ruleset. Last of the netfilter round, after iptables, # nftables, ebtables, arptables, ipset and ulogd2, and the only one of the seven # that compiles nothing at all: 79 files, every one of them text. # # WHAT "PORTED" MEANS FOR A PACKAGE WITH NO OBJECT CODE, AND WHAT THIS SUITE # THEREFORE ASSERTS. There is no binary to run, no symbol to resolve and no # calling convention to get wrong, so the two questions worth asking are # (1) did the content reach the target intact, and (2) can the target actually # run it. They need different evidence and the second is the one a file check # cannot reach: a payload that is byte-perfect is still dead if the commands the # scripts shell out to are not here. # # section 1 the payload, per file by size, mode and SHA-256, against a # NATIVE x86-64 run of the same do_install(). The package compiles # nothing, so the two are the identical byte stream and an exact # equality is the right assertion rather than a lenient one. # section 2 every installed shell file parses. # section 3 every command the package declares, checked the two ways the # package itself looks for one -- at the absolute path # find_command() expects, and on PATH the way check_command() # falls back. # section 4 the front-end reads its reference configuration, reports on it, # reaches the point where it invokes iptables, and emits rules. # section 5 and cannot apply them, because this kernel has no netfilter. # section 6 arno-fwfilter -- installed, and NOT runnable here: it passes its # whole awk program in one argv element, 19 754 B against an 8 KiB # argv block (brintos/hardwarejs#15). # # ONE FRONT-END RUN, AND THE REASON IS MEASURED. Each invocation parses and # sources ~10 000 lines -- the 6217-line front-end, the 2206-line environment, # the 1317-line firewall.conf and 22 plugins -- and on this arm a process's # memory is not returned when it exits (brintos/linux#44). Two runs ~90 s apart # grew the host guest process 2287 MiB/min and the harness ended the boot at its # 2048 MiB/min bound; a healthy sweep grows ~83. `force-reload` is the mode that # buys the most for one run: sanity_check() -- where the front-end first invokes # iptables -- then config_check(), then a rule-emitting path. `check-conf`, # `stop` and `start` are asserted on the native control instead; section 4 # records that as a skip rather than dropping it silently. # # THE RULESET IS RECORDED, NOT APPLIED, AND THAT IS NOT A CONCESSION. # environment resolves IP4TABLES only when the configuration has not already set # one (`if [ -z "$IP4TABLES" ]`), so binding it to a shell function is the # package's own documented knob and not a patched file. The rules become # comparable text that must match the native control exactly -- a stronger # statement than "iptables was invoked and failed" -- and each costs a shell # function call instead of an exec of a 9.4 MB binary. Section 5 keeps the other # half honest: the same call is put to the real iptables, and the kernel refuses. # # THE ONE FILE THIS SUITE DERIVES is the front-end, from the INSTALLED copy: # CONFIG_FILE is repointed at the reference config (upstream hard-codes # /etc/arno-iptables-firewall/firewall.conf and the rootfs is read-only at # browser runtime), and data/arno-iptables-firewall-rec.sh is inserted # immediately after it sources environment, replacing the two thin wrappers that # exec iptables. environment itself, all 22 plugins and every config file are # used exactly as installed -- and section 1 has already proved that "as # installed" is byte-for-byte the reference. The native control was built by the # same two edits; the only asymmetry is that it also had to point ENV_FILE at # its own tree, a line neither side ever prints. S=arno-iptables-firewall D=$BST_DATA FE=/usr/sbin/arno-iptables-firewall tmp=${TMPDIR:-/tmp} W=$tmp/bst-aif.$$ # find_command()/check_command() reach for /sbin and /usr/sbin by absolute path # and only then fall back to `which`, so the package does not depend on this -- # but section 3's PATH half, and iptables in section 5, would report a tool as # absent rather than present if the runner's PATH omitted the sbin directories. PATH="/usr/sbin:/sbin:$PATH" export PATH require gawk $S/gawk-present require sha256sum $S/sha256sum-present require find $S/find-present if [ -x "$FE" ]; then bst_ok $S/front-end; else bst_notok $S/front-end "$FE is not an executable file" return 2>/dev/null || exit 0 fi # ------------------------------------------------------- 1: the payload, whole # One find over the five install roots -- it sees EXTRA files, which a per-path # stat cannot -- and one sha256sum over the paths the manifest names. Both run # from / so their paths are the manifest's own, and both are single execs: a # `cut` per field here would cost more than the assertion it serves, and $W is a # FILE PREFIX rather than a directory so that not even `mkdir` is spawned. set -- $(gawk '!/^#/ { print $4 }' "$D/$S-manifest.txt") ( cd / && find etc/arno-iptables-firewall usr/share/arno-iptables-firewall \ usr/bin/arno-fwfilter usr/bin/arno-iptables-firewall \ usr/sbin/arno-iptables-firewall \ -type f -printf '%m %s %p\n' ) > "$W.stat" 2>&1 ( cd / && sha256sum "$@" ) > "$W.sha" 2>&1 gawk ' function q(k, v) { printf "%s='"'"'%s'"'"'\n", k, v } ARGIND == 1 { if ($0 ~ /^#/) next; wm[$4] = $1; ws[$4] = $2; wh[$4] = $3; want++; next } ARGIND == 2 { lm[$3] = $1; ls[$3] = $2; live++; bytes += $2 if ($3 ~ /\.CHANGELOG$/) chg++; else scr++ next } ARGIND == 3 { lh[$2] = $1; next } END { for (f in wm) { if (!(f in lm)) { d++; if (!ex) ex = "missing " f } else if (lm[f] != wm[f]) { d++; if (!ex) ex = f " mode " lm[f] ", want " wm[f] } else if (ls[f] != ws[f]) { d++; if (!ex) ex = f " is " ls[f] " bytes, want " ws[f] } else if (lh[f] != wh[f]) { d++; if (!ex) ex = f " sha256 " lh[f] ", want " wh[f] } } for (f in lm) if (!(f in wm)) { d++; if (!ex) ex = "extra " f } q("AIF_WANT", want + 0); q("AIF_LIVE", live + 0); q("AIF_BYTES", bytes + 0) q("AIF_SCRIPTS", scr + 0); q("AIF_CHANGELOGS", chg + 0) q("AIF_DIFFS", d + 0); q("AIF_EX", ex) }' "$D/$S-manifest.txt" "$W.stat" "$W.sha" > "$W.f1" . "$W.f1" # The manifest is the assertion; these are what make a red readable without # reaching for the file -- a short count, a wrong split, a first offender by name. check_eq $S/payload-reference 79 "$AIF_WANT" check_eq $S/payload-files 79 "$AIF_LIVE" check_eq $S/payload-scripts 58 "$AIF_SCRIPTS" check_eq $S/payload-changelogs 21 "$AIF_CHANGELOGS" check_eq $S/payload-bytes 596568 "$AIF_BYTES" check_eq $S/payload-diffs 0 "$AIF_DIFFS" check_eq $S/payload-first-diff "" "$AIF_EX" # do_install() writes a systemd unit and poky's rm_systemd_unitdir deletes it # again, because DISTRO_FEATURES carries no systemd. Pinned as ABSENT rather # than left unmentioned: it is the one file where the recipe's own install list # and what ships disagree, and the day this distro gains systemd the count above # goes to 80 and this case is what says why. if [ -e /lib/systemd/system/arno-iptables-firewall.service ]; then bst_notok $S/systemd-unit-absent "the unit is present; DISTRO_FEATURES gained systemd -- the manifest is now short by one" else bst_ok $S/systemd-unit-absent fi # ------------------------------------------------------ 2: every script parses # All 58 non-CHANGELOG files, in ONE bash: each is wrapped in a function body and # the lot handed to `bash -n`. Per-file would be 58 execs of a shell this size # for an assertion that is normally a constant. When it does go red the wrapper # cannot say WHICH file, so the fallback re-runs per file and stops at the first # -- 58 execs on the one run where they buy something. if command -v bash >/dev/null 2>&1; then gawk '!/^#/ && $4 !~ /\.CHANGELOG$/ { n++; printf "__bstchk_%d() {\n:\n", n f = "/" $4; while ((getline l < f) > 0) print l; close(f); print "\n}" } ' "$D/$S-manifest.txt" > "$W.wrapped" if bash -n "$W.wrapped" 2>"$W.parse"; then bst_ok $S/bash-parse else bad="" for f in $(gawk '!/^#/ && $4 !~ /\.CHANGELOG$/ { print $4 }' "$D/$S-manifest.txt"); do bash -n "/$f" 2>/dev/null || { bad=$f; break; } done bst_notok $S/bash-parse "${bad:-}: $(head -1 "$W.parse")" fi else bst_skip $S/bash-parse "bash is not on this image" fi # ------------------------------------------- 3: what the scripts shell out to # Not a guess. The hard list is bin/arno-iptables-firewall sanity_check()'s own # check_command_error() calls; the soft list is its check_command_warning()s plus # the commands individual plugins check for and disable themselves without. # `getopt` is on neither list and belongs on the hard one: arno-fwfilter parses # its options with it, before anything else it does. All of these are shell # builtins here -- `command -v` forks nothing. for c in iptables ip6tables awk tr ip cut uname sed cat date modprobe sysctl \ head tail wc logger which getopt sh; do if command -v "$c" >/dev/null 2>&1; then bst_ok "$S/cmd-$c" else bst_notok "$S/cmd-$c" "declared by the package and not on this image"; fi done # The soft half: a warning or a self-disabling plugin, never a stopped firewall. # An absent one is a `skip` because that is what it is -- the tool is legitimately # not on this image, and the package says so itself rather than failing. for c in pgrep pkill dig nslookup ipset tc start-stop-daemon sort uniq rpcinfo; do if command -v "$c" >/dev/null 2>&1; then bst_ok "$S/optcmd-$c" else bst_skip "$S/optcmd-$c" "optional; the package warns or disables the plugin that wants it"; fi done # find_command() takes the FIRST of these that is executable and only then falls # back to `which`, so a tool that is on PATH at some other place still leaves the # package resolving it the slow way. These are the paths it actually names. for p in /usr/sbin/iptables /usr/sbin/ip6tables /usr/sbin/iptables-save \ /usr/sbin/iptables-restore /sbin/ip /sbin/tc /sbin/sysctl \ /sbin/modprobe /usr/bin/dig; do if [ -x "$p" ]; then bst_ok "$S/path-$p" else bst_notok "$S/path-$p" "find_command() names this path and nothing is executable there"; fi done # ---------------------------------------- 4: it reads a config and emits rules # force-reload is sanity_check() + config_check() + setup_hostblock_chain() + # show_applied(): the version handshake with iptables, the configuration parse, # and two rules through the same ip4tables() the whole ruleset would use. sed -e "s|^CONFIG_FILE=.*|CONFIG_FILE=$D/$S-ref.conf|" \ -e "/^\. \"\$ENV_FILE\"\$/r $D/$S-rec.sh" \ "$FE" > "$W.aif" AIF_TRACE="$W.trace" sh "$W.aif" force-reload > "$W.out" 2>&1 check_eq $S/force-reload-rc 0 "$?" # One pass over both artefacts. The transcript is compared whole against the # control, less the two lines that cannot be the same on two machines: the # `Platform:` line is `uname -s -r -m`, and the closing line carries a date. gawk ' function q(k, v) { printf "%s='"'"'%s'"'"'\n", k, v } ARGIND == 1 { wt[FNR] = $0; wtn = FNR; next } ARGIND == 2 { if ($0 ~ /^Platform: /) { plat = 1; next } if ($0 ~ /^logger: cannot connect/) { nolog = 1; next } sub(/^[A-Z][a-z][a-z] +[0-9]+ [0-9:]+ /, "") n++ if ($0 != wt[n]) { td++; if (!tex) tex = "line " n ": " $0 } next } ARGIND == 3 { r++; live = live $0 ";"; next } END { if (n < wtn) { td += wtn - n; if (!tex) tex = "transcript stops at line " n+0 " of " wtn } q("AIF_TLINES", n + 0); q("AIF_TWANT", wtn + 0) q("AIF_TDIFFS", td + 0); q("AIF_TEX", tex); q("AIF_PLAT", plat + 0) q("AIF_NOLOG", nolog + 0) q("AIF_RULES", r + 0); q("AIF_RULETEXT", live) }' "$D/$S-forcereload.txt" "$W.out" "$W.trace" > "$W.f2" . "$W.f2" # The transcript IS the assertion: it carries the version the front-end read back # from iptables, config_check()'s verdict on the reference configuration, and the # closing line, in order and with their ANSI colouring. check_eq $S/force-reload-transcript-lines 6 "$AIF_TLINES" check_eq $S/force-reload-transcript-diffs 0 "$AIF_TDIFFS" check_eq $S/force-reload-first-diff "" "$AIF_TEX" # `Platform:` is dropped from the comparison, so assert separately that the line # was there -- uname is one of the sixteen commands the package requires, and # sanity_check() printing nothing would otherwise pass unnoticed. check_eq $S/force-reload-platform-line 1 "$AIF_PLAT" # THE ONE LINE THE NATIVE CONTROL DOES NOT PRINT, and it is a property of this # distro rather than of the port. log_msg() pipes through logger(1), which is # installed and required (section 3) and cannot connect, because the image ships # no syslog daemon at all -- the image recipe excludes busybox-syslog outright, # so there is no /dev/log for it to reach. AIF carries on and returns 0, which is # upstream's behaviour for a machine with no syslog. Asserted as PRESENT so that # the day this image gains a syslog socket the case goes red and says the # transcript has moved, rather than the line reappearing unnoticed. check_eq $S/force-reload-logger-unavailable 1 "$AIF_NOLOG" # Two rules, by name. They are HOST_BLOCK_SRC/DST -- setup_hostblock_chain()'s # flush -- and they are the proof that rules reach ip4tables() at all. check_eq $S/force-reload-rules 2 "$AIF_RULES" check_eq $S/force-reload-ruletext \ "-w -F HOST_BLOCK_SRC;-w -F HOST_BLOCK_DST;" "$AIF_RULETEXT" # What is NOT run here, said out loud rather than left to a reader to notice. bst_skip $S/full-ruleset \ "start generates 237 rules (129 -A, 35 -N) at ~250 forks and ~75 execs and exceeded a 2400s bound on this arm; check-conf and stop are a second and third parse of the same 10000 lines. All three are asserted on the native x86-64 control -- see the design doc -- brintos/linux#44" # ---------------------------------------------- 5: and it cannot apply them here # The expected red of this port, asserted as the platform's own behaviour rather # than left to fail: BASE_INPUT_CHAIN is the first chain a full start would # create, and the real iptables cannot create it because wasm32_defconfig has no # CONFIG_NETFILTER (brintos/linux#86 -- https://brintos.io/brintos/linux/issues/86). # The same shell also settles why load_modules() finds nothing: `# CONFIG_MODULES # is not set`, so /proc/modules does not exist and modprobe(1) cannot load a # thing. Both are written as "the operation FAILS", so the day #86 lands this # case goes red and says to come back to this port -- which is why it is pinned. check $S/ruleset-not-applicable sh -c \ '/usr/sbin/iptables -w -N BST_AIF_PROBE 2>/dev/null && { /usr/sbin/iptables -w -X BST_AIF_PROBE >/dev/null 2>&1; exit 1; } /sbin/modprobe ip_tables 2>/dev/null && exit 1 exit 0' # ------------------------------------------------------------- 6: arno-fwfilter # arno-fwfilter is the one part of this package that needs no kernel at all: a # gawk program that turns AIF's syslog lines into readable events, resolving port # and ICMP type/code names from tables of its own. It is ALSO the one part that # cannot run on this image, and the reason is not the port. # # The script holds its whole awk program inline and passes it to gawk as a single # argv element. This guest's argv block is __wasm32_args_buf, 8192 bytes, and it # measured this one at 19 754 -- refused, and the refusal does not come back as an error # the shell can report -- "the exec never returns", so the case would take the # whole boot and every later suite with it (brintos/hardwarejs#15). It is # therefore NOT run. Asserting it by running it is exactly the thing that cannot # be done here, so what is asserted is that the file is installed and executable, # and the behaviour is recorded against the platform issue instead. Measured on # the native x86-64 control, the same three-line capture produces six lines of # decoded output (SSH(22), DNS(53), "Echo request(8,0)"), byte for byte -- the # design doc carries it. if [ -x /usr/bin/arno-fwfilter ]; then bst_ok $S/fwfilter-installed else bst_notok $S/fwfilter-installed "/usr/bin/arno-fwfilter is not an executable file"; fi bst_skip $S/fwfilter-run \ "arno-fwfilter passes its whole awk program as one argv element and the guest measured that block at 19754B against an __wasm32_args_buf of 8192B; the exec never returns, so running it would wedge the boot -- brintos/hardwarejs#15" rm -f "$W".*