. "$BST_LIB" # BST-TIMEOUT: 600 # dhcpcd 10.0.6 -- Roy Marples' DHCP client, the one most Linux distributions # ship as their default (poky recipes-connectivity/dhcpcd). Test-plan entry 379. # # IT IS THE FIRST DHCP *CLIENT* ON THIS IMAGE. 88-dnsmasq.test next door covers a # DHCP *server*; until this port nothing on the machine had ever asked for a # lease. It installs to /sbin, not /usr/sbin, and that is the recipe's doing: # poky passes --sbindir=${base_sbindir}. # # --------------------------------------------------------------------------- # THE TICKET'S CRITERION IS A LEASE, AND THE PLATFORM WITHHOLDS THE TRANSPORT # RATHER THAN THE PROTOCOL. That split is the shape of this whole suite. # # A DHCP client sends DISCOVER from an interface that has no address yet, so it # cannot use a UDP socket: on Linux dhcpcd opens AF_PACKET and attaches a BPF # filter (src/bpf.c, src/if-linux.c -- there is no other path). This kernel is # built `# CONFIG_PACKET is not set` (arch/wasm32/configs/wasm32_defconfig: "raw # sockets -- no consumer on the v0.1 surface"), and the same defconfig block says # the net stack is LOOPBACK ONLY -- no veth, no dummy, nothing to put a second # interface on. So the veth-pair-plus-dnsmasq lease the ticket names has neither # an interface to run on nor a socket to run over, and both halves are scored # here as attributed reds rather than skipped: dhcpcd is present and working, and # what stops it is the platform, which is exactly the finding worth surfacing. # # WHAT IS ASSERTED INSTEAD IS THE DECODER, OFFLINE AND EXACTLY. `dhcpcd -U -4` # reads a lease from standard input -- a raw BOOTP message, the same bytes a # server would have put on the wire -- and prints what it parsed. This suite # builds a DHCPACK byte by byte and asserts the offered address, the netmask and # the lease time that come back out, which is the ticket's criterion put to # dhcpcd's own option parser with the wire taken out of the middle. Octal # escapes, not \xNN: \x is a bashism the target's printf need not honour. # # NO PRIVSEP, AND THE ARTIFACT SAYS SO RATHER THAN THE RECIPE. privsep-linux.c # ends at `#error "Platform does not support seccomp filter yet"` for wasm32, and # --disable-seccomp would only have traded a compile error for a privsep that # cannot run: separation needs setgroups/setgid/setuid and chroot, and this # kernel implements no credential setter at all (brintos/linux#19 -- the same # wall 88-dnsmasq.test's daemonise case records from the server side). The # bbappend passes --disable-privsep, and `dhcpcd --version` prints a "Compiled in # features:" line built from the same #ifdefs, so the decision is read back off # the binary below. require /sbin/dhcpcd dhcpcd/present DC=/sbin/dhcpcd D=${TMPDIR:-/tmp}/bst-dhcpcd.$$ rm -rf "$D"; mkdir -p "$D" || { bst_notok dhcpcd/workdir "cannot create $D"; exit 0; } # The lease this suite asserts. 10.74.0.74 is 0a4a004a on the wire; the octal # forms below are \012\112\000\112. WANT_IP=10.74.0.74 WANT_MASK=255.255.255.0 WANT_LEASE=3600 WANT_SRV=10.74.0.1 db() { bst_run "$@"; } # lib/bst.sh: a bound that FIRES # (timeout(1) did not, before the # brintos/linux#77 fix) and a file capture # no wedged child can hold open. # 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 } # field : the value of the first `key=value` line, "" if absent. field() { printf '%s\n' "$1" | while IFS= read -r _l; do case "$_l" in "$2="*) printf '%s' "${_l#*=}"; break ;; esac done } zeros() { _i=0; while [ "$_i" -lt "$1" ]; do printf '\000'; _i=$((_i+1)); done; } # ---------------------------------------------------------------- inventory -- check_eq dhcpcd/inventory "yes" "$([ -x "$DC" ] && 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 on which /sbin lost its place. check_eq dhcpcd/on-path "$DC" "$(command -v dhcpcd 2>/dev/null)" # CONFFILES:${PN}. /etc/dhcpcd.conf is parsed for real further down; here it is # only asserted to exist, so a red there separates "not shipped" from "shipped # and unparseable". check dhcpcd/conffile-shipped test -f /etc/dhcpcd.conf # dhcpcd-run-hooks is not a nicety: it is ctx->script, and script_runreason() # returns before printing anything when ctx->script is NULL and no control # socket is open. Every lease-dump case below therefore depends on this file # being present, which is why it is asserted before them and not after. check dhcpcd/run-hooks-shipped test -x /usr/libexec/dhcpcd-run-hooks for h in 20-resolv.conf 30-hostname; do check "dhcpcd/hook-$h" test -f "/usr/libexec/dhcpcd-hooks/$h" done # The recipe's do_install writes ${systemd_system_unitdir}/dhcpcd*.service # UNCONDITIONALLY; what removes them again is systemd.bbclass's # rm_systemd_unitdir, which fires because DISTRO_FEATURES carries no systemd. If # that stopped firing the files would be installed-but-unpackaged and the build # would QA-fail, so this is the on-image half of a build-side invariant. # # Asserted against dhcpcd's OWN unit names, not against /lib/systemd existing. # The directory is not this port's to own: another package on the image ships # units into it (nuttcp), so the wider assertion reported a red for a file # dhcpcd never wrote. check_eq dhcpcd/no-systemd-unitdir "" "$(ls -d /lib/systemd/system/dhcpcd* /usr/lib/systemd/system/dhcpcd* 2>/dev/null)" # USERADD_PARAM creates a system user for the privsep child. privsep is compiled # out here, so the account is inert -- but the recipe still creates it and # do_install still chowns ${DBDIR} to it, so its absence would mean the useradd # postinst did not run rather than that the port changed its mind. check_eq dhcpcd/privsep-user "dhcpcd" "$(awk -F: '$1=="dhcpcd"{print $1}' /etc/passwd 2>/dev/null)" # ----------------------------------------------------------- the binary runs -- # --help and --version are answered in main() before anything else happens -- # argv[1] is compared literally, ahead of getopt, the config file and any socket # -- so a red in this block separates "the binary is broken" from every runtime # case below. db 30 "$DC" --version check_eq dhcpcd/version-rc "0" "$rc" case "$out" in "dhcpcd 10.0.6"*) bst_ok dhcpcd/version-is-10.0.6 ;; *) bst_notok dhcpcd/version-is-10.0.6 "want a leading 'dhcpcd 10.0.6', got '$(_bst_1line "$out")'" ;; esac # The features line is generated from the SAME #ifdefs that decide what compiled, # so it reads the configure outcome off this binary rather than off the recipe. # INET/ARP/ARPing/IPv4LL are the IPv4 client; the negatives are as load-bearing # as the positives and each has its own reason. PRIVSEP: --disable-privsep, per # the header. INET6/DHCPv6: PACKAGECONFIG = "" drops poky's ipv6 arm, so # configure gets --disable-ipv6 and half the coroutinized net surface with it. if has "$out" " INET" " ARP" " IPv4LL"; then bst_ok dhcpcd/features-inet else bst_notok dhcpcd/features-inet "the IPv4 client features are not all compiled in -- $(_bst_1line "$out")" fi case "$out" in *PRIVSEP*) bst_notok dhcpcd/features-no-privsep "PRIVSEP is compiled in, but this arm has no seccomp AUDIT_ARCH and no credential setters -- $(_bst_1line "$out")" ;; *) bst_ok dhcpcd/features-no-privsep ;; esac case "$out" in *INET6*|*DHCPv6*) bst_notok dhcpcd/features-no-inet6 "IPv6 is compiled in, but the kernel is built without CONFIG_IPV6 -- $(_bst_1line "$out")" ;; *) bst_ok dhcpcd/features-no-inet6 ;; esac db 30 "$DC" --help check_eq dhcpcd/help-rc "0" "$rc" case "$out" in *"usage: dhcpcd"*) bst_ok dhcpcd/help-text ;; *) bst_notok dhcpcd/help-text "usage text not as expected -- $(_bst_1line "$out")" ;; esac # An unknown option must be refused rather than ignored. getopt_long runs before # any socket or interface work, so this stays offline. db 20 "$DC" --no-such-option if [ "$rc" -eq 124 ]; then bst_notok dhcpcd/rejects-unknown-option "hung on a bad option -- $(_bst_1line "$out")" elif [ "$rc" -eq 0 ]; then bst_notok dhcpcd/rejects-unknown-option "accepted an unknown option -- $(_bst_1line "$out")" elif has "$out" "usage: dhcpcd"; then bst_ok dhcpcd/rejects-unknown-option else bst_notok dhcpcd/rejects-unknown-option "rejected (rc=$rc) but did not print the usage -- $(_bst_1line "$out")" fi # ------------------------------------------------- THE CRITERION, decoded ---- # A DHCPACK, built here field by field. 236 bytes of BOOTP header, the four-byte # magic cookie, then the options. read_lease() reads it straight off stdin and # hands it to the same option parser a received packet goes through, so what the # variables below say is what dhcpcd would have said about a real offer. { printf '\002\001\006\000' # op=BOOTREPLY htype=ETHER hlen=6 hops=0 printf '\336\255\276\357' # xid = deadbeef printf '\000\000\000\000' # secs, flags printf '\000\000\000\000' # ciaddr printf '\012\112\000\112' # yiaddr 10.74.0.74 -- the offered address printf '\012\112\000\001' # siaddr 10.74.0.1 printf '\000\000\000\000' # giaddr printf '\002\000\000\000\000\112'; zeros 10 # chaddr 02:00:00:00:00:4a zeros 64 # sname zeros 128 # file printf '\143\202\123\143' # magic cookie 63 82 53 63 printf '\065\001\005' # 53 message-type = 5 (DHCPACK) printf '\001\004\377\377\377\000' # 1 subnet-mask = 255.255.255.0 printf '\063\004\000\000\016\020' # 51 lease-time = 3600 printf '\066\004\012\112\000\001' # 54 server-id = 10.74.0.1 printf '\003\004\012\112\000\001' # 3 routers = 10.74.0.1 printf '\377' # 255 end } > "$D/lease.bin" # 236 bytes of header, 4 of cookie, 28 of options. Asserted rather than assumed: # a printf that dropped an escape would otherwise surface as a puzzling parse # failure several cases later, and DHCP_MIN_LEN (240) would reject anything # shorter outright -- which is the same red the truncated-lease case wants for a # different reason. check_eq dhcpcd/lease-crafted "268" "$(wc -c < "$D/lease.bin" | tr -d ' ')" db 60 sh -c "$DC -U -4 < '$D/lease.bin'" dump=$out if [ "$rc" -eq 124 ]; then bst_notok dhcpcd/lease-dump "dumplease hung -- $(_bst_1line "$dump")" elif bst_execfail "$dump"; then bst_unrun dhcpcd/lease-dump "$dump" elif [ "$rc" -ne 0 ]; then bst_notok dhcpcd/lease-dump "rc=$rc -- $(_bst_1line "$dump")" elif [ -z "$dump" ]; then # The one shape worth naming: dumplease reaches stdin through # ioctl(FIONREAD), and a kernel that answers 0 there makes dhcpcd skip the # whole block and exit 0 having printed nothing. bst_notok dhcpcd/lease-dump "exit 0 and no output: stdin was not seen, which is ioctl(FIONREAD) on a redirected file rather than a dhcpcd fault" else bst_ok dhcpcd/lease-dump fi # The three fields the ticket names, each read out of the dump by key. They are # scored separately on purpose: an address that survives while the netmask does # not is an option-parser fault, whereas all three failing together is the # transport case above. check_eq dhcpcd/lease-ip-address "$WANT_IP" "$(field "$dump" ip_address)" check_eq dhcpcd/lease-subnet-mask "$WANT_MASK" "$(field "$dump" subnet_mask)" check_eq dhcpcd/lease-time "$WANT_LEASE" "$(field "$dump" dhcp_lease_time)" # Corroboration from three more options, so a decoder that happened to get the # first three right by position rather than by code is still caught. check_eq dhcpcd/lease-server-id "$WANT_SRV" "$(field "$dump" dhcp_server_identifier)" check_eq dhcpcd/lease-routers "$WANT_SRV" "$(field "$dump" routers)" check_eq dhcpcd/lease-message-type "5" "$(field "$dump" dhcp_message_type)" # Derived, not carried: dhcpcd computes the CIDR and the broadcast address from # yiaddr and the mask, so these say the numbers were understood rather than # merely copied. check_eq dhcpcd/lease-cidr "24" "$(field "$dump" subnet_cidr)" check_eq dhcpcd/lease-broadcast "10.74.0.255" "$(field "$dump" broadcast_address)" # A decoder that only ever succeeds is not a decoder. DHCP_MIN_LEN is 240, so a # 100-byte input is refused as a truncated lease before any option is read. head -c 100 "$D/lease.bin" > "$D/short.bin" db 60 sh -c "$DC -U -4 < '$D/short.bin'" if [ "$rc" -eq 124 ]; then bst_notok dhcpcd/lease-refuses-truncated "hung on a truncated lease -- $(_bst_1line "$out")" elif [ "$rc" -eq 0 ] && has "$out" "ip_address=$WANT_IP"; then bst_notok dhcpcd/lease-refuses-truncated "a 100-byte lease was accepted and dumped an address" elif has "$out" "truncated lease"; then bst_ok dhcpcd/lease-refuses-truncated else bst_notok dhcpcd/lease-refuses-truncated "want the documented 'truncated lease' diagnostic, rc=$rc -- $(_bst_1line "$out")" fi # ------------------------------------------------- config file, parsed ------ # The ticket's second half. A config that is merely ACCEPTED proves nothing, so # the staged file carries an `env` line whose value must reappear in the dump: # ifo->environ is appended to the same buffer script_dump() prints, so the string # below can only be there if the file was opened, tokenised and acted on. cat > "$D/dhcpcd.conf" <` and returns # -1, which read_config IGNORES -- so this is scored on the DIAGNOSTIC and not on # the exit status, and asserting a non-zero rc here would be a red that reads # like a parser fault. printf 'no_such_directive yes\n' > "$D/bad.conf" db 60 sh -c "$DC -U -4 -f '$D/bad.conf' < '$D/lease.bin'" if has "$out" "unknown option: no_such_directive"; then bst_ok dhcpcd/conf-rejects-unknown else bst_notok dhcpcd/conf-rejects-unknown "want 'unknown option: no_such_directive', rc=$rc -- $(_bst_1line "$out")" fi # ------------------------------------------- what the platform withholds ---- # The two facts that stop the ticket's wire criterion, each read from the kernel # rather than inferred from a failed command, and each an ATTRIBUTED RED: dhcpcd # is not at fault and nothing here is a skip, because a skip would say the # subject was legitimately absent when what is absent is the transport. # # /proc/net/packet exists if and only if CONFIG_PACKET=y. dhcpcd has no fallback: # src/if-linux.c opens AF_PACKET for both the BPF read and the raw send, so # without it DISCOVER cannot leave the machine. if [ -e /proc/net/packet ]; then bst_ok dhcpcd/af-packet-available else bst_notok dhcpcd/af-packet-available "brintos/linux: the kernel is built '# CONFIG_PACKET is not set' (arch/wasm32/configs/wasm32_defconfig), so AF_PACKET cannot be opened and no DHCP request can be transmitted" fi # The other half: something to transmit on. `ip link add` is the cheapest honest # probe -- it either creates the pair or says why not -- and it returns at once, # so it is safe on a machine where a blocked command can never be killed # (brintos/linux#21). VETH_UP=0 if ! command -v ip >/dev/null 2>&1; then bst_notok dhcpcd/veth-pair "iproute2 ip is not on this image, so no second interface could even be attempted" else db 30 ip link add bstv0 type veth peer name bstv1 if [ "$rc" -eq 0 ]; then VETH_UP=1 bst_ok dhcpcd/veth-pair else bst_notok dhcpcd/veth-pair "brintos/linux: no veth driver in this kernel -- the wasm32 defconfig brings up the net stack LOOPBACK ONLY, so the ticket's veth pair cannot be created -- rc=$rc $(_bst_1line "$out")" fi fi # dhcpcd's own view of the same fact, and the one case here that exercises its # interface discovery for real. WHAT IT ASSERTS WAS WRONG UNTIL NOW AND THE # CORRECTION IS THE POINT: the IFF_LOOPBACK skip at src/if-linux.c:996 sits in # the RTM_NEWLINK/RTM_DELLINK *link-message* handler, not in if_discover(), which # enumerates through getifaddrs() and keeps loopback like any other interface. # dhcpcd does not refuse lo. dhcpcd_initstate1() (src/dhcpcd.c:463) sees # IFF_LOOPBACK, clears ARP and, absent --inform/--dhcp, turns the interface # STATIC -- so naming lo is accepted, no DISCOVER is ever composed, and the run # ends 0 having asked for nothing. That is the honest statement of why the # ticket's criterion has nowhere to run: the machine's only interface is one DHCP # is defined not to solicit on, which is a quieter fact than a refusal but the # same fact. # # One benign message rides along and is NOT scored: `dhcp_vendor: Invalid # argument`. if_machinearch() (src/if-linux.c:216) picks its /proc/cpuinfo key # from a fixed #elif chain of architectures, wasm32 is in none of them, so mproc # is NULL and it returns EINVAL. dhcp_vendor()'s caller only logerr()s it -- the # vendor-class option is left unset and nothing else changes. Same shape as the # seccomp AUDIT_ARCH chain this port drops privsep for, and non-fatal where that # one is a compile error. # # ONE WALL SITS IN FRONT OF IT AND IS SCORED APART. -T clears DHCPCD_DAEMONISE, # so main() jumps straight to start_manager and takes the pidfile lock there -- # a site that, unlike the one at line 2348, is NOT guarded by DHCPCD_TEST. The # -T branch also never fills ctx.pidfile, so pidfile_lock() falls back to # _PATH_VARRUN and lands on /run. If /run is read-only at browser runtime the # run ends there, having reached no interface at all: that is our own wall and # reporting it as the loopback rule would be a false green in one direction and # an unattributed red in the other. mkdir first, then tell the outcomes apart. mkdir -p /run 2>/dev/null db 60 "$DC" -T lo if [ "$rc" -eq 124 ]; then bst_notok dhcpcd/loopback-no-dhcp "hung instead of settling lo static -- $(_bst_1line "$out")" elif has "$out" "pidfile_lock"; then bst_notok dhcpcd/loopback-no-dhcp "stopped at the pidfile lock under /run before reaching interface discovery, which is this image's read-only /run rather than anything about dhcpcd or the loopback rule -- $(_bst_1line "$out")" elif has "$out" "if_discover"; then bst_notok dhcpcd/loopback-no-dhcp "interface discovery itself failed, so this is rtnetlink rather than the loopback rule -- $(_bst_1line "$out")" elif has "$out" "interface not found"; then bst_notok dhcpcd/loopback-no-dhcp "lo was not enumerated at all, so getifaddrs reported no AF_PACKET entry for it -- $(_bst_1line "$out")" elif [ "$rc" -ne 0 ]; then bst_notok dhcpcd/loopback-no-dhcp "reached discovery but exited rc=$rc -- $(_bst_1line "$out")" elif ! has "$out" "DUID"; then bst_notok dhcpcd/loopback-no-dhcp "never reached DUID/discovery, so it stopped earlier than this case can name -- $(_bst_1line "$out")" elif has "$out" "soliciting"; then bst_notok dhcpcd/loopback-no-dhcp "composed a DHCP request on a loopback interface, which dhcpcd_initstate1 is supposed to have made STATIC -- $(_bst_1line "$out")" else bst_ok dhcpcd/loopback-no-dhcp fi # THE TICKET'S CRITERION ON THE WIRE. It runs only if the pair above came up; # otherwise it is the attributed red that says so, since a skip would claim the # subject was absent when it is the interface that is. Written out in full # deliberately: the day CONFIG_PACKET and veth land, this flips green with no # edit here. if [ "$VETH_UP" -ne 1 ]; then bst_notok dhcpcd/wire-lease "not attempted: no veth pair (see dhcpcd/veth-pair) and no AF_PACKET (see dhcpcd/af-packet-available), which is the platform gap this port surfaces rather than a dhcpcd fault" elif ! command -v dnsmasq >/dev/null 2>&1; then bst_notok dhcpcd/wire-lease "dnsmasq is not on this image, so no DHCP server could be stood up on the peer" else ip link set bstv0 up >/dev/null 2>&1 ip link set bstv1 up >/dev/null 2>&1 ip addr add "$WANT_SRV/24" dev bstv1 >/dev/null 2>&1 cat > "$D/dhcp.conf" < "$D/dnsmasq.log" 2>&1 & dpid=$! i=0 while [ $i -lt 30 ]; do grep -q "started, version" "$D/dnsmasq.log" 2>/dev/null && break kill -0 "$dpid" 2>/dev/null || break i=$((i+1)); sleep 1 done # -T is test mode: it prints what it would configure and configures nothing. # -K drops link detection, -L drops IPv4LL, -t 20 bounds the wait from inside # dhcpcd rather than relying on a wrapper that cannot signal it. db 120 "$DC" -T -K -L -t 20 bstv0 if [ "$rc" -eq 124 ]; then bst_notok dhcpcd/wire-lease "dhcpcd -T hung -- $(_bst_1line "$out")" elif [ "$(field "$out" ip_address)" = "$WANT_IP" ] && \ [ "$(field "$out" subnet_mask)" = "$WANT_MASK" ] && \ [ "$(field "$out" dhcp_lease_time)" = "$WANT_LEASE" ]; then bst_ok dhcpcd/wire-lease else bst_notok dhcpcd/wire-lease "no lease over the pair, rc=$rc -- $(_bst_1line "$out")" fi kill -TERM "$dpid" 2>/dev/null ip link del bstv0 >/dev/null 2>&1 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 -- the # runner counts what it saw and would otherwise report a complete green. bst_ok dhcpcd/suite-complete rm -rf "$D"