. "$BST_LIB" # tunctl 1.5 (meta-networking, recipes-support) — Jeff Dike's TUN/TAP interface # manager from the User-mode Linux tools, installed as /usr/sbin/tunctl. It # creates and deletes PERSISTENT tun/tap interfaces: open a tun clone device, # ioctl(TUNSETIFF) to name the interface, then TUNSETOWNER / TUNSETGROUP / # TUNSETPERSIST. Five ioctls, and TUNSETIFF gates the other four. # # THE SHAPE OF THIS SUITE IS DECIDED BY ONE FACT: this kernel has NO TUN/TAP # DRIVER — CONFIG_TUN is not set — so there is no /dev/net/tun clone device and # not one of those five ioctls can succeed here. That is the same class of fact # 63-ifupdown records for CONFIG_DUMMY, and a property of a browser machine # whose networking is whatever HardwareJS gives it. It is not a defect. # # What that leaves is more than it sounds, because tunctl has an escape hatch # the device-oriented ports do not: -f names the clone device to open, so ANY # read/write path carries the program past open(2) and INTO ioctl(TUNSETIFF). # So on a machine with no tun driver at all, the ioctl is still proven to be # issued and its failure proven to be reported. Asserted for real below: the # option parser including tunctl's OWN -p/-n exclusion (a separate check from # getopt's), the getpwnam/getgrnam owner and group resolution — which happens # during option parsing, BEFORE any device is opened — the compiled-in default # clone device, both open(2) diagnostics, and the ioctl boundary itself. # Everything downstream of a SUCCESSFUL TUNSETIFF is gated on a runtime probe, # so it runs the real assertion the day this kernel grows a tun driver rather # than reporting a stale skip. require tunctl tunctl/present tmp=${TMPDIR:-/tmp}/bst-tunctl.$$ out=$tmp.out; reg=$tmp.reg; node=$tmp.node missing=/nonexistent/bst-tunctl-no-such-clone # The tun clone device — the gate for every case downstream of TUNSETIFF. # Both spellings tunctl's own help text names are probed. tunclone="" for d in /dev/net/tun /dev/misc/net/tun; do if [ -c "$d" ]; then tunclone="$d"; break; fi done # ------------------------------------------------------------- packaging --- # tunctl installs to sbindir, not bindir. Asserting where it resolves catches a # packaging change that leaves it off the PATH the runner actually has. check_eq tunctl/resolves-to-sbin "/usr/sbin/tunctl" "$(command -v tunctl)" # ----------------------------------------------------------- usage output --- # Usage() ends in exit(1) and upstream wires -h straight into it, so --help is # an ERROR path here: rc 1, everything on stderr, stdout left empty. A suite # written against the usual "help exits 0" convention would be red on a correct # binary. check_rc tunctl/help-rc 1 sh -c "tunctl -h > '$out' 2>&1" check_out tunctl/help-create "Create:" cat "$out" check_out tunctl/help-delete "Delete:" cat "$out" check_out tunctl/help-clone "The default tun clone device is /dev/net/tun" cat "$out" check_out tunctl/help-brief "-b will result in brief output" cat "$out" check_out tunctl/help-tun "-n will result in a point-to-point tun device" cat "$out" check_out tunctl/help-tap "-p in an ethernet tap device" cat "$out" check_eq tunctl/help-stderr "" "$(tunctl -h 2>/dev/null)" # getopt's own rejection, with tunctl's usage appended to it. check_rc tunctl/badopt-rc 1 sh -c "tunctl -Z > '$out' 2>&1" check_out tunctl/badopt-msg "invalid option" cat "$out" check_out tunctl/badopt-usage "Create:" cat "$out" check_rc tunctl/missing-arg-rc 1 sh -c "tunctl -t > '$out' 2>&1" check_out tunctl/missing-arg-msg "option requires an argument" cat "$out" # -p and -n are tunctl's OWN exclusion — `if(type != 0) Usage()` — not getopt's: # both are perfectly valid options and it is the second one that is refused. The # case below asserts the message, the one after it asserts WHICH check produced # it, which is the part that would silently change if the option table did. check_rc tunctl/pn-exclusive-rc 1 sh -c "tunctl -p -n > '$out' 2>&1" check_out tunctl/pn-exclusive-msg "Create:" cat "$out" case "$(cat "$out")" in *"invalid option"*) bst_notok tunctl/pn-own-check "-p -n was refused by getopt, not by tunctl's own type check" ;; *) bst_ok tunctl/pn-own-check ;; esac # A trailing non-option argument is refused after getopt returns (argc > 0). check_rc tunctl/extra-arg-rc 1 sh -c "tunctl bst-extra > '$out' 2>&1" check_out tunctl/extra-arg-msg "Create:" cat "$out" # ------------------------------------- owner/group resolution (NSS, no I/O) --- # -u/-g resolve DURING option parsing, before any clone device is opened: # getpwnam/getgrnam first, then a strtol(base 0) fallback. So these cases run to # completion on a machine with no tun driver, and they are a real exercise of # glibc's builtin 'files' NSS service on this static wasm32 image — the same # path 06-base-passwd asserts through coreutils id(1), reached here by a # different consumer. # # The names are derived from the databases tunctl actually consults rather than # hardcoded, so each case compares tunctl's answer against the file it read. realuser=$(awk -F: '$3 == 0 { print $1; exit }' /etc/passwd 2>/dev/null) realgrp=$(awk -F: '$3 == 0 { print $1; exit }' /etc/group 2>/dev/null) # A name glibc resolved produces NO diagnostic and falls through to the open. # That absence is the only evidence available on this target: tunctl prints the # uid it resolved only on a path that needs a working tun device. if [ -n "$realuser" ]; then tunctl -u "$realuser" -f "$missing" > "$out" 2>&1 case "$(cat "$out")" in *"is neither a username"*) bst_notok tunctl/user-getpwnam "getpwnam('$realuser') did not resolve: $(_bst_1line "$(cat "$out")")" ;; *"Failed to open"*) bst_ok tunctl/user-getpwnam ;; *) bst_notok tunctl/user-getpwnam "unexpected output for -u $realuser: $(_bst_1line "$(cat "$out")")" ;; esac else bst_notok tunctl/user-getpwnam "no uid-0 entry in /etc/passwd for tunctl to resolve" fi if [ -n "$realgrp" ]; then tunctl -g "$realgrp" -f "$missing" > "$out" 2>&1 case "$(cat "$out")" in *"is neither a groupname"*) bst_notok tunctl/group-getgrnam "getgrnam('$realgrp') did not resolve: $(_bst_1line "$(cat "$out")")" ;; *"Failed to open"*) bst_ok tunctl/group-getgrnam ;; *) bst_notok tunctl/group-getgrnam "unexpected output for -g $realgrp: $(_bst_1line "$(cat "$out")")" ;; esac else bst_notok tunctl/group-getgrnam "no gid-0 entry in /etc/group for tunctl to resolve" fi # The differential that proves the lookup really consults the database rather # than accepting whatever it is handed: the SAME name in the wrong case is not a # user. Without this pair, a getpwnam stub that always succeeded would pass. if [ -n "$realuser" ]; then upper=$(printf '%s' "$realuser" | tr 'a-z' 'A-Z') if [ "$upper" != "$realuser" ]; then tunctl -u "$upper" -f "$missing" > "$out" 2>&1 check_out tunctl/user-case-sensitive "is neither a username" cat "$out" else bst_skip tunctl/user-case-sensitive "the uid-0 name '$realuser' has no distinct upper-case spelling to test with" fi else bst_skip tunctl/user-case-sensitive "no uid-0 entry in /etc/passwd to derive a wrong-case name from" fi check_rc tunctl/user-bogus-rc 1 sh -c "tunctl -u zzznosuchuser -f '$missing' > '$out' 2>&1" check_out tunctl/user-bogus-msg "'zzznosuchuser' is neither a username nor a numeric uid." cat "$out" # ...and it stops there. The clone device is never opened, which is what pins # the ordering of the resolution against the open. case "$(cat "$out")" in *"Failed to open"*) bst_notok tunctl/user-bogus-noopen "tunctl opened the clone device despite an unresolvable -u" ;; *) bst_ok tunctl/user-bogus-noopen ;; esac check_rc tunctl/group-bogus-rc 1 sh -c "tunctl -g zzznosuchgroup -f '$missing' > '$out' 2>&1" check_out tunctl/group-bogus-msg "'zzznosuchgroup' is neither a groupname nor a numeric group." cat "$out" # The strtol fallback is base 0, so decimal AND 0x hex are accepted; a trailing # non-digit is exactly what makes an argument "neither". tunctl -u 1234 -f "$missing" > "$out" 2>&1 check_out tunctl/user-numeric "Failed to open" cat "$out" tunctl -u 0x10 -f "$missing" > "$out" 2>&1 check_out tunctl/user-hex "Failed to open" cat "$out" check_rc tunctl/user-partial-rc 1 sh -c "tunctl -u 12abc -f '$missing' > '$out' 2>&1" check_out tunctl/user-partial-msg "'12abc' is neither a username nor a numeric uid." cat "$out" # --------------------------------------------------- the clone device open --- # -f with a path that does not exist: open(2) fails and tunctl names the path it # was given, so this also covers that -f is actually honoured. check_rc tunctl/open-enoent-rc 1 sh -c "tunctl -f '$missing' > '$out' 2>&1" check_out tunctl/open-enoent-msg "Failed to open '$missing' : No such file or directory" cat "$out" # A directory reaches the same diagnostic through a different errno. check_rc tunctl/open-eisdir-rc 1 sh -c "tunctl -f / > '$out' 2>&1" check_out tunctl/open-eisdir-msg "Is a directory" cat "$out" # ------------------------------------------------------ the ioctl boundary --- # THE PAIR THIS PORT IS REALLY WORTH. -f accepts any path, so an ordinary # writable file carries tunctl PAST open(2) and INTO ioctl(TUNSETIFF) on a # machine that has no tun driver at all. Asserted as a differential rather than # as one message: the missing path stops at open and never names TUNSETIFF; the # regular file clears open and fails IN the ioctl. Together they prove the # program advanced from the one to the other, which neither case proves alone. : > "$reg" check_rc tunctl/ioctl-reached-rc 1 sh -c "tunctl -f '$reg' > '$out' 2>&1" check_out tunctl/ioctl-reached-msg "TUNSETIFF" cat "$out" case "$(cat "$out")" in *"Failed to open"*) bst_notok tunctl/ioctl-past-open "a writable regular file did not get past open(2): $(_bst_1line "$(cat "$out")")" ;; *) bst_ok tunctl/ioctl-past-open ;; esac tunctl -f "$missing" > "$out" 2>&1 case "$(cat "$out")" in *TUNSETIFF*) bst_notok tunctl/ioctl-not-reached "tunctl named TUNSETIFF for a path it could not open" ;; *) bst_ok tunctl/ioctl-not-reached ;; esac # A character device that is not a tun clone reaches the same boundary, so the # result above is not an artefact of the file being regular. if [ -c /dev/null ]; then check_rc tunctl/ioctl-devnull-rc 1 sh -c "tunctl -f /dev/null > '$out' 2>&1" check_out tunctl/ioctl-devnull-msg "TUNSETIFF" cat "$out" else bst_skip tunctl/ioctl-devnull-rc "no /dev/null character device on this image" bst_skip tunctl/ioctl-devnull-msg "no /dev/null character device on this image" fi # The delete path funnels through the same TUNSETIFF before it can reach # TUNSETPERSIST, so -d stops in exactly the same place as a create does. check_rc tunctl/delete-ioctl-rc 1 sh -c "tunctl -d bst0 -f '$reg' > '$out' 2>&1" check_out tunctl/delete-ioctl-msg "TUNSETIFF" cat "$out" # ------------------------------ no device node, or no driver? say which one --- # /dev/net/tun being absent could mean the DEVICE NODE was never created — a # devtmpfs/populate gap, which would be ours to fix — or that the kernel has no # TUN DRIVER at all, which is a config fact and is not. Those are different # findings with different owners, so the suite distinguishes them from the # target side instead of assuming: make the node by hand at TUN's fixed misc # minor (char 10, 200) and try to open it. ENODEV from a node that exists is the # kernel answering that the driver is not there. if [ -z "$tunclone" ]; then if mknod "$node" c 10 200 >/dev/null 2>&1; then tunctl -f "$node" > "$out" 2>&1 case "$(cat "$out")" in *"No such device"*) bst_ok tunctl/no-driver-not-no-node ;; *TUNSETIFF*) bst_notok tunctl/no-driver-not-no-node "a hand-made tun node OPENED and reached TUNSETIFF: this kernel does have a tun driver and the missing /dev/net/tun is a device-node gap, not CONFIG_TUN" ;; *) bst_notok tunctl/no-driver-not-no-node "unexpected result opening a hand-made tun node: $(_bst_1line "$(cat "$out")")" ;; esac rm -f "$node" else bst_skip tunctl/no-driver-not-no-node "mknod is refused on this filesystem, so 'no node' and 'no driver' cannot be told apart from the target side; the kernel .config is the record (CONFIG_TUN is not set)" fi else bst_skip tunctl/no-driver-not-no-node "a tun clone device exists at $tunclone, so there is nothing to disambiguate" fi # -------------------------------------------------- tun-driver-gated cases --- # Everything downstream of a successful TUNSETIFF. Gated on the RUNTIME probe, # so these run the real assertion the moment this kernel grows a tun driver. if [ -n "$tunclone" ]; then # Capture tunctl's OWN exit status. Asserting on the output file instead # (`test -s`) would report ok for any non-empty output, including a failed # ioctl's perror line — a false green in the one branch this target cannot # exercise, which is exactly where it would never be noticed. own=${realuser:-0} cout=$(tunctl -f "$tunclone" -t bsttap0 -u "$own" 2>&1); crc=$? check_eq tunctl/create-rc 0 "$crc" check_out tunctl/create-msg "Set 'bsttap0' persistent and owned by" printf '%s\n' "$cout" bout=$(tunctl -f "$tunclone" -b -t bsttap1 2>&1) check_eq tunctl/brief-name "bsttap1" "$bout" dout=$(tunctl -f "$tunclone" -d bsttap0 2>&1); drc=$? check_eq tunctl/delete-rc 0 "$drc" check_out tunctl/delete-msg "Set 'bsttap0' nonpersistent" printf '%s\n' "$dout" gout=$(tunctl -f "$tunclone" -g "${realgrp:-0}" -t bsttap2 2>&1) check_out tunctl/owner-ioctl "gid" printf '%s\n' "$gout" tunctl -f "$tunclone" -d bsttap1 >/dev/null 2>&1 tunctl -f "$tunclone" -d bsttap2 >/dev/null 2>&1 else bst_skip tunctl/create-rc "no TUN/TAP driver on this kernel (CONFIG_TUN is not set), so no /dev/net/tun clone device exists, TUNSETIFF cannot succeed and no persistent interface can be created — the same class of fact 63-ifupdown records for CONFIG_DUMMY" bst_skip tunctl/create-msg "same: the persistent/owned-by message follows a successful TUNSETIFF" bst_skip tunctl/brief-name "same: -b prints the interface name only after a successful TUNSETIFF" bst_skip tunctl/delete-rc "same: TUNSETPERSIST(0) is reached only after TUNSETIFF succeeds" bst_skip tunctl/delete-msg "same: the nonpersistent message follows a successful TUNSETPERSIST(0)" bst_skip tunctl/owner-ioctl "same: TUNSETOWNER/TUNSETGROUP are reached only after TUNSETIFF succeeds" fi rm -f "$out" "$reg" "$node" 2>/dev/null