. "$BST_LIB" # BST-TIMEOUT: 600 # rarpd ss981107 (meta-oe recipes-extended) -- Alexey Kuznetsov's userland RARP # daemon. What ships is /usr/sbin/rarpd, the /etc/ethers map this port writes, # the /etc/init.d/rarpd upstream carries, rarpd.8 in -doc, and # /usr/libexec/rarpd-tests/rarpd-ethers-probe from -tests. # # THE WALL IS PF_PACKET, AND IT STANDS BEHIND THE ARGUMENT PARSER. main() runs # the getopt loop, then the `argc > optind+1' arity check, and only THEN reaches # `pset[0].fd = socket(PF_PACKET, SOCK_DGRAM, 0)'. This kernel is built # `# CONFIG_PACKET is not set' (arch/wasm32/configs/wasm32_defconfig) -- # brintos/linux#81 -- so that socket is EAFNOSUPPORT, psize falls to 0, and # rarpd prints `failed to bind any socket. Aborting.' and exits 1. # # That ordering is the whole reason this suite can assert the ticket's # argument-validation half FOR REAL rather than around it: usage text, the # option that requires an argument, the arity refusal and the exit status all # happen before the socket call and are measured here as ordinary behaviour, not # as a wall. Compare ipvsadm, whose ipvs_init() is main()'s first statement and # therefore swallows even --help. # # WHAT THE WALL DOES COST is the discrimination between a KNOWN and an UNKNOWN # interface. With no socket, `pset[0].fd' is -1 and the SIOCGIFINDEX ioctl fails # with EBADF whatever name was typed, so `rarpd lo' and `rarpd nosuchif0' end # identically. The exit status the ticket names is still asserted for real; the # uniformity is asserted as a positive fact of its own, so the day #81 lands and # the two answers diverge, this suite says so rather than going quietly green. # # THE /etc/ethers LOOKUP IS REACHED THROUGH A PROBE, NOT THROUGH rarpd. # rarp_lookup() -- ether_ntohost(3), gethostbyname(3), h_addr_list[0] -- is file # and NSS work that would run perfectly here, but rarpd calls it only from # serve_it(), on a frame read off the socket above. rarpd-ethers-probe calls the # same three functions in the same order with the same arguments, so the map is # established by value. Its expectation is /etc/ethers itself, read below: a # packaging change that dropped this port's map would redden the comparison # rather than quietly reduce the daemon to answering nobody. # # NOTHING IS LEFT RUNNING. rarpd forks only AFTER the socket bind succeeds, so # on this image every invocation below exits on its own before there is a daemon # to leave behind. Every one is nevertheless bounded by `timeout' AND passed -d # wherever it could reach the fork -- a machine that one day HAS AF_PACKET must # report rc=124 here, not spawn something this platform cannot kill # (brintos/linux#21). require /usr/sbin/rarpd rarpd/present RARPD=/usr/sbin/rarpd ETHERS=/etc/ethers PROBE=/usr/libexec/rarpd-tests/rarpd-ethers-probe USAGE="Usage: rarpd [ -dveaA ] [ -b tftpdir ] [ interface]" NOBIND="failed to bind any socket. Aborting." D=${TMPDIR:-/tmp}/bst-rarpd.$$ rm -rf "$D"; mkdir -p "$D" || { bst_notok rarpd/workdir "cannot create $D"; exit 0; } # rb -- run bounded, capturing stdout+stderr; sets $out and $rc. # rarpd writes usage() and the bind diagnostic to stderr and nothing to stdout, # so the two streams are merged and read together. rb() { _t=$1; shift; out=$(timeout "$_t" "$@" 2>&1); rc=$?; } # has has() { case "$2" in *"$1"*) return 0 ;; *) return 1 ;; esac; } # --- inventory: what the package put on the image ----------------------------- check_eq rarpd/inventory "yes" "$([ -x "$RARPD" ] && echo yes)" # The bare name a user types must be this binary; `command -v' reads root's real # PATH, so this also catches an image where /usr/sbin is not on it. check_eq rarpd/on-path "$RARPD" "$(command -v rarpd 2>/dev/null)" check_eq rarpd/man-installed "yes" \ "$([ -f /usr/share/man/man8/rarpd.8 ] && echo yes)" check_eq rarpd/probe-installed "yes" "$([ -x "$PROBE" ] && echo yes)" # /etc/init.d/rarpd ships from upstream unchanged and is INERT on this image for # two independent reasons, both asserted rather than assumed: this distro sets # DISTRO_FEATURES = "" so nothing reads /etc/init.d at boot, and the script's # first act is `. /etc/init.d/functions', which initscripts-functions would have # provided and this image does not install. It is scored as a positive statement # of what is there, not as a defect: a distro with no sysvinit is where the file # is inert, and the port is not the thing that made it so. check_eq rarpd/initscript-installed "yes" \ "$([ -f /etc/init.d/rarpd ] && echo yes)" check_eq rarpd/initscript-is-inert "yes" \ "$([ ! -f /etc/init.d/functions ] && echo yes)" # --- the ethers map: the file ------------------------------------------------ check_eq rarpd/ethers-installed "yes" "$([ -f "$ETHERS" ] && echo yes)" # Upstream's ethers.sample is one comment line and nothing else, which leaves # rarp_lookup() answering nobody. This port replaces it, and that replacement is # the subject of every lookup case below -- so a regression to the empty file # must redden HERE rather than turn the map cases into vacuous passes. ENT=$(sed -e 's/#.*//' "$ETHERS" 2>/dev/null | awk 'NF>=2 && $1 ~ /:/' | wc -l) [ -n "$ENT" ] || ENT=0 if [ "$ENT" -ge 3 ]; then bst_ok rarpd/ethers-not-upstream-sample else bst_notok rarpd/ethers-not-upstream-sample "$ETHERS carries $ENT address lines; upstream's empty sample would leave rarpd with nothing to answer from"; fi # --- the ethers map: the lookup, by value ------------------------------------ # The probe is rarp_lookup() lifted out of the callback the packet socket would # have driven. Every uncommented line of the shipped file is put through it, and # the expectation is READ FROM THE FILE -- so this pair of cases ties the map # rarpd would answer with to the map the image actually ships, in both # directions, without a second pinned copy to drift from it. # PAIRS is `=' per address line, read once and iterated with `for' # so that both loops run in THIS shell -- a pipeline would put them in a subshell # and lose the failure lists they accumulate, and a redirect from the file would # end the suite silently on this root (see the packager README). PAIRS=$(sed -e 's/#.*//' "$ETHERS" 2>/dev/null | awk 'NF>=2 && $1 ~ /:/ {print $1 "=" $2}') if [ ! -x "$PROBE" ]; then bst_notok rarpd/ethers-forward "the probe is not installed, so the lookup rarpd cannot reach was not measured" bst_notok rarpd/ethers-reverse "the probe is not installed" bst_notok rarpd/ethers-unknown "the probe is not installed" bst_notok rarpd/ethers-name-branch "the probe is not installed" elif [ -z "$PAIRS" ]; then bst_notok rarpd/ethers-forward "no address lines were read out of $ETHERS, so nothing was measured" bst_notok rarpd/ethers-reverse "no address lines were read out of $ETHERS" bst_notok rarpd/ethers-unknown "no address lines were read out of $ETHERS" bst_notok rarpd/ethers-name-branch "no address lines were read out of $ETHERS" else # ONE probe invocation for the whole file, both directions. The probe is a # 3 MB wasm module and each exec is a real cost on a guest that is short of # memory to begin with (brintos/linux#44), so the arguments are accumulated # first and the map is measured in a single run. _args=; fwd_bad=; rev_bad= for _p in $PAIRS; do _args="$_args ${_p%%=*} ${_p#*=}" done rb 60 "$PROBE" $_args map_out=$out if bst_execfail "$map_out"; then bst_unrun rarpd/ethers-forward "$map_out" bst_unrun rarpd/ethers-reverse "$map_out" else for _p in $PAIRS; do _mac=${_p%%=*}; _name=${_p#*=} has "R-ntohost $_mac ok $_name" "$map_out" || \ fwd_bad="$fwd_bad $_mac(want $_name)" has "R-hostton $_name ok $_mac" "$map_out" || \ rev_bad="$rev_bad $_name(want $_mac)" done fi if bst_execfail "$map_out"; then : elif [ -n "$fwd_bad" ]; then bst_notok rarpd/ethers-forward "ether_ntohost disagreed with $ETHERS for:$(_bst_1line "$fwd_bad")" else bst_ok rarpd/ethers-forward fi # reverse: ether_hostton on each name must return the address it came from. # Asserted separately because rarpd reads the map one way and a file the # database can only half-read is a different fault from one it cannot read. if bst_execfail "$map_out"; then : elif [ -n "$rev_bad" ]; then bst_notok rarpd/ethers-reverse "ether_hostton disagreed with $ETHERS for:$(_bst_1line "$rev_bad")" else bst_ok rarpd/ethers-reverse fi # An address the file does NOT carry must come back `noent'. Without this the # forward case is satisfiable by a lookup that answers everything. rb 30 "$PROBE" 00:00:5e:00:53:fe if has "R-lookup 00:00:5e:00:53:fe noent" "$out"; then bst_ok rarpd/ethers-unknown elif bst_execfail "$out"; then bst_unrun rarpd/ethers-unknown "$out" else bst_notok rarpd/ethers-unknown "a MAC absent from $ETHERS was resolved anyway -- rc=$rc $(_bst_1line "$out")"; fi # The second half of rarp_lookup(): the name ether_ntohost returned is fed to # gethostbyname(3), and the ANSWER rarpd would send is that call's first # address. The file's `localhost' line is the case that exercises the NSS # branch -- a dotted quad comes back from gethostbyname unchanged and would # prove nothing about it, so a name that /etc/hosts resolves is what makes the # difference between "the map was read" and "the address was computed". rb 30 "$PROBE" 00:00:5e:00:53:03 if has "R-lookup 00:00:5e:00:53:03 ok localhost 127.0.0.1" "$out"; then bst_ok rarpd/ethers-name-branch elif has "R-lookup 00:00:5e:00:53:03 noname" "$out"; then bst_notok rarpd/ethers-name-branch "ether_ntohost found the name but gethostbyname could not resolve it, so rarpd would decline to answer this client -- $(_bst_1line "$out")" elif bst_execfail "$out"; then bst_unrun rarpd/ethers-name-branch "$out" else bst_notok rarpd/ethers-name-branch "want the ethers name resolved through NSS to 127.0.0.1 -- rc=$rc $(_bst_1line "$out")"; fi fi # --- argument validation, asserted for real ----------------------------------- # usage() is `fprintf(stderr, ...); exit(1)'. Every case here is reached BEFORE # the socket call, so none of them is behind #81 and none may be excused by it. # An option the optstring does not carry. opterr is 0, so the ONLY diagnostic is # usage()'s own line -- which is why the text is asserted and not merely rc. rb 20 "$RARPD" -Z if [ "$rc" -eq 124 ]; then bst_notok rarpd/reject-unknown-option "rarpd -Z did not return within 20 s (rc=124)" elif [ "$rc" -eq 1 ] && has "$USAGE" "$out"; then bst_ok rarpd/reject-unknown-option elif bst_execfail "$out"; then bst_unrun rarpd/reject-unknown-option "$out" else bst_notok rarpd/reject-unknown-option "want rc=1 and the usage line; got rc=$rc -- $(_bst_1line "$out")"; fi # The usage text must go to STDERR. Asserted apart from the line's content # because a diagnostic on stdout is the difference between a script that can # separate rarpd's complaint from its output and one that cannot. so=$(timeout 20 "$RARPD" -Z 2>/dev/null); so_rc=$? se=$(timeout 20 "$RARPD" -Z 2>&1 >/dev/null) if has "$USAGE" "$se" && [ -z "$so" ]; then bst_ok rarpd/usage-on-stderr else bst_notok rarpd/usage-on-stderr "stdout:'$(_bst_1line "$so")' stderr:'$(_bst_1line "$se")' rc=$so_rc"; fi # THERE IS NO --version, AND THAT IS THE FINDING, NOT AN OMISSION HERE. rarpd # ss981107 has no version option and prints no version string anywhere: the # optstring is "aAb:dvoe" and -v is VERBOSE. glibc's getopt is called with no # longopts, so `--version' is read as the short options `-', `v', `e'... and # stops at `-', which is not in the optstring -- so it lands on usage() like any # other unknown option. The ticket's "version text" is asserted as the exit # status and the usage line a user actually gets, which is the honest subject. rb 20 "$RARPD" --version if [ "$rc" -eq 124 ]; then bst_notok rarpd/no-version-option "rarpd --version did not return within 20 s (rc=124)" elif [ "$rc" -eq 1 ] && has "$USAGE" "$out"; then bst_ok rarpd/no-version-option elif bst_execfail "$out"; then bst_unrun rarpd/no-version-option "$out" else bst_notok rarpd/no-version-option "rarpd ss981107 has no version option; want the usage refusal at rc=1, got rc=$rc -- $(_bst_1line "$out")"; fi # -b takes an argument (`b:'), and a missing one must be refused rather than # silently defaulting the TFTP directory. rb 20 "$RARPD" -b if [ "$rc" -eq 124 ]; then bst_notok rarpd/b-requires-argument "rarpd -b did not return within 20 s (rc=124)" elif [ "$rc" -eq 1 ] && has "$USAGE" "$out"; then bst_ok rarpd/b-requires-argument elif bst_execfail "$out"; then bst_unrun rarpd/b-requires-argument "$out" else bst_notok rarpd/b-requires-argument "-b with no argument was accepted: rc=$rc -- $(_bst_1line "$out")"; fi # ...and -b WITH an argument must be accepted, which is what says the case above # measured the missing argument rather than the option. `Accepted' is read as # "did not print usage": the run goes on to the socket and stops there, so the # positive evidence is the bind diagnostic in place of the usage line. -d is # passed because on a machine that HAS AF_PACKET this invocation would otherwise # reach the fork. rb 20 "$RARPD" -d -b "$D" if has "$USAGE" "$out"; then bst_notok rarpd/b-accepts-argument "-b $D was refused by the argument parser: $(_bst_1line "$out")" elif [ "$rc" -eq 124 ] || [ "$rc" -eq 1 ]; then bst_ok rarpd/b-accepts-argument elif bst_execfail "$out"; then bst_unrun rarpd/b-accepts-argument "$out" else bst_notok rarpd/b-accepts-argument "rc=$rc -- $(_bst_1line "$out")"; fi # The arity check: `if (argc > optind+1) usage()'. One interface operand is the # documented maximum. rb 20 "$RARPD" -d lo lo if [ "$rc" -eq 124 ]; then bst_notok rarpd/reject-two-interfaces "rarpd -d lo lo did not return within 20 s (rc=124)" elif [ "$rc" -eq 1 ] && has "$USAGE" "$out"; then bst_ok rarpd/reject-two-interfaces elif bst_execfail "$out"; then bst_unrun rarpd/reject-two-interfaces "$out" else bst_notok rarpd/reject-two-interfaces "two interface operands were accepted: rc=$rc -- $(_bst_1line "$out")"; fi # The flag-only options must all be accepted together -- the case that says the # refusals above are the parser working rather than the parser refusing. rb 20 "$RARPD" -d -a -A -v -o -e if has "$USAGE" "$out"; then bst_notok rarpd/accepts-documented-flags "-a -A -v -o -e were refused by the argument parser: $(_bst_1line "$out")" elif [ "$rc" -eq 124 ] || [ "$rc" -eq 1 ]; then bst_ok rarpd/accepts-documented-flags elif bst_execfail "$out"; then bst_unrun rarpd/accepts-documented-flags "$out" else bst_notok rarpd/accepts-documented-flags "rc=$rc -- $(_bst_1line "$out")"; fi # --- the interface operand and its exit status -------------------------------- # The ticket's "documented exit code for an unknown interface": SIOCGIFINDEX # fails, perror() names the ioctl, usage() exits 1. rc and the diagnostic's # subject are asserted; the ERRNO deliberately is not, because which one comes # back is a fact about #81 and not about rarpd -- see the next case. rb 20 "$RARPD" -d nosuchif0 unk_out=$out; unk_rc=$rc if [ "$unk_rc" -eq 124 ]; then bst_notok rarpd/unknown-interface-rc "rarpd -d nosuchif0 did not return within 20 s (rc=124)" elif [ "$unk_rc" -eq 1 ] && has "ioctl(SIOCGIFINDEX)" "$unk_out" && has "$USAGE" "$unk_out"; then bst_ok rarpd/unknown-interface-rc elif bst_execfail "$unk_out"; then bst_unrun rarpd/unknown-interface-rc "$unk_out" else bst_notok rarpd/unknown-interface-rc "want rc=1 with the SIOCGIFINDEX diagnostic and the usage line; got rc=$unk_rc -- $(_bst_1line "$unk_out")"; fi # BRINTOS-WORKAROUND: brintos/linux#81 revisit -- the uniformity, scored as the # positive fact it is. With no packet socket pset[0].fd is -1, so SIOCGIFINDEX # answers EBADF for a name that exists exactly as for one that does not, and # `rarpd lo' cannot be told from `rarpd nosuchif0'. OK is GREEN while they agree; # the day #81 lands they will diverge and this case goes red, which is the signal # to replace it with the real discrimination rather than to leave a stale pass. rb 20 "$RARPD" -d lo if has "ioctl(SIOCGIFINDEX)" "$unk_out" && has "ioctl(SIOCGIFINDEX)" "$out" && [ "$rc" -eq "$unk_rc" ]; then bst_ok rarpd/ifname-wall-is-uniform elif ! has "ioctl(SIOCGIFINDEX)" "$out"; then bst_notok rarpd/ifname-wall-is-uniform "an existing interface no longer fails SIOCGIFINDEX -- the packet socket is back (brintos/linux#81 lifted?), so the known/unknown discrimination is now observable and this suite must assert it instead: rc=$rc $(_bst_1line "$out")" elif bst_execfail "$out"; then bst_unrun rarpd/ifname-wall-is-uniform "$out" else bst_notok rarpd/ifname-wall-is-uniform "lo and nosuchif0 no longer end alike: lo rc=$rc, nosuchif0 rc=$unk_rc -- $(_bst_1line "$out")"; fi # --- the platform's half ------------------------------------------------------ # Read from the kernel rather than inferred from a failed command. This is the # ONE red this suite expects, and it is attributed, not skipped: what is absent # is the transport, not the subject. # # BRINTOS-WORKAROUND: brintos/linux#81 remove -- /proc/net/packet exists if and # only if CONFIG_PACKET=y. if [ -e /proc/net/packet ]; then bst_ok rarpd/af-packet-available else bst_notok rarpd/af-packet-available "brintos/linux#81 (https://brintos.io/brintos/linux/issues/81): the kernel is built '# CONFIG_PACKET is not set' (arch/wasm32/configs/wasm32_defconfig), so socket(PF_PACKET, SOCK_DGRAM, 0) is EAFNOSUPPORT and rarpd can bind nothing" fi # rarpd's own reading of the same fact, through the invocation the daemon is # actually started with. Two independent readings of one gap: the kernel's, and # the program's. Scored so OK is GREEN -- it flips by itself when #81 lands. rb 30 "$RARPD" -d -a fg_out=$out; fg_rc=$rc if has "$NOBIND" "$fg_out"; then bst_notok rarpd/binds-packet-socket "brintos/linux#81, consequential: rarpd bound neither the RARP nor the ARP socket and aborted before daemonizing -- rc=$fg_rc '$(_bst_1line "$fg_out")'" elif [ "$fg_rc" -eq 124 ]; then bst_ok rarpd/binds-packet-socket elif bst_execfail "$fg_out"; then bst_unrun rarpd/binds-packet-socket "$fg_out" else bst_notok rarpd/binds-packet-socket "the foreground run ended for a reason that is NOT the packet socket, which is not the tracked gap and wants reading: rc=$fg_rc -- $(_bst_1line "$fg_out")" fi # The daemon must not have survived. On this image it cannot -- the fork is past # the bind -- and that is worth asserting rather than assuming, because a process # left behind here could not be killed afterwards (brintos/linux#21). if command -v pidof >/dev/null 2>&1 && pidof rarpd >/dev/null 2>&1; then bst_notok rarpd/nothing-left-running "a rarpd is still running after the bounded foreground runs above, and this platform cannot kill it (brintos/linux#21)" else bst_ok rarpd/nothing-left-running fi # --- the runtime-probed half: answering a RARP request ------------------------ # The ticket's one probed assertion. It needs BOTH a packet socket and a peer on # the link, so both are probed and the skip states which one is missing. The # else branch is deliberately a LOUD RED and not an assertion, because there is # no frame injector on this image to compose a RARP request with: a machine that # has grown AF_PACKET and a second interface has outgrown this suite, and saying # so is the one answer that cannot become a stale pass. PEER= for _i in /sys/class/net/*; do _n=${_i##*/} [ "$_n" = "lo" ] && continue [ -e "$_i" ] || continue PEER=$_n; break done if [ ! -e /proc/net/packet ]; then bst_skip rarpd/answers-rarp-request "no AF_PACKET on this kernel ('# CONFIG_PACKET is not set') -- brintos/linux#81; rarpd cannot receive a RARP request, so there is nothing to answer" elif [ -z "$PEER" ]; then bst_skip rarpd/answers-rarp-request "AF_PACKET is present but this machine has no interface other than lo, so no peer can send a RARP request" else bst_notok rarpd/answers-rarp-request "AF_PACKET is available and $PEER is up, so this assertion is now REACHABLE and must be written: start 'rarpd -d -e -a', inject a RARP request from a MAC in $ETHERS, and assert the reply carries the mapped address. This suite has no frame injector and must be extended -- do not re-skip it" fi rm -rf "$D"