. "$BST_LIB" # BST-TIMEOUT: 300 # zeroconf 0.9 -- Anand Kumria's IPv4 link-local address allocator (RFC 3927), # meta-networking recipes-protocols/zeroconf. Test-plan entry 530. # # IT IS AN ARP DAEMON, AND THIS MACHINE HAS NEITHER THE SOCKET NOR A PEER. # RFC 3927 is a protocol of claims: a host picks an address out of # 169.254.0.0/16, ARP-probes it three times to see whether anybody objects, # announces it twice, and thereafter defends it. Every one of those four steps # is an ARP frame, and zeroconf puts them on the wire through exactly one # socket -- arp_open() is `socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ARP))`, # with no fallback anywhere in zeroconf.c. This kernel is built # `# CONFIG_PACKET is not set` (arch/wasm32/configs/wasm32_defconfig, "raw # sockets -- no consumer on the v0.1 surface"), so that call cannot succeed # and the daemon can never reach its state machine. Even if it could, RFC 3927 # is a two-party protocol: a probe answered by nobody is indistinguishable from # a probe nobody heard, so a single-host link can only ever show the address # being taken, never defended. # # SO THE SPLIT IS DELIBERATE AND IT IS THE SHAPE OF THIS WHOLE FILE. Everything # zeroconf decides BEFORE arp_open() -- argument parsing, the usage text, the # netlink RTM_GETLINK interface lookup, the RFC 3927 suitability rule, and -f # overriding it -- runs for real on this image and is asserted exactly. main() # runs those in a fixed order (check_args, netlink_open, check_ifname_exists, # check_ifname_type, arp_open), which is what lets each case below name WHICH # stage answered rather than merely that the process exited 1. # # THE ARP EXCHANGE ITSELF IS PROBED, NOT ASSUMED. The two things it needs are # read off this machine rather than inferred from a failed run: AF_PACKET, via # /proc/net/packet, and a non-loopback interface, via /sys/class/net. A run # that confirms both are absent SKIPS the exchange -- the subject is genuinely # not present, which is what a skip means -- while the platform gap that makes # it absent is scored once, in its own case, as the attributed red it is. # Written so that the day CONFIG_PACKET and a virtio-net peer land, the skip # becomes a real run with no edit here. # # EVERY INVOCATION PASSES -n. Without it main() calls daemon(0,0) and, when # also not verbose, closes all three standard descriptors -- so a run that got # past arp_open() would detach, take the pidfile, and be unkillable on a # machine where a blocked process cannot be signalled (brintos/linux#21). # -n keeps it in the foreground; `timeout` bounds it anyway. require /usr/sbin/zeroconf zeroconf/present ZC=/usr/sbin/zeroconf zc() { _t=$1; shift; out=$(timeout "$_t" "$@" 2>&1); rc=$?; } # has ... : true iff EVERY needle is a substring. has() { _h=$1; shift for _n in "$@"; do case "$_h" in *"$_n"*) ;; *) return 1 ;; esac; done return 0 } # ---------------------------------------------------------------- inventory -- check_eq zeroconf/inventory "yes" "$([ -x "$ZC" ] && 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 on which /usr/sbin lost its place. check_eq zeroconf/on-path "$ZC" "$(command -v zeroconf 2>/dev/null)" # The recipe's do_install writes three files and this is the other two. The # if-up.d hook is what a Debian-style ifupdown would run; nothing on this image # has an ifupdown, so it is asserted as shipped-and-executable rather than # exercised through a network event that cannot occur here. check zeroconf/default-shipped test -f /etc/default/zeroconf check zeroconf/ifup-hook-shipped test -x /etc/network/if-up.d/zeroconf # /etc/default/zeroconf is sourced by that hook, so a syntax error in it would # be a runtime fault nobody sees until an interface comes up. Sourcing it in a # subshell both parses it and reads back the one variable it sets unconditionally # (IFBLACKLIST=""); DISABLE and FALLBACK ship commented out, and the hook's # `[ -n "$DISABLE" ] && exit 0` means a stray uncomment would silently disable # the daemon, so their emptiness is asserted too. zc 20 sh -c '. /etc/default/zeroconf; printf "blacklist=[%s] disable=[%s] fallback=[%s]" "$IFBLACKLIST" "$DISABLE" "$FALLBACK"' check_eq zeroconf/default-parses "blacklist=[] disable=[] fallback=[]" "$out" # The hook refuses to act on address families and methods where an IPv4LL # address is meaningless. Run with no METHOD at all it must still exit 0 having # done nothing -- ADDRFAM is unset, so its first test wins. Asserted because # this hook is the only thing on the image that would ever start the daemon # unattended, and a hook that ran regardless would start it on lo. zc 20 sh -c 'ADDRFAM=inet6 IFACE=lo METHOD=loopback /etc/network/if-up.d/zeroconf' check_eq zeroconf/ifup-hook-declines-inet6 "0" "$rc" # ------------------------------------------------------- argv, before sockets -- # check_args() is the first thing main() calls and it touches no socket, so this # whole block is exact on any kernel. It has TWO distinct rejections and they # print different things: usage() for a malformed command line, and a bare # "no interface specified" for a well-formed one that named no interface. # Keeping them apart is the point -- a build that collapsed them would still # exit 1 on both. # # `argc < 3` is the literal guard, so the bare name cannot reach getopt at all. zc 20 "$ZC" check_eq zeroconf/no-args-rc "1" "$rc" if has "$out" "usage error" "zeroconf [-f|--force] [-v|--verbose] [-n|--no-fork] [-i|--interface] "; then bst_ok zeroconf/no-args-usage else bst_notok zeroconf/no-args-usage "want the usage error and its synopsis, rc=$rc -- $(_bst_1line "$out")" fi # The usage body documents all four flags. Asserted line by line rather than as # one blob: this is the only self-description the binary has -- there is no # --help and no --version option in zeroconf.c -- so it is the whole of what a # user on this image can ask it about itself. for pair in \ "force:force zeroconf to run on the specified interface" \ "verbose:report verbose information" \ "nofork:do not fork into the background" \ "iface:which interface to run on [required]" do _id=${pair%%:*}; _want=${pair#*:} if has "$out" "$_want"; then bst_ok "zeroconf/usage-documents-$_id" else bst_notok "zeroconf/usage-documents-$_id" "the usage text does not describe -$_id -- want '$_want'" fi done # A well-formed command line that names no interface. This must NOT print the # usage -- it is the other branch, at the bottom of check_args(). zc 20 "$ZC" -n -v check_eq zeroconf/no-interface-rc "1" "$rc" if has "$out" "no interface specified"; then if has "$out" "usage error"; then bst_notok zeroconf/no-interface-diagnostic "printed the usage as well, so the two argv rejections have collapsed into one -- $(_bst_1line "$out")" else bst_ok zeroconf/no-interface-diagnostic fi else bst_notok zeroconf/no-interface-diagnostic "want 'no interface specified', rc=$rc -- $(_bst_1line "$out")" fi # An unknown option must be refused rather than ignored. getopt_long returns '?' # and check_args prints its own line before calling usage(), so both are asserted # -- the getopt half proves the option string was consulted at all. zc 20 "$ZC" -Z -i lo if [ "$rc" -eq 124 ]; then bst_notok zeroconf/rejects-unknown-option "hung on a bad option -- $(_bst_1line "$out")" elif [ "$rc" -eq 0 ]; then bst_notok zeroconf/rejects-unknown-option "accepted an unknown option" elif has "$out" "unknown option" "usage error"; then bst_ok zeroconf/rejects-unknown-option else bst_notok zeroconf/rejects-unknown-option "rejected (rc=$rc) but not with the documented diagnostic -- $(_bst_1line "$out")" fi # -p/--ipaddr is a declared option whose body is `fprintf(stderr, "XXX: not # implemented")` and nothing else -- upstream's own TODO. It is asserted rather # than passed over so that the day it acquires a meaning, this case says so. zc 20 "$ZC" -n -p 169.254.7.7 if has "$out" "XXX: not implemented"; then bst_ok zeroconf/ipaddr-option-is-a-stub else bst_notok zeroconf/ipaddr-option-is-a-stub "want upstream's 'XXX: not implemented' for -p, rc=$rc -- $(_bst_1line "$out")" fi # LONG OPTIONS ARE A BUILD FACT, NOT A CONVENIENCE. zeroconf.c redefines # getopt_long to plain getopt under __KLIBC__, so a build that lost 's # GNU declaration would parse `--interface` as an error and still exit 1 like # every other case here. Asserting that --interface lo reaches the SAME stage as # -i lo separates a working getopt_long from a silently degraded one. zc 30 "$ZC" --no-fork --interface lo long_out=$out; long_rc=$rc zc 30 "$ZC" -n -i lo if [ "$long_rc" = "$rc" ] && [ "$long_out" = "$out" ]; then bst_ok zeroconf/long-options else bst_notok zeroconf/long-options "--no-fork --interface lo (rc=$long_rc) did not behave as -n -i lo (rc=$rc) -- $(_bst_1line "$long_out")" fi # ------------------------------------------------- netlink, and the RFC rule -- # `$out`/`$rc` above are the -n -i lo run, and they are what the next three # cases read. lo EXISTS, so getting the "not suitable" verdict proves the whole # netlink path ran: socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE), bind, an # RTM_GETLINK carrying IFLA_IFNAME, and the reply parsed far enough to fill in # both the index and the flags. check_eq zeroconf/loopback-rc "1" "$rc" if has "$out" "unable to connect to netlink" "socket(PF_NETLINK)"; then bst_notok zeroconf/netlink-usable "netlink could not be opened at all, so nothing below this line is about zeroconf -- $(_bst_1line "$out")" elif has "$out" "unable to connect to netlink"; then bst_notok zeroconf/netlink-usable "netlink_open() failed after the socket, i.e. at bind() -- $(_bst_1line "$out")" elif has "$out" "not suitable for zeroconf"; then bst_ok zeroconf/netlink-usable else bst_notok zeroconf/netlink-usable "did not reach the suitability check, so where it stopped is unknown -- rc=$rc $(_bst_1line "$out")" fi # RFC 3927 s1.1: IPv4LL is defined for links that carry ARP. check_ifname_type() # refuses IFF_LOOPBACK, IFF_NOARP, IFF_SLAVE and IFF_POINTOPOINT, and lo is the # one of the four this machine can present. The message carries the INDEX, which # the netlink reply supplied -- so asserting the whole line, not just the verb, # also asserts that the lookup returned lo's real index rather than the -1 the # struct was initialised with. if has "$out" "Interface lo (1) not suitable for zeroconf"; then bst_ok zeroconf/refuses-loopback elif has "$out" "not suitable for zeroconf"; then bst_notok zeroconf/refuses-loopback "refused lo, but not with its netlink index -- $(_bst_1line "$out")" else bst_notok zeroconf/refuses-loopback "lo was not refused as an IPv4LL interface -- rc=$rc $(_bst_1line "$out")" fi # An interface that does not exist. Run VERBOSE, and that is not for the # reading: check_ifname_exists() sends its RTM_GETLINK with NLM_F_MATCH and no # NLM_F_DUMP, which the kernel answers with the WHOLE link table rather than an # NLMSG_ERROR -- measured against this same source on x86-64, where a bogus name # produces no ENODEV anywhere in the output. So the interface is reported missing # because zeroconf walked every reply and matched none, and the ONLY thing that # tells that apart from the 500 ms poll giving up is the enumeration itself. -v # prints one `got iface :` per link, so asserting lo appears in it # says the lookup was answered; the verdict line says the walk then rejected the # name. A run that printed the verdict without the enumeration would be the # timeout wearing the same words. # # ONE UPSTREAM LIMIT IS WORTH NAMING HERE AND IS NOT ASSERTED, BECAUSE THIS # MACHINE CANNOT REACH IT. check_ifname_exists() issues ONE recv(2) of 4096 # bytes and walks only that datagram; a netlink dump is delivered as a SEQUENCE # of datagrams, so an interface whose RTM_NEWLINK lands in the second or later # one is reported "does not exist". Measured against this same source on an # x86-64 host with a dozen links: it enumerated two and then rejected a name # that was plainly present. Unreachable here -- this image has lo and at most # one virtio-net link, which share the first datagram -- and not reported # upstream: zeroconf 0.9 is a 2006 release with no maintainer, and # docs/conventions/no-public-upstream-reports.md forbids filing against # third-party public repositories. Recorded so a future case that DOES depend # on the interface table's length is written knowing it. zc 30 "$ZC" -n -v -i bstzc-nodev check_eq zeroconf/unknown-interface-rc "1" "$rc" if ! has "$out" "Interface bstzc-nodev does not exist"; then bst_notok zeroconf/unknown-interface "want 'Interface bstzc-nodev does not exist', rc=$rc -- $(_bst_1line "$out")" elif has "$out" "couldn't find interface"; then bst_notok zeroconf/unknown-interface "the netlink poll timed out rather than being answered, so the verdict is not the name lookup's -- $(_bst_1line "$out")" elif has "$out" "got iface 1:lo"; then bst_ok zeroconf/unknown-interface else bst_notok zeroconf/unknown-interface "reported the interface missing, but the RTM_GETLINK reply enumerated no links at all -- $(_bst_1line "$out")" fi # -f/--force exists to override the rule above, and overriding it is observable: # the run must get PAST check_ifname_type and stop at the next stage instead. # Whatever that next stage says, it must no longer be the suitability message -- # a -f that changed nothing would look identical to a -f that was not parsed. zc 30 "$ZC" -n -f -i lo force_out=$out; force_rc=$rc if has "$force_out" "not suitable for zeroconf"; then bst_notok zeroconf/force-overrides-suitability "-f did not override the IPv4LL interface rule -- $(_bst_1line "$force_out")" elif has "$force_out" "unable to obtain ARP socket" "arp socket failed"; then bst_ok zeroconf/force-overrides-suitability elif [ "$force_rc" -eq 124 ]; then bst_notok zeroconf/force-overrides-suitability "hung past the suitability check -- $(_bst_1line "$force_out")" else bst_notok zeroconf/force-overrides-suitability "got past the suitability check but stopped somewhere unnamed, rc=$force_rc -- $(_bst_1line "$force_out")" fi # --------------------------------------------- what the platform withholds ---- # /proc/net/packet exists if and only if CONFIG_PACKET=y. This is the single # fact that decides whether zeroconf can ever run here, and it is read from the # kernel rather than inferred from the failed run above. HAVE_PACKET=0 if [ -e /proc/net/packet ]; then HAVE_PACKET=1 bst_ok zeroconf/af-packet-available else bst_notok zeroconf/af-packet-available "brintos/linux: the kernel is built '# CONFIG_PACKET is not set' (arch/wasm32/configs/wasm32_defconfig), so arp_open()'s socket(PF_PACKET, SOCK_DGRAM, ETH_P_ARP) cannot be created and no ARP probe, announcement or defence can leave this machine" fi # zeroconf's OWN report of the same fact, from the -f run above. This is scored # as a PASS when the socket is unavailable: refusing cleanly, naming the errno # and exiting 1 is correct behaviour for a daemon that cannot reach its wire, # and it is the behaviour a caller depends on. The platform gap is charged once, # in the case above, not twice. if [ "$HAVE_PACKET" -eq 1 ]; then if has "$force_out" "unable to obtain ARP socket"; then bst_notok zeroconf/arp-socket-refusal "AF_PACKET is available yet arp_open() still failed -- $(_bst_1line "$force_out")" else bst_ok zeroconf/arp-socket-refusal fi elif has "$force_out" "arp socket failed" "unable to obtain ARP socket"; then bst_ok zeroconf/arp-socket-refusal else bst_notok zeroconf/arp-socket-refusal "with no AF_PACKET, arp_open() must fail with both of its diagnostics and exit 1, rc=$force_rc -- $(_bst_1line "$force_out")" fi # The other half of what the exchange needs: something other than lo to run on. # /sys/class/net is the enumeration; a link whose type is 1 (ARPHRD_ETHER) and # whose name is not lo is a candidate. VIRTIO_NET=y is in the defconfig, so this # is genuinely a probe and not a foregone conclusion. LINK= for d in /sys/class/net/*; do [ -d "$d" ] || continue n=${d##*/} [ "$n" = "lo" ] && continue [ "$(cat "$d/type" 2>/dev/null)" = "1" ] || continue LINK=$n break done if [ -n "$LINK" ]; then bst_ok zeroconf/arp-capable-link else bst_notok zeroconf/arp-capable-link "brintos/linux: no ARP-capable interface is registered on this machine -- /sys/class/net holds lo alone, so RFC 3927 has no link to claim an address on even with a working AF_PACKET socket" fi # ------------------------------------------- THE PROBE/ANNOUNCE EXCHANGE ------ # The ticket's runtime criterion. It is SKIPPED only on a probe that confirms # the subject is absent -- both preconditions read from this kernel above -- and # is otherwise run for real. Written out in full deliberately: nothing here # needs editing on the day the platform supplies them. if [ "$HAVE_PACKET" -ne 1 ] && [ -z "$LINK" ]; then bst_skip zeroconf/probe-announce "neither precondition exists on this machine: no AF_PACKET (see zeroconf/af-packet-available) and no ARP-capable interface (see zeroconf/arp-capable-link), so there is no ARP exchange to observe rather than a broken one" elif [ "$HAVE_PACKET" -ne 1 ]; then bst_skip zeroconf/probe-announce "a link is present ($LINK) but AF_PACKET is not (see zeroconf/af-packet-available), so no ARP frame can be composed" elif [ -z "$LINK" ]; then bst_skip zeroconf/probe-announce "AF_PACKET is present but no ARP-capable interface is (see zeroconf/arp-capable-link), so there is nothing to probe on" else # -v keeps the three standard descriptors open past the daemon() branch and # makes the state machine narrate itself; -n keeps it in the foreground so # `timeout` can end it. It never terminates on its own once it takes an # address, so the SIGTERM `timeout` sends IS the expected end and rc=124 is # not a failure here. What is asserted is the narration: the probe state # entered, an address picked out of 169.254/16, and the claim reached. # # The link must be RUNNING before it is handed over. main()'s loop begins # `if (!intf.up) { delay_cancel(); reset; state = ADDR_PROBE; continue; }`, # and that `continue` skips the switch -- so on a link the kernel does not # report IFF_RUNNING the daemon spins without ever entering a state, and the # run below would end at `timeout` having narrated nothing. That is a # different fact from a probe that failed, so bring the link up first and, if # it will not come up, say THAT. ip link set "$LINK" up >/dev/null 2>&1 flags=$(cat "/sys/class/net/$LINK/flags" 2>/dev/null) running=$(( ${flags:-0} & 0x40 )) zc 45 "$ZC" -n -v -i "$LINK" picked=$(printf '%s\n' "$out" | sed -n 's/^picked address: //p' | head -n 1) if has "$out" "arp socket failed"; then bst_notok zeroconf/probe-announce "the ARP socket was refused although /proc/net/packet exists -- $(_bst_1line "$out")" elif has "$out" "not suitable for zeroconf"; then bst_notok zeroconf/probe-announce "$LINK was refused as an IPv4LL interface -- it carries one of IFF_NOARP/IFF_SLAVE/IFF_POINTOPOINT, so this machine's only non-loopback link cannot carry RFC 3927 -- $(_bst_1line "$out")" elif has "$out" "does not exist"; then bst_notok zeroconf/probe-announce "$LINK is in /sys/class/net but RTM_GETLINK did not enumerate it -- $(_bst_1line "$out")" elif ! has "$out" "entering ADDR_PROBE" && [ "$running" -eq 0 ]; then bst_notok zeroconf/probe-announce "$LINK never reached IFF_RUNNING (flags=$flags), so main() spun on its !intf.up branch and no state was ever entered -- the link is down, not the probe broken" elif ! has "$out" "entering ADDR_PROBE"; then bst_notok zeroconf/probe-announce "never reached ADDR_PROBE on a RUNNING link, so no probe was sent -- rc=$rc $(_bst_1line "$out")" else case "$picked" in 169.254.*) if has "$out" "entering ADDR_CLAIM"; then bst_ok zeroconf/probe-announce else bst_notok zeroconf/probe-announce "probed and picked $picked but never announced it (ADDR_CLAIM not reached), which is a probe answered by a conflict or a send that did not complete -- $(_bst_1line "$out")" fi ;; "") bst_notok zeroconf/probe-announce "reached ADDR_PROBE but printed no picked address -- rc=$rc $(_bst_1line "$out")" ;; *) bst_notok zeroconf/probe-announce "picked '$picked', which is not from the 169.254.0.0/16 link-local range RFC 3927 defines -- $(_bst_1line "$out")" ;; esac fi fi # --------------------------------------------------------------- hygiene ----- # Nothing above may have left a daemon behind. Every invocation passed -n and # every one of them was expected to exit before the poll loop; the pidfile is # written only after that point, so its absence is the cheap proof that none of # them detached. /var/run/zeroconf..pid is the literal path in main(). left=$(ls /var/run/zeroconf.*.pid 2>/dev/null) if [ -z "$left" ]; then bst_ok zeroconf/no-stray-pidfile else bst_notok zeroconf/no-stray-pidfile "a zeroconf pidfile was left behind: $(_bst_1line "$left")" fi # The marker every case above is counted against. Its absence is the only signal # that the suite died mid-file and the remaining cases were lost silently. bst_ok zeroconf/suite-complete