. "$BST_LIB" require wg wireguard-tools/present # BST-TIMEOUT: 900 # ~120 execs, seven of which start bash (wg-quick) # wireguard-tools 1.0.20210914 (meta-networking recipes-kernel/wireguard) -- # `wg', which configures and inspects WireGuard interfaces over generic netlink, # and `wg-quick', the bash driver that brings one up from a config file. # # THERE IS NO WIREGUARD INTERFACE ON THIS MACHINE, AND THAT IS THE ASSERTION. # The kernel is a single vmlinux.wasm with no loadable modules, so the # `wireguard' generic-netlink family is never registered and no device of that # kind can be created. The recipe only RRECOMMENDS kernel-module-wireguard (this # port clears it, because the name RPROVIDES nothing here), so nothing in the # port waits on the module -- what the port has to be is DETERMINISTIC in its # absence, and that is what this suite measures. # # So the suite splits three ways: # # PURE USERLAND, asserted for real and needing no interface, no netlink and no # privilege -- the version and help banners with their streams, the key # arithmetic (genkey/pubkey/genpsk) against fixed vectors, the malformed-input # rejection, wg's own configuration-file PARSER, and every wg-quick path that # ends before it touches a device. This is most of the reachable surface and # all of the part that computes anything. # # THE NETLINK HALF, asserted as the documented refusal. `wg show', `wg show # all' and `wg show interfaces' enumerate through RTNETLINK (RTM_GETLINK) and # are asserted to report NO INTERFACE with status 0 and no output -- not to # fail. Everything that names a device goes through GENERIC netlink instead # and is asserted to refuse with the documented message and status 1. The two # halves take different netlink protocols on purpose: the rtnetlink core comes # in unconditionally with CONFIG_NET (88-babeld.test measures it usable on # this image, and 60-net.test reads lo through iproute2), while the # `wireguard' genl FAMILY does not exist here at all. # # PINNED AS PACKAGING FACTS -- what the port ships and what it deliberately # does not: wg as a wasm module rather than a host binary, wg-quick present at # all (it reaches the image only through a PACKAGECONFIG), the two man pages, # the two bash completions, /etc/wireguard, and the ABSENCE of the systemd # units, which upstream's Makefile decides from the BUILD HOST's own # /lib/systemd/system. # # EVERY EXPECTED VALUE BELOW WAS EXECUTED AGAINST wireguard-tools 1.0.20210914 # BUILT FROM THE PINNED SRCREV (3ba65271) ON AN x86-64 HOST BEFORE IT WAS # WRITTEN DOWN, rc and output compared. The refusals were measured against a # control built with the genl family name changed to one no kernel registers, # which is byte for byte the world this image is in -- and the errno is the same # either way, because mnlg_socket_open() translates the family lookup's ENOENT # into EPROTONOSUPPORT and a kernel with no NETLINK_GENERIC at all would return # EPROTONOSUPPORT from socket(2) directly. # # THE CURVE25519 VECTORS ARE THE ONE PLACE THIS ARM COULD BE SILENTLY WRONG. # curve25519.c picks its implementation on __SIZEOF_INT128__: an x86-64 host # takes curve25519-hacl64.h, and wasm32 takes curve25519-fiat32.h -- a different # body of arithmetic over 32-bit limbs. BRINTOS-217 is why that matters here # (oniguruma compiled and packaged perfectly on this arm and then returned # silently wrong offsets to every caller). So the vectors below were taken from # BOTH: the native hacl64 build and a native build with the 128-bit arm compiled # out, which is the same source path wasm32 takes. The two agreed exactly, which # is what makes a disagreement here a finding about this arm rather than about # the expectation. W=${TMPDIR:-/tmp}/bst-wireguard-tools.$$ mkdir -p "$W" # The endpoint resolver retries a TRANSIENT getaddrinfo(3) failure fifteen times # with a 1s..20s backoff, which on a bad day is several minutes inside one case. # This image's getaddrinfo has a known broken DNS leg # (linux-in-the-browser#96), and while every endpoint below is a NUMERIC address # that should never reach it, a suite must not be able to spend its whole bound # discovering that. Zero retries makes the first answer the answer. WG_ENDPOINT_RESOLUTION_RETRIES=0 export WG_ENDPOINT_RESOLUTION_RETRIES # _case : ok iff exits AND its # combined output contains . bst.sh has check_out for the rc-0 form and # check_rc for the status alone; nearly every assertion here needs both halves, # because wg answers most things with status 1 and the whole of the information # is in the string. _case() { _cid=$1; _cwant=$2; _cn=$3; shift 3 _co=$("$@" 2>&1); _cr=$? case "$_co" in *"$_cn"*) _ch=1 ;; *) _ch=0 ;; esac if [ "$_cr" -eq "$_cwant" ] && [ "$_ch" -eq 1 ]; then bst_ok "$_cid" elif bst_execfail "$_co"; then bst_unrun "$_cid" "$_co" else bst_notok "$_cid" "rc=$_cr want=$_cwant needle:'$_cn' got:'$(_bst_1line "$_co")'"; fi } # --- what the package ships -------------------------------------------------- check wgt/wg-path test -x /usr/bin/wg check_out wgt/wg-on-path "/usr/bin/wg" command -v wg check wgt/wg-quick-path test -x /usr/bin/wg-quick check_out wgt/wg-quick-on-path "/usr/bin/wg-quick" command -v wg-quick # wg is compiled C and must be a wasm module; wg-quick is a bash script and must # not be. do_package_qa inspects ELF and is content with anything that is not # one, so a host binary installed by a Makefile that found a stale object would # package cleanly and fail at exec(2) and nowhere earlier. check_eq wgt/wg-wasm "00 61 73 6d" "$(od -An -tx1 -N4 /usr/bin/wg 2>/dev/null | tr -s ' ' | sed 's/^ //;s/ $//')" check wgt/wg-not-elf test "$(od -An -tx1 -N4 /usr/bin/wg 2>/dev/null | tr -s ' ' | sed 's/^ //;s/ $//')" != "7f 45 4c 46" _sb=$(head -n 1 /usr/bin/wg-quick 2>/dev/null) case "$_sb" in "#!"*bash) bst_ok wgt/wg-quick-is-bash ;; *) bst_notok wgt/wg-quick-is-bash "first line is not a bash shebang: '$(_bst_1line "$_sb")'" ;; esac check wgt/man-wg test -s /usr/share/man/man8/wg.8 check wgt/man-wg-quick test -s /usr/share/man/man8/wg-quick.8 check wgt/completion-wg test -s /usr/share/bash-completion/completions/wg check wgt/completion-wg-quick test -s /usr/share/bash-completion/completions/wg-quick # /etc/wireguard is created by the Makefile at mode 0700 and is where wg-quick # looks up a bare interface name. Both halves are asserted: an interface-name # argument resolves against this directory, and the mode is what keeps a private # key out of every other account's reach. check wgt/etc-wireguard test -d /etc/wireguard check_eq wgt/etc-wireguard-mode "700" "$(stat -c %a /etc/wireguard 2>/dev/null)" # The systemd units are NOT shipped, and their absence is this port's own doing. # Upstream's Makefile enables them from `$(wildcard $(SYSTEMDUNITDIR))' -- an # unprefixed /lib/systemd/system, which is a path on the BUILD MACHINE rather # than under ${D} -- so on a builder that runs systemd the package silently # grows two unit files for an init manager this image does not have. The # bbappend removes them; this is the case that says so on the target. if [ -e /lib/systemd/system/wg-quick@.service ] || [ -e /lib/systemd/system/wg-quick.target ]; then bst_notok wgt/no-systemd-units "the build host's own /lib/systemd/system leaked wg-quick units into a distro with no systemd" else bst_ok wgt/no-systemd-units fi # --- the version and help banners, with their streams ------------------------ # The version string is asserted as a PREFIX. The Makefile prefers `git describe # --dirty' over version.h, so a fetch that carries the tag and a fetch that does # not both have to satisfy this, and a `-dirty' suffix must not red it. _case wgt/version-text 0 "wireguard-tools v1.0.20210914" wg --version _case wgt/version-url 0 "https://git.zx2c4.com/wireguard-tools/" wg --version _case wgt/version-v 0 "wireguard-tools v1.0.20210914" wg -v # `wg help' goes to STDOUT and exits 0; an INVALID subcommand prints the same # banner to STDERR and exits 1. Both streams are pinned, because a port that # "fixed" either would break every script that pipes one of them. _case wgt/help-rc 0 "Usage: wg []" wg help _case wgt/help-long 0 "Usage: wg []" wg --help _case wgt/help-short 0 "Usage: wg []" wg -h _ho=$(wg help 2>/dev/null); _he=$(wg help 2>&1 >/dev/null) case "$_ho" in *"Usage: wg "*) bst_ok wgt/help-on-stdout ;; *) bst_notok wgt/help-on-stdout "banner not on stdout: '$(_bst_1line "$_ho")'" ;; esac if [ -z "$_he" ]; then bst_ok wgt/help-stderr-empty else bst_notok wgt/help-stderr-empty "help wrote to stderr: '$(_bst_1line "$_he")'"; fi # Every subcommand the banner documents, swept as a table. A subcommand dropped # from subcommands.h vanishes from the banner AND from the dispatcher together, # and no single-name case would notice unless it happened to be the one that # went. _hb=$(wg help 2>&1) _hmiss= for _s in show showconf set setconf addconf syncconf genkey genpsk pubkey; do case "$_hb" in *" $_s: "*) ;; *) _hmiss="$_hmiss $_s" ;; esac done if [ -z "$_hmiss" ]; then bst_ok wgt/help-subcommand-table else bst_notok wgt/help-subcommand-table "not documented:$_hmiss"; fi _case wgt/invalid-subcommand 1 "Invalid subcommand: \`bogus'" wg bogus _io=$(wg bogus 2>/dev/null) if [ -z "$_io" ]; then bst_ok wgt/invalid-subcommand-on-stderr else bst_notok wgt/invalid-subcommand-on-stderr "reached stdout: '$(_bst_1line "$_io")'"; fi # Each subcommand's own usage line, and its status. They are NOT uniform, and # that is the point of taking them one at a time: `show --help' is handled and # exits 0, `showconf --help' is not handled at all and treats --help as an # INTERFACE NAME (so it reaches netlink and refuses), and the rest print usage # and exit 1. _case wgt/usage-show 0 "Usage: wg show { | all | interfaces }" wg show --help _case wgt/usage-set 1 "Usage: wg set " wg set --help _case wgt/usage-setconf 1 "Usage: wg setconf " wg setconf --help _case wgt/usage-addconf 1 "Usage: wg addconf " wg addconf --help _case wgt/usage-syncconf 1 "Usage: wg syncconf " wg syncconf --help _case wgt/usage-genkey 1 "Usage: wg genkey" wg genkey --help _case wgt/usage-genpsk 1 "Usage: wg genpsk" wg genpsk --help _case wgt/usage-pubkey 1 "Usage: wg pubkey" wg pubkey --help # Too many arguments takes the same usage path, from a different call site. _case wgt/usage-show-extra-args 1 "Usage: wg show {" wg show wgbst0 dump extra # --- the key arithmetic ------------------------------------------------------ # THE FIXED VECTORS. Both private keys and their public keys are the pair the # wg(8) manual uses in its own example, and both were re-derived here from the # 32-bit fiat arm as well as the 64-bit hacl one. A curve25519 that is wrong on # this arm reds HERE and nowhere else -- everything downstream of it (a # handshake) needs an interface this machine cannot have. _case wgt/pubkey-vector-1 0 "HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=" \ sh -c "printf '%s\n' 'yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=' | wg pubkey" _case wgt/pubkey-vector-2 0 "gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA=" \ sh -c "printf '%s\n' 'gI6EdUSYvn8ugXOt8QQD6Yc+JyiZxIhp3GInSWRfWGE=' | wg pubkey" # A third vector chosen for its shape rather than its provenance: the scalar # whose only set bit is the one clamping forces on, so the base-point # multiplication is a single doubling chain with no addition. It separates a # ladder that is wrong in its conditional swap from one that is wrong in its # field arithmetic. _case wgt/pubkey-vector-clamped-min 0 "L+V9o0fNYkMVKNqsX7spBzD/9oSvxM/C7ZCZX1jLO3Q=" \ sh -c "printf '%s\n' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEA=' | wg pubkey" # pubkey is a FUNCTION: the same private key gives the same public key twice. # Asserted separately from the vectors, because a build that returned a fresh # wrong answer each time and one that returned the same wrong answer twice are # different defects. _p1=$(printf '%s\n' 'yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=' | wg pubkey 2>&1) _p2=$(printf '%s\n' 'yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=' | wg pubkey 2>&1) check_eq wgt/pubkey-deterministic "$_p1" "$_p2" # genkey: 44 base64 characters, and CLAMPED. The clamp is curve25519's, applied # in genkey.c before the key is printed: byte 0 has its low three bits cleared # and byte 31 is forced into [64,127]. It is asserted on the decoded bytes # rather than on the base64, because that is where it is stated. # _b64key : true iff is a 44-character base64 key ending in '=' -- the # only shape 32 bytes can take. ${#s} rather than a wildcard run of 44, so the # length is a number a reader can check rather than one they have to count. _b64key() { [ ${#1} -eq 44 ] || return 1 case "$1" in *"=") ;; *) return 1 ;; esac return 0 } _gk=$(wg genkey 2>&1) if _b64key "$_gk"; then bst_ok wgt/genkey-length else bst_notok wgt/genkey-length "not 43 base64 chars + '=': '$(_bst_1line "$_gk")'"; fi case "$_gk" in *[!A-Za-z0-9+/=]*) bst_notok wgt/genkey-charset "not base64: '$(_bst_1line "$_gk")'" ;; *) bst_ok wgt/genkey-charset ;; esac _b0=$(printf '%s' "$_gk" | base64 -d 2>/dev/null | od -An -tu1 -N1 | tr -d ' \n') _b31=$(printf '%s' "$_gk" | base64 -d 2>/dev/null | od -An -tu1 -j31 -N1 | tr -d ' \n') case "$_b0" in ''|*[!0-9]*) bst_notok wgt/genkey-clamp-low "could not read byte 0 of '$(_bst_1line "$_gk")'" ;; *) if [ $((_b0 % 8)) -eq 0 ]; then bst_ok wgt/genkey-clamp-low else bst_notok wgt/genkey-clamp-low "byte 0 is $_b0, not a multiple of 8"; fi ;; esac case "$_b31" in ''|*[!0-9]*) bst_notok wgt/genkey-clamp-high "could not read byte 31 of '$(_bst_1line "$_gk")'" ;; *) if [ "$_b31" -ge 64 ] && [ "$_b31" -le 127 ]; then bst_ok wgt/genkey-clamp-high else bst_notok wgt/genkey-clamp-high "byte 31 is $_b31, outside [64,127]"; fi ;; esac # Two genkeys differ. This is the whole of what can be asserted about the CSPRNG # from outside; it fails on the one failure mode that matters, a getrandom(2) # that returns the same buffer (or none) every time. _gk2=$(wg genkey 2>&1) if [ "$_gk" != "$_gk2" ]; then bst_ok wgt/genkey-distinct else bst_notok wgt/genkey-distinct "two genkey calls returned the same key: '$(_bst_1line "$_gk")'"; fi # A generated private key round-trips through pubkey. Nothing about the value is # known in advance, so what is asserted is the shape and that it is not the # input. _gp=$(printf '%s\n' "$_gk" | wg pubkey 2>&1) if _b64key "$_gp"; then bst_ok wgt/genkey-pubkey-shape else bst_notok wgt/genkey-pubkey-shape "not a 44-char key: '$(_bst_1line "$_gp")'"; fi if [ "$_gp" != "$_gk" ]; then bst_ok wgt/genkey-pubkey-differs else bst_notok wgt/genkey-pubkey-differs "pubkey returned its own input"; fi # genpsk is a DIFFERENT generator: 32 random bytes with no clamp. Sharing # genkey's code path would be invisible in the length and the charset, so what # separates them is the clamp -- over five samples, at least one must violate # it. A clamped byte pair occurs by chance in one draw in 32, so five clean # draws is a one-in-thirty-three-million coincidence and a defect every other # time. _pskclamped=0; _psktotal=0; _pskprev=; _pskdistinct=1 for _i in 1 2 3 4 5; do _psk=$(wg genpsk 2>&1) if ! _b64key "$_psk"; then bst_notok wgt/genpsk-length "not 43 base64 chars + '=': '$(_bst_1line "$_psk")'" _psktotal=-1; break fi [ -n "$_pskprev" ] && [ "$_pskprev" = "$_psk" ] && _pskdistinct=0 _pskprev=$_psk _pb0=$(printf '%s' "$_psk" | base64 -d 2>/dev/null | od -An -tu1 -N1 | tr -d ' \n') _pb31=$(printf '%s' "$_psk" | base64 -d 2>/dev/null | od -An -tu1 -j31 -N1 | tr -d ' \n') case "$_pb0$_pb31" in ''|*[!0-9]*) continue ;; esac if [ $((_pb0 % 8)) -eq 0 ] && [ "$_pb31" -ge 64 ] && [ "$_pb31" -le 127 ]; then _pskclamped=$((_pskclamped + 1)) fi _psktotal=$((_psktotal + 1)) done if [ "$_psktotal" -eq 5 ]; then bst_ok wgt/genpsk-length; fi if [ "$_psktotal" -eq 5 ] && [ "$_pskdistinct" -eq 1 ]; then bst_ok wgt/genpsk-distinct elif [ "$_psktotal" -eq 5 ]; then bst_notok wgt/genpsk-distinct "two genpsk calls returned the same key"; fi if [ "$_psktotal" -eq 5 ] && [ "$_pskclamped" -lt 5 ]; then bst_ok wgt/genpsk-unclamped elif [ "$_psktotal" -eq 5 ]; then bst_notok wgt/genpsk-unclamped "all five preshared keys satisfied curve25519 clamping -- genpsk is sharing genkey's clamp"; fi # --- malformed key input ----------------------------------------------------- # One message, four ways in. key_from_base64() checks the length first and the # alphabet second, and both land on the same diagnostic; what changes is only # which check refused, so the cases are separate and the needle is shared. for _bad in notakey "" "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBm" "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBm!="; do case "$_bad" in notakey) _bid=short ;; "") _bid=empty ;; *"!="*) _bid=bad-alphabet ;; *) _bid=truncated ;; esac _bo=$(printf '%s\n' "$_bad" | wg pubkey 2>&1); _br=$? case "$_bo" in *"Key is not the correct length or format"*) if [ "$_br" -eq 1 ]; then bst_ok "wgt/pubkey-rejects-$_bid" else bst_notok "wgt/pubkey-rejects-$_bid" "right message, rc=$_br want=1"; fi ;; *) if bst_execfail "$_bo"; then bst_unrun "wgt/pubkey-rejects-$_bid" "$_bo" else bst_notok "wgt/pubkey-rejects-$_bid" "rc=$_br got:'$(_bst_1line "$_bo")'"; fi ;; esac done # The diagnostic is prefixed with the program name and goes to stderr. _ko=$(printf 'notakey\n' | wg pubkey 2>/dev/null) if [ -z "$_ko" ]; then bst_ok wgt/pubkey-error-on-stderr else bst_notok wgt/pubkey-error-on-stderr "reached stdout: '$(_bst_1line "$_ko")'"; fi _case wgt/pubkey-error-prefixed 1 "wg: Key is not the correct length or format" \ sh -c "printf 'notakey\n' | wg pubkey" # --- wg's configuration-file parser, which touches no device ----------------- # config.c decides every one of these before ipc_set_device() is reached, so # they are real assertions on a machine with no WireGuard interface. The three # subcommands that take a file share the parser and are asserted through it once # each, because they reach it from three different call sites. printf '%s\n' '[Interface]' 'PrivateKey = notakey' > "$W/badkey.conf" printf '%s\n' '[Interface]' 'PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=' \ 'Address = 10.66.66.1/24' > "$W/wgquickonly.conf" printf '%s\n' '[Interface]' 'PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=' \ 'ListenPort = notaport' > "$W/badport.conf" printf '%s\n' '[Interface]' 'PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=' \ 'ListenPort = 51820' '' '[Peer]' \ 'PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA=' \ 'AllowedIPs = 10.66.66.2/32' > "$W/valid.conf" printf '%s\n' '[Interface]' 'PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=' \ 'ListenPort = 51820' '' '[Peer]' \ 'PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA=' \ 'AllowedIPs = 10.66.66.2/32' 'Endpoint = 192.0.2.1:51820' > "$W/endpoint.conf" printf '%s\n' '[Interface]' 'PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=' \ 'ListenPort = 51820' '' '[Peer]' \ 'PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA=' \ 'AllowedIPs = 10.66.66.2/32' 'Endpoint = 192.0.2.1' > "$W/noport.conf" _case wgt/conf-bad-key 1 "Key is not the correct length or format: \`notakey'" wg setconf wgbst0 "$W/badkey.conf" _case wgt/conf-bad-key-fatal 1 "Configuration parsing error" wg setconf wgbst0 "$W/badkey.conf" _case wgt/conf-bad-port 1 "Configuration parsing error" wg setconf wgbst0 "$W/badport.conf" # The wg-quick-only keys are wg's business to REJECT: Address, DNS, MTU and Table # are handled by the script and are not part of the interface configuration, so # a config file passed straight to `wg setconf' must not be silently accepted # with them in it. _case wgt/conf-rejects-address 1 "Line unrecognized: \`Address=10.66.66.1/24'" wg setconf wgbst0 "$W/wgquickonly.conf" _case wgt/conf-endpoint-no-port 1 "Unable to find port of endpoint: \`192.0.2.1'" wg setconf wgbst0 "$W/noport.conf" _case wgt/conf-missing-file 1 "No such file or directory" wg setconf wgbst0 "$W/does-not-exist.conf" # addconf and syncconf reach the same parser from their own call sites. _case wgt/addconf-parses 1 "Line unrecognized: \`Address=10.66.66.1/24'" wg addconf wgbst0 "$W/wgquickonly.conf" _case wgt/syncconf-parses 1 "Line unrecognized: \`Address=10.66.66.1/24'" wg syncconf wgbst0 "$W/wgquickonly.conf" # A peer key given on the COMMAND line takes a different path into the same # check, in set.c rather than config.c. _case wgt/set-bad-peer-key 1 "Key is not the correct length or format: \`notakey'" wg set wgbst0 peer notakey # --- enumeration: rtnetlink says there is no WireGuard interface ------------- # `wg show' with no argument, `wg show all' and `wg show interfaces' all call # ipc_list_devices(), which opens NETLINK_ROUTE and dumps RTM_GETLINK. On this # image that dump SUCCEEDS and returns no wireguard-kind link, so the documented # answer is status 0 and no output -- not a failure. A red here is the netlink # open or the dump failing, which is a platform finding rather than a port one, # and it is stated as a separate case from the emptiness for exactly that # reason. _lo=$(wg show interfaces 2>&1); _lr=$? if [ "$_lr" -eq 0 ]; then bst_ok wgt/show-interfaces-rc elif bst_execfail "$_lo"; then bst_unrun wgt/show-interfaces-rc "$_lo" else bst_notok wgt/show-interfaces-rc "rc=$_lr -- rtnetlink enumeration failed: '$(_bst_1line "$_lo")'"; fi if [ -z "$_lo" ]; then bst_ok wgt/show-interfaces-empty else bst_notok wgt/show-interfaces-empty "an interface was reported: '$(_bst_1line "$_lo")'"; fi _ao=$(wg show all 2>&1); _ar=$? if [ "$_ar" -eq 0 ]; then bst_ok wgt/show-all-rc elif bst_execfail "$_ao"; then bst_unrun wgt/show-all-rc "$_ao" else bst_notok wgt/show-all-rc "rc=$_ar: '$(_bst_1line "$_ao")'"; fi if [ -z "$_ao" ]; then bst_ok wgt/show-all-empty else bst_notok wgt/show-all-empty "output on an image with no interface: '$(_bst_1line "$_ao")'"; fi # A bare `wg' is `wg show', not a usage banner -- main() dispatches argc==1 to # show_main(). It is pinned because it is the first thing anyone types. _no=$(wg 2>&1); _nr=$? if [ "$_nr" -eq 0 ] && [ -z "$_no" ]; then bst_ok wgt/bare-wg-is-show elif bst_execfail "$_no"; then bst_unrun wgt/bare-wg-is-show "$_no" else bst_notok wgt/bare-wg-is-show "rc=$_nr out:'$(_bst_1line "$_no")'"; fi _dmo=$(wg show all dump 2>&1); _dmr=$? if [ "$_dmr" -eq 0 ] && [ -z "$_dmo" ]; then bst_ok wgt/show-all-dump-empty elif bst_execfail "$_dmo"; then bst_unrun wgt/show-all-dump-empty "$_dmo" else bst_notok wgt/show-all-dump-empty "rc=$_dmr out:'$(_bst_1line "$_dmo")'"; fi # --- naming a device: generic netlink refuses, and says so ------------------- # Everything that names an interface goes through mnlg_socket_open("wireguard"), # which resolves the generic-netlink FAMILY. No such family is registered on # this kernel, so CTRL_CMD_GETFAMILY answers ENOENT and netlink.h translates # that to EPROTONOSUPPORT -- "Protocol not supported". A kernel with no # NETLINK_GENERIC at all would give the same errno from socket(2), so the case # does not distinguish them and does not need to. # # The block is taken only in the probed-absent world. A machine that grew a # WireGuard device would apply these instead of refusing them, and that is not a # regression of this port. if [ -n "$_lo" ]; then _why="a WireGuard interface is present on this image ($(_bst_1line "$_lo")), so these operations are applied rather than refused" for _skid in show-device showconf-device setconf-device set-device refusal-errno \ showconf-help-is-an-interface; do bst_skip "wgt/$_skid" "$_why" done else # Each names its own call site's message, so a `showconf' that had fallen # into show_main(), or a `setconf' that reached the device before its # parser, reads differently and reds. _case wgt/show-device 1 "Unable to access interface" wg show wgbst0 _case wgt/showconf-device 1 "Unable to access interface" wg showconf wgbst0 _case wgt/setconf-device 1 "Unable to modify interface" wg setconf wgbst0 "$W/valid.conf" _case wgt/set-device 1 "Unable to modify interface" wg set wgbst0 listen-port 51820 # The errno itself, asserted ONCE, so that a kernel which starts refusing # differently reds here alone instead of reddening the whole block and # hiding which half moved. _case wgt/refusal-errno 1 "Unable to access interface: Protocol not supported" wg show wgbst0 # showconf handles no --help of its own: subcommands.h routes the whole of # argv to showconf_main(), which takes argv[1] as an INTERFACE NAME. So # `wg showconf --help' asks the kernel about a device called `--help' and # refuses. It is pinned rather than treated as a bug, because a script that # probed for help support would otherwise read the refusal as the interface # being unavailable. _case wgt/showconf-help-is-an-interface 1 "Unable to access interface" wg showconf --help fi # A valid endpoint is a separate case from the refusal above: it is the one # assertion that the endpoint parser resolved a NUMERIC address rather than # falling into getaddrinfo's broken DNS leg. Reaching the netlink refusal is # proof the whole file parsed, including the endpoint. if [ -n "$_lo" ]; then bst_skip wgt/conf-numeric-endpoint "a WireGuard interface is present, so setconf is applied rather than refused" else _case wgt/conf-numeric-endpoint 1 "Unable to modify interface" wg setconf wgbst0 "$W/endpoint.conf" fi # --- wg-quick ---------------------------------------------------------------- # The bash driver. Its whole reachable surface here is the part that ends before # a device is created, and that part is most of it: option parsing, the config # file's name rules, the `strip' transform, and the two subcommands that check # for the interface first and give up. require wg-quick wgt/wg-quick-runs # The fixture is written at 0600 so `wg-quick' does not print its # world-accessible warning; the warning is correct behaviour and would only make # every needle below have to allow for it. printf '%s\n' '[Interface]' \ 'PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=' \ 'ListenPort = 51820' \ 'Address = 10.66.66.1/24' \ 'MTU = 1420' \ '' '[Peer]' \ 'PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA=' \ 'AllowedIPs = 10.66.66.2/32' \ 'Endpoint = 192.0.2.1:51820' > "$W/wgbst0.conf" chmod 600 "$W/wgbst0.conf" cp "$W/wgbst0.conf" "$W/wgbst0.cfg"; chmod 600 "$W/wgbst0.cfg" _case wgq/help-rc 0 "Usage: wg-quick [ up | down | save | strip ]" wg-quick --help _case wgq/help-h 0 "Usage: wg-quick [ up | down | save | strip ]" wg-quick -h # No arguments is the SAME banner with status 1. Both halves are pinned: a # wrapper script that tested wg-quick for success would otherwise read a # usage-only run as a successful one. _case wgq/no-args-usage 1 "Usage: wg-quick [ up | down | save | strip ]" wg-quick _case wgq/bad-verb-usage 1 "Usage: wg-quick [ up | down | save | strip ]" wg-quick bogus "$W/wgbst0.conf" # The banner is on stderr, all of it, including the rc-0 --help form. _qo=$(wg-quick --help 2>/dev/null) if [ -z "$_qo" ]; then bst_ok wgq/help-on-stderr else bst_notok wgq/help-on-stderr "banner reached stdout: '$(_bst_1line "$_qo")'"; fi # The config file's NAME is a rule, not a convenience: the interface name is # taken from it by regex, so a file that is not .conf has no interface # to name and wg-quick refuses before reading a byte of it. _case wgq/config-must-be-dot-conf 1 "The config file must be a valid interface name, followed by .conf" \ wg-quick up "$W/wgbst0.cfg" _case wgq/config-must-exist 1 "does not exist" wg-quick up "$W/no-such-interface.conf" # A BARE interface name resolves against /etc/wireguard, which is the directory # the package created. The message names the resolved path, which is what proves # the resolution happened. _case wgq/bare-name-resolves 1 "\`/etc/wireguard/wgbstx.conf' does not exist" wg-quick up wgbstx # `strip' is the whole of wg-quick that COMPUTES something: it emits the config # with the four keys wg does not understand removed, which is exactly what # cmd_up feeds to `wg setconf'. It needs no device and no privilege beyond # reading the file. _so=$(wg-quick strip "$W/wgbst0.conf" 2>/dev/null); _sr=$? if [ "$_sr" -eq 0 ]; then bst_ok wgq/strip-rc elif bst_execfail "$_so"; then bst_unrun wgq/strip-rc "$_so" else bst_notok wgq/strip-rc "rc=$_sr: '$(_bst_1line "$_so")'"; fi _smissing=; _skept= for _k in 'PrivateKey' 'ListenPort' 'PublicKey' 'AllowedIPs' 'Endpoint' '[Interface]' '[Peer]'; do case "$_so" in *"$_k"*) ;; *) _smissing="$_smissing $_k" ;; esac done for _k in 'Address' 'MTU'; do case "$_so" in *"$_k"*) _skept="$_skept $_k" ;; esac done if [ -z "$_smissing" ]; then bst_ok wgq/strip-keeps-interface-keys else bst_notok wgq/strip-keeps-interface-keys "dropped:$_smissing"; fi if [ -z "$_skept" ]; then bst_ok wgq/strip-drops-wg-quick-keys else bst_notok wgq/strip-drops-wg-quick-keys "left in:$_skept -- wg setconf would reject them"; fi # The strip output is what cmd_up hands to `wg setconf', so it must parse. On # this image that parse ends at the netlink refusal, which is the proof it got # all the way through the file. printf '%s\n' "$_so" > "$W/stripped.conf" if [ -n "$_lo" ]; then bst_skip wgq/strip-output-parses "a WireGuard interface is present, so setconf is applied rather than refused" else _case wgq/strip-output-parses 1 "Unable to modify interface" wg setconf wgbst0 "$W/stripped.conf" fi # `down' and `save' both begin by asking `wg show interfaces' whether the # interface is one of ours, and give up naming it when it is not. That is the # ticket's assertion in wg-quick's own words: it reports the missing interface # and exits, rather than trying to tear down something that is not there. _case wgq/down-reports-missing 1 "wg-quick: \`wgbst0' is not a WireGuard interface" wg-quick down "$W/wgbst0.conf" _case wgq/save-reports-missing 1 "wg-quick: \`wgbst0' is not a WireGuard interface" wg-quick save "$W/wgbst0.conf" # `up' is the one path that reaches the kernel. It asks `ip link add wgbst0 type # wireguard', which cannot succeed without the module; add_if() then finds no # /sys/module/wireguard and no userspace implementation and gives up, and the # EXIT trap tears down whatever had been created. What is asserted is that it # ANNOUNCES the attempt, fails, and LEAVES NOTHING BEHIND -- the last of those # is the ticket's requirement and is checked afterwards, from the outside. _uo=$(wg-quick up "$W/wgbst0.conf" 2>&1); _ur=$? case "$_uo" in *"ip link add wgbst0 type wireguard"*) bst_ok wgq/up-announces-link-add ;; *) if bst_execfail "$_uo"; then bst_unrun wgq/up-announces-link-add "$_uo" else bst_notok wgq/up-announces-link-add "rc=$_ur got:'$(_bst_1line "$_uo")'"; fi ;; esac if [ "$_ur" -ne 0 ]; then bst_ok wgq/up-fails-without-module else bst_notok wgq/up-fails-without-module "wg-quick up succeeded on a kernel with no wireguard support"; fi # Nothing left behind: no interface in sysfs, and nothing wg can enumerate. if [ -e /sys/class/net/wgbst0 ]; then bst_notok wgq/up-leaves-no-interface "/sys/class/net/wgbst0 survived a failed wg-quick up" else bst_ok wgq/up-leaves-no-interface fi _afo=$(wg show interfaces 2>&1) if [ "$_afo" = "$_lo" ]; then bst_ok wgq/up-leaves-enumeration-unchanged else bst_notok wgq/up-leaves-enumeration-unchanged "wg show interfaces changed from '$(_bst_1line "$_lo")' to '$(_bst_1line "$_afo")'"; fi rm -rf "$W"