. "$BST_LIB" # BST-TIMEOUT: 600 # fping 5.1 -- David Papp / Thomas Dzubin's parallel ICMP reachability prober # (meta-networking recipes-support/fping). NOT ping(1): inetutils' ping walks one # target until it is done and 88-inetutils covers it. fping's whole reason to # exist is the opposite shape -- send one probe per target, move on, and collect # the replies as they land -- so the multi-target case below is the subject, not # a variation on a single-host ping. # # IT INSTALLS TO /usr/sbin, NOT /usr/bin. Upstream's src/Makefile.am declares # `sbin_PROGRAMS = fping`, so the tool lands at /usr/sbin/fping. No busybox or # toybox applet is named fping in this defconfig and no other package registers # the name, so there is no update-alternatives contest here and a bare `fping` # on root's PATH is unambiguously this port. Every functional case runs the # concrete path anyway, so a future package appearing under the bare name shows # up as a red on the on-path case rather than silently changing this suite's # subject. # # NO fping6, DELIBERATELY. DISTRO_FEATURES is empty in brintos.conf, so the # recipe's PACKAGECONFIG resolves to --disable-ipv6 and IPV6 is never defined -- # socket6.c is not even compiled in. That matches the kernel, which is built # CONFIG_IPV6=n (brintos/linux#54), so an IPv6-capable fping would have nothing # to talk to. The absence is asserted, not merely tolerated: an fping6 appearing # on the image would mean the configure arm flipped under us. # # WHY EVERY INVOCATION IS BOUNDED BY `timeout`. fping waits for replies that may # never come; the defaults are 3 retries with a backoff, and a target that # swallows probes costs seconds per host. Where a platform gap eats the replies # an unbounded case would spend the suite's whole 600 s budget and take the # cases after it down with it. The bound is part of the assertion: a case that # trips it reports rc=124, which every helper below distinguishes from fping's # own documented exit statuses. require /usr/sbin/fping fping/present FP=/usr/sbin/fping # fping's documented exit statuses (doc/fping.pod), all of which appear below: # 0 every target replied 2 a target name did not resolve # 1 at least one target did not 4 a system call failed (incl. no socket) # Note 1, not 3, is what an unknown OPTION actually returns -- src/fping.c's # optparse '?' arm exits 1 while the man page says 3. That is upstream's own # inconsistency, not this platform's, and the case below asserts the code's # behaviour because that is what a user on this image gets. # fb -- run bounded, capturing stdout+stderr; sets $out and $rc. fb() { _t=$1; shift; out=$(timeout "$_t" "$@" 2>&1); rc=$?; } # --- inventory: what the package put on the image ----------------------------- # fping-doc is not in EMCRAFT_TOOLS, so fping.8 is deliberately not expected -- # asserting on it would red every build for a packaging split made on purpose. check_eq fping/inventory "yes" "$([ -x "$FP" ] && echo yes)" # The bare name a user types must be this binary. `command -v` reads root's # actual PATH, so this also catches an image where /usr/sbin is not on it. check_eq fping/on-path "$FP" "$(command -v fping 2>/dev/null)" # IPv6 was configured out (above). fping6 must therefore be absent; its presence # would mean the PACKAGECONFIG arm flipped and the suite's IPv4-only reasoning # no longer holds. check_eq fping/no-fping6 "" "$(command -v fping6 2>/dev/null)" # --- the binary runs ---------------------------------------------------------- # -v prints ": Version " and exits 0. It is handled inside the # option loop, i.e. AFTER the ICMP socket has been opened, so unlike -h below it # is not a socket-free path. fb 20 "$FP" -v check_eq fping/version-rc "0" "$rc" case "$out" in *"Version 5.1"*) bst_ok fping/version-is-5.1 ;; *) bst_notok fping/version-is-5.1 "want 'Version 5.1', got '$(_bst_1line "$out")'" ;; esac # -h is pre-parsed before open_ping_socket_ipv4() precisely so that usage works # where the socket cannot be had. Running it separately is what lets a red on # the socket cases below be read as "the socket failed" rather than "the binary # is broken". fb 20 "$FP" -h check_eq fping/help-rc "0" "$rc" case "$out" in *"Usage:"*"Probing options:"*) bst_ok fping/help-text ;; *) bst_notok fping/help-text "usage text not as expected -- $(_bst_1line "$out")" ;; esac # --- the ICMP socket, and the two platform walls it walks past ---------------- # open_ping_socket_ipv4() does two things that can fail on this stack, and this # case separates them from a mere lack of replies: # # getprotobyname("icmp") -- an NSS/protocols-database lookup that reads # /etc/protocols. netbase supplies it (EMCRAFT_TOOLS names netbase); without # it fping dies "icmp: unknown protocol" and exit 4 before a probe is sent. # # socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) -- and, if that is refused, a # fallback to SOCK_DGRAM. Both failing is crash_and_burn("can't create # socket (must run as root?)"), also exit 4. # # So an exit of 4 with no reply text means the tool never got onto the network # at all, which is a completely different finding from an unreachable host, and # the criterion cases below would not tell them apart on their own. fb 30 "$FP" -c 1 -t 500 127.0.0.1 case "$rc:$out" in 4:*"unknown protocol"*) bst_notok fping/icmp-socket "getprotobyname(icmp) failed -- /etc/protocols missing or unreadable: $(_bst_1line "$out")" ;; 4:*"can't create socket"*) bst_notok fping/icmp-socket "neither SOCK_RAW nor SOCK_DGRAM ICMP could be opened: $(_bst_1line "$out")" ;; 4:*) bst_notok fping/icmp-socket "exit 4 (system call failure) before any verdict: $(_bst_1line "$out")" ;; 124:*) bst_notok fping/icmp-socket "hung with no verdict: $(_bst_1line "$out")" ;; *) bst_ok fping/icmp-socket ;; esac # fping never drops privilege HERE, and that is why it is green where # 88-inetutils' ping is red. Both seteuid() sites (src/fping.c, the temporary # drop after socket creation and the permanent one after option parsing) are # guarded by `geteuid() != getuid()`, which is false for root on this image, so # the calls are short-circuited and the ENOSYS credential wall # (brintos/linux#19) is never reached. Asserted rather than argued: any # complaint on that path prints "setuid" or "privileges" to stderr, and the run # above would have carried it. case "$out" in *setuid*|*privileges*) bst_notok fping/no-privilege-drop "fping attempted a credential change -- $(_bst_1line "$out")" ;; *) bst_ok fping/no-privilege-drop ;; esac # SO_TIMESTAMPNS is compiled in (configure only checks that the constant is # DECLARED, which the headers do regardless of kernel support) and set on the # socket at startup. A kernel that refuses it makes fping perror and carry on, # so it is invisible in the exit status -- which is exactly why it gets a case: # a red here is a real statement about this stack's setsockopt coverage and # costs nothing to observe, while the tool keeps working either way. case "$out" in *"setting SO_TIMESTAMPNS option"*) bst_notok fping/so-timestampns "kernel refused SO_TIMESTAMPNS -- $(_bst_1line "$out")" ;; *) bst_ok fping/so-timestampns ;; esac # --- THE TICKET'S NAMED CRITERION, first half --------------------------------- # "fping 127.0.0.1 returns alive with a zero exit status" -- deterministic and # offline, no external peer. Both halves are required: the verbose default # prints " is alive", and finish() exits 0 only when num_alive equals # num_hosts. fb 30 "$FP" 127.0.0.1 if [ "$rc" -eq 124 ]; then bst_notok fping/loopback-alive "timed out with no verdict -- $(_bst_1line "$out")" elif bst_execfail "$out"; then bst_unrun fping/loopback-alive "$out" else case "$rc:$out" in 0:*"127.0.0.1 is alive"*) bst_ok fping/loopback-alive ;; 0:*) bst_notok fping/loopback-alive "exit 0 but no 'is alive' line -- $(_bst_1line "$out")" ;; *"is alive"*) bst_notok fping/loopback-alive "reported alive but exit status was $rc -- $(_bst_1line "$out")" ;; *) bst_notok fping/loopback-alive "rc=$rc, loopback not reported alive -- $(_bst_1line "$out")" ;; esac fi # --- THE TICKET'S NAMED CRITERION, second half -------------------------------- # "an unreachable address returns the documented non-zero status". # # 192.0.2.1 is TEST-NET-1 (RFC 5737), reserved for documentation and guaranteed # never to be routed -- so this stays deterministic and offline whatever # networking the machine is given. On a guest whose only interface is lo the # sendto fails immediately with ENETUNREACH, which send_ping() reports as a # warning and counts as no-reply rather than an error; either way the host ends # with num_recv == 0 and finish() exits 1. -r 0 drops the retries and -t 500 # caps the single wait, so the case costs well under a second in both shapes. fb 30 "$FP" -r 0 -t 500 192.0.2.1 if [ "$rc" -eq 124 ]; then bst_notok fping/unreachable-nonzero "timed out with no verdict -- $(_bst_1line "$out")" elif [ "$rc" -eq 1 ]; then case "$out" in *"192.0.2.1 is unreachable"*) bst_ok fping/unreachable-nonzero ;; *) bst_notok fping/unreachable-nonzero "exit 1 as documented, but no 'is unreachable' line -- $(_bst_1line "$out")" ;; esac elif [ "$rc" -eq 0 ]; then bst_notok fping/unreachable-nonzero "TEST-NET-1 reported reachable, which cannot be right -- $(_bst_1line "$out")" else bst_notok fping/unreachable-nonzero "want the documented exit 1, got $rc -- $(_bst_1line "$out")" fi # --- the shape that makes fping fping ----------------------------------------- # Several targets in one run, probed round-robin rather than one-after-another, # with a per-target verdict and an aggregate exit status. This is the single # case that ping(1) cannot stand in for, and it exercises both criterion halves # together: one reachable target and one that can never be, in one invocation, # with the documented "some hosts unreachable" exit 1. fb 40 "$FP" -r 0 -t 500 127.0.0.1 192.0.2.1 _alive=0; _unreach=0 case "$out" in *"127.0.0.1 is alive"*) _alive=1 ;; esac case "$out" in *"192.0.2.1 is unreachable"*) _unreach=1 ;; esac if [ "$rc" -eq 124 ]; then bst_notok fping/multi-target "timed out with no verdict -- $(_bst_1line "$out")" elif [ "$_alive" -eq 1 ] && [ "$_unreach" -eq 1 ] && [ "$rc" -eq 1 ]; then bst_ok fping/multi-target else bst_notok fping/multi-target "want both verdicts and exit 1, got rc=$rc alive=$_alive unreachable=$_unreach -- $(_bst_1line "$out")" fi # --- counted mode and its statistics ------------------------------------------ # -c switches on per-target statistics, which is the reporting path a user # actually reads and a different code path from the one-shot verdict above. On # loopback all three probes must land, so the summary is fully determined. # # The literal is "xmt/rcv/%loss = 3/3/0%" -- three slash-separated fields, NOT # the "3/3, 0%" spelling fping uses in its per-target summary elsewhere. That # was checked against the target rather than assumed; the two formats differ by # a comma and would otherwise have been a red that reads like a lost packet. fb 40 "$FP" -c 3 -p 100 -t 500 127.0.0.1 if [ "$rc" -eq 124 ]; then bst_notok fping/count-stats "timed out -- $(_bst_1line "$out")" else case "$rc:$out" in 0:*"xmt/rcv/%loss = 3/3/0%"*) bst_ok fping/count-stats ;; *) bst_notok fping/count-stats "want 'xmt/rcv/%loss = 3/3/0%' and exit 0, got rc=$rc -- $(_bst_1line "$out")" ;; esac fi # --- the two report filters --------------------------------------------------- # -a prints only the targets that answered, as bare addresses with no "is alive" # suffix (finish() clears verbose_flag for it). -u is its complement. Together # they are the scripting interface fping is usually reached for, and they are # each a distinct branch of finish(), so a regression in one would not show up # in the verbose cases above. fb 40 "$FP" -a -r 0 -t 500 127.0.0.1 192.0.2.1 check_eq fping/alive-only "127.0.0.1" "$out" fb 40 "$FP" -u -r 0 -t 500 127.0.0.1 192.0.2.1 check_eq fping/unreachable-only "192.0.2.1" "$out" # --- target generation -------------------------------------------------------- # -g builds the target list from a CIDR block instead of the command line, which # is a parser (add_cidr) rather than a probing path. 192.0.2.0/30 yields the two # host addresses .1 and .2, both TEST-NET-1 and so both unreachable by # construction -- which makes the assertion about the ENUMERATION, not about # reachability: two targets in, two "is unreachable" verdicts out, exit 1. # # THE MATCH IS ANCHORED, AND THAT IS LOAD-BEARING. This case runs verbose (no # -a/-u to clear verbose_flag), so on a guest whose only interface is lo the # immediate ENETUNREACH also prints ": error while sending ping: Network # is unreachable" per target. That line CONTAINS the substring "is unreachable", # so a bare grep counts 4 where 2 is right -- observed, not hypothesised. # Anchoring on the address at start of line counts finish()'s verdicts only, and # keeps the case a statement about enumeration rather than about routing. fb 40 "$FP" -g 192.0.2.0/30 -r 0 -t 500 _n=$(printf '%s\n' "$out" | grep -cE '^192\.0\.2\.[0-9]+ is unreachable$') if [ "$rc" -eq 124 ]; then bst_notok fping/generate-cidr "timed out -- $(_bst_1line "$out")" elif [ "$_n" -eq 2 ] && [ "$rc" -eq 1 ]; then bst_ok fping/generate-cidr else bst_notok fping/generate-cidr "want 2 unreachable targets and exit 1, got rc=$rc n=$_n -- $(_bst_1line "$out")" fi # --- argument and name handling ----------------------------------------------- # A name that does not resolve is its own documented status (2, "IP addresses # not found"), distinct from a target that resolves and does not answer (1). It # also exercises the resolver, which every numeric case above deliberately # avoids -- so this is the one case here that touches NSS. fb 30 "$FP" -r 0 -t 500 no-such-host.invalid if [ "$rc" -eq 124 ]; then bst_notok fping/unresolvable-name "hung instead of failing -- $(_bst_1line "$out")" elif [ "$rc" -eq 2 ]; then bst_ok fping/unresolvable-name elif [ "$rc" -eq 0 ]; then bst_notok fping/unresolvable-name "exited 0 on an unresolvable name -- $(_bst_1line "$out")" else bst_notok fping/unresolvable-name "want the documented exit 2, got $rc -- $(_bst_1line "$out")" fi # An unknown option must be rejected rather than ignored. The status asserted is # 1, which is what src/fping.c's optparse '?' arm returns; the man page's claim # of 3 is upstream's own inconsistency and is not what this image does. fb 20 "$FP" --no-such-option 127.0.0.1 if [ "$rc" -eq 124 ]; then bst_notok fping/rejects-unknown-option "hung on a bad option -- $(_bst_1line "$out")" elif [ "$rc" -eq 0 ]; then bst_notok fping/rejects-unknown-option "accepted an unknown option -- $(_bst_1line "$out")" else case "$out" in *"fping -h"*|*"no-such-option"*) bst_ok fping/rejects-unknown-option ;; *) bst_notok fping/rejects-unknown-option "rejected (rc=$rc) but said nothing about the option -- $(_bst_1line "$out")" ;; esac fi # Mutually exclusive flags are refused before any probe is sent -- a pure # option-validation path, and the one place fping exits non-zero without having # touched the network at all. fb 20 "$FP" -a -u 127.0.0.1 case "$rc:$out" in 0:*) bst_notok fping/rejects-a-with-u "accepted -a and -u together -- $(_bst_1line "$out")" ;; 124:*) bst_notok fping/rejects-a-with-u "hung -- $(_bst_1line "$out")" ;; *"specify only one of a, u"*) bst_ok fping/rejects-a-with-u ;; *) bst_notok fping/rejects-a-with-u "rejected (rc=$rc) but not with the expected diagnostic -- $(_bst_1line "$out")" ;; esac