# BST-TIMEOUT: 600 # MEASURED: see the ==BST-SUITE== line this suite emits. # # 26 exec's of two ~4 MB static wasm programs, each doing # # bounded work (parse one small file, or reject an # # argument vector before opening a socket). The bound is # # headroom for a loaded nightly host, not a cap on # # anything this suite is known to approach. # # . "$BST_LIB" is the first executable line; everything above and below is the # reader's brief. . "$BST_LIB" # tsocks 1.8beta5 (meta-networking recipes-protocols) -- the LD_PRELOAD SOCKS # wrapper. What ships on this image is /usr/bin/validateconf, # /usr/bin/inspectsocks and the /usr/bin/tsocks script. There is no # /usr/lib/libtsocks.so, and there cannot be one; that is the first thing this # suite pins. # # --------------------------------------------------------------------------- # THE TICKET ASKED FOR TWO THINGS. ONE OF THEM CANNOT BE DONE HERE, AND THIS # SUITE MEASURES WHY RATHER THAN ASSERTING IT. # # "Assert that the wrapper loads into a process" needs libtsocks.so, and the # only way to build one is a -shared link. wasmcc-glibc appends its own NATIVE # wasm runtime objects to every link it drives, and wasmld requires every input # to a side-module link to arrive as bitcode, so the link is refused before it # starts -- brintos.io/brintos/hwjs-cc/issues/76, open. -nostdlib does not help # and no recipe-level flag removes the driver's own inputs. So the interposer # is absent by construction, not by omission, and the LOADER section below # pins that state three ways: the library is not on disk, LD_PRELOAD naming a # missing object is silently ignored by this runtime (which is what makes an # unguarded wrapper dangerous), and the wrapper therefore refuses to run. # # When #76 closes, the first of those goes red on its own and says so. The # recipe additionally refuses to build if the .so ever appears, so the two # cannot drift apart in silence. # # "Drive its parser against fixture tsocks.conf files" is the half that CAN be # done, and it is the bulk of this suite. validateconf links the very parser.o # the interposer would have linked, so the parse and the routing decisions are # the port's real code either way. # # --------------------------------------------------------------------------- # EVERY PARSE EXPECTATION COMES FROM A NATIVE x86-64 BUILD, NOT FROM READING # THE SOURCE. # # data/tsocks-oracle.txt is generated by src/tsocks_mknative.sh from the same # pinned tarball and the same patch series this recipe applies, compiled by the # build host's own cc. A difference here is therefore a property of the wasm32 # port and of nothing else. Hand-written expectations would instead pin # whatever the author believed the parser does -- and this parser has surprises # in it (a rejected network's diagnostic used to print freed memory; port 0 is # rejected because the range check tests the value's truth rather than its # parse; `IP (x) & SUBNET (y)' arrives as two show_msg calls, so the program # name appears twice in one sentence). # # The oracle's record separator is '|', and tsocks_mknative.sh refuses to # generate a file whose cases contain one, so splitting on it here is safe. # # The ARGUMENT VECTORS below must stay identical to the generator's. They are # the contract between the two files; the oracle-case-count assertion at the # end is what notices if one side gains a case the other does not. VC=/usr/bin/validateconf IS=/usr/bin/inspectsocks TS=/usr/bin/tsocks TSOCKS_LIB=/usr/lib/libtsocks.so ORACLE=$BST_DATA/tsocks-oracle.txt NL=' ' TMP=${TMPDIR:-/tmp} # --- structure -------------------------------------------------------------- for p in "$VC" "$IS" "$TS"; do n=${p##*/} if [ -x "$p" ]; then bst_ok "tsocks/installed-$n" else bst_notok "tsocks/installed-$n" "$p is not installed or not executable"; fi done # THE assertion this port turns on. A red here does not mean the suite is # wrong: it means -shared started linking on this arm, which is hwjs-cc#76 # closing, and this suite plus the bbappend both need revisiting. if [ -e "$TSOCKS_LIB" ] || [ -e "$TSOCKS_LIB.1" ] || [ -e "$TSOCKS_LIB.1.8" ]; then bst_notok tsocks/no-preload-library \ "$TSOCKS_LIB exists -- hwjs-cc#76 appears fixed; revisit the bbappend and this suite" else bst_ok tsocks/no-preload-library fi # tsocks.conf(5) is the grammar the fixtures are written against, so a port # that dropped it would leave the configuration format undocumented on target. for m in /usr/share/man/man1/tsocks.1 /usr/share/man/man5/tsocks.conf.5 \ /usr/share/man/man8/tsocks.8; do n=${m##*/} if [ -f "$m" ]; then bst_ok "tsocks/man-$n" else bst_notok "tsocks/man-$n" "$m is not installed"; fi done if [ ! -f "$ORACLE" ]; then bst_notok tsocks/oracle-present "the x86-64 oracle is not installed at $ORACLE" exit 0 fi bst_ok tsocks/oracle-present if [ ! -x "$VC" ]; then # Everything below drives it; without it the cases would all report the same # red for the same reason and drown the three findings above. bst_skip tsocks/parse "validateconf is not installed on this image" exit 0 fi # --- oracle comparison ------------------------------------------------------ # Walk two newline-separated lists in lockstep, one case per line, so a single # drifting answer names itself instead of reddening a whole dump. Pure # parameter expansion: a sed(1) per line would be ~240 execs on a guest where # an exec is the expensive operation. # # The walk continues while EITHER side has lines left, so a truncated or an # over-long stream is caught by the same mechanism -- the extra lines land as # `want:'' got:'X'' (or the reverse) rather than passing silently. That is why # there is no separate line-count case here. cmp_lines() { # cmp_lines _p=$1; _w=$2; _g=$3; _n=0 while [ -n "$_w" ] || [ -n "$_g" ]; do _n=$((_n+1)) case "$_w" in *"$NL"*) _wl=${_w%%"$NL"*}; _w=${_w#*"$NL"} ;; *) _wl=$_w; _w= ;; esac case "$_g" in *"$NL"*) _gl=${_g%%"$NL"*}; _g=${_g#*"$NL"} ;; *) _gl=$_g; _g= ;; esac check_eq "$_p-$_n" "$_wl" "$_gl" done } # run_case # # The cd into $BST_DATA is load-bearing: validateconf echoes the path it was # given, and the generator ran from that directory too, so both sides see a # bare basename. Normalising an absolute path afterwards would put a rewrite # between the two halves of the comparison, which is one more thing that can # differ between host and target. run_case() { _bin=$1; _id=$2; shift 2 _o=$TMP/tsocks-o.$$ _e=$TMP/tsocks-e.$$ ( cd "$BST_DATA" && "$_bin" "$@" ) >"$_o" 2>"$_e" _rc=$? _gotout=$(cat "$_o"); _goterr=$(cat "$_e") rm -f "$_o" "$_e" # A command that could not be staged reports the shell's own diagnostic on # stderr; without this the case would read as tsocks printing the wrong thing. if bst_execfail "$_goterr"; then bst_unrun "tsocks/$_id-rc" "$_goterr" return fi check_eq "tsocks/$_id-rc" "$(grep "^$_id|rc|" "$ORACLE" | sed 's/^[^|]*|[^|]*|//')" "$_rc" cmp_lines "tsocks/$_id-out" \ "$(grep "^$_id|stdout|" "$ORACLE" | sed 's/^[^|]*|[^|]*|//')" "$_gotout" cmp_lines "tsocks/$_id-err" \ "$(grep "^$_id|stderr|" "$ORACLE" | sed 's/^[^|]*|[^|]*|//')" "$_goterr" } # Configuration dumps: what the parser stored, for a valid file, for every # documented diagnostic, for the network/netmask and port-range edges, and for # a file that parses but says nothing useful. run_case "$VC" dump-simple -f tsocks-simple.conf run_case "$VC" dump-complex -f tsocks-complex.conf run_case "$VC" dump-errors -f tsocks-errors.conf run_case "$VC" dump-netmask -f tsocks-netmask.conf run_case "$VC" dump-nodefault -f tsocks-nodefault.conf run_case "$VC" dump-missing -f tsocks-absent.conf run_case "$VC" dump-default # Routing decisions -- the question the wrapper would ask the parser for every # connect(2). The discriminating pairs are port 80 vs 81 on the same address # (one path claims the port, the other falls through to the default server) and # 198.51.100.7:22 vs :26 either side of a reach statement's range. run_case "$VC" route-local-192 -f tsocks-complex.conf -t 192.168.0.5 run_case "$VC" route-local-10 -f tsocks-complex.conf -t 10.10.1.1 run_case "$VC" route-loopback -f tsocks-complex.conf -t 127.0.0.1 run_case "$VC" route-path1 -f tsocks-complex.conf -t 150.0.1.2 run_case "$VC" route-path2-port80 -f tsocks-complex.conf -t 150.1.0.9:80 run_case "$VC" route-path2-port81 -f tsocks-complex.conf -t 150.1.0.9:81 run_case "$VC" route-host-inrange -f tsocks-complex.conf -t 198.51.100.7:22 run_case "$VC" route-host-outrange -f tsocks-complex.conf -t 198.51.100.7:26 run_case "$VC" route-default -f tsocks-complex.conf -t 203.0.113.200 run_case "$VC" usage-odd-argc -f run_case "$VC" usage-unknown-option -x foo if [ -x "$IS" ]; then run_case "$IS" inspect-no-args run_case "$IS" inspect-too-many 1.2.3.4 1080 extra run_case "$IS" inspect-bad-host not.an.ip run_case "$IS" inspect-bad-port 1.2.3.4 0 else bst_skip tsocks/inspect-args "inspectsocks is not installed on this image" fi # The two files agree on the case list. A case the generator emits and this # suite never runs would otherwise sit in the oracle unread -- coverage that # reads as present and is not. want_cases=$(grep -c '|rc|' "$ORACLE") check_eq tsocks/oracle-case-count 22 "$want_cases" # --- the loader half -------------------------------------------------------- # THE MEASUREMENT the wrapper's guard exists for. LD_PRELOAD naming an object # that is not there is not an error the loader reports -- the program simply # runs uninterposed. That is why `tsocks prog' had to be made to refuse: left # alone it would run prog with every connection going DIRECT while printing # nothing at all, which on a tool whose whole purpose is to force traffic # through a proxy is a failure open. # # Written as a fact about THIS runtime rather than assumed, because the guard # is only justified while it holds. # # The property asserted is the one the guard turns on: the program RUNS. A # loader that additionally warns on stderr (glibc's ld.so does, and says # "ignored") still runs it, so the warning is recorded as a diagnostic rather # than scored -- scoring silence would make this case a statement about which # loader is present instead of about whether traffic escapes. if command -v env >/dev/null 2>&1 && [ -x /bin/sh ]; then preload_out=$(env LD_PRELOAD=$TSOCKS_LIB /bin/sh -c 'echo RAN' 2>&1) preload_rc=$? printf '#diag: LD_PRELOAD=%s sh -c echo -> rc=%s out=[%s]\n' \ "$TSOCKS_LIB" "$preload_rc" "$(_bst_1line "$preload_out")" case "$preload_out" in *RAN*) preload_ran=1 ;; *) preload_ran=0 ;; esac if [ "$preload_rc" -eq 0 ] && [ "$preload_ran" -eq 1 ]; then bst_ok tsocks/preload-of-missing-object-does-not-stop-the-program else # Not a regression in tsocks: it means the runtime started REFUSING a # missing preload object, which would be an improvement and would change # what the wrapper's guard is for. bst_notok tsocks/preload-of-missing-object-does-not-stop-the-program \ "the runtime now refuses a missing LD_PRELOAD object (rc=$preload_rc out=[$(_bst_1line "$preload_out")]) -- revisit patch 0002" fi else bst_skip tsocks/preload-of-missing-object-does-not-stop-the-program \ "env(1) or /bin/sh is not available on this image" fi if [ -x "$TS" ]; then # Fails closed: refuses, names the missing library, and does NOT run the # program. `echo tsocks-ran-the-program' is the canary -- if the guard were # absent the wrapper would exec it and the string would appear. ts_out=$("$TS" /bin/sh -c 'echo tsocks-ran-the-program' 2>&1) ts_rc=$? printf '#diag: tsocks sh -c echo -> rc=%s out=[%s]\n' "$ts_rc" "$(_bst_1line "$ts_out")" check_eq tsocks/wrapper-refuses-rc 1 "$ts_rc" # Matched on the wrapper's own wording rather than on $TSOCKS_LIB: that # variable means "where this suite looks for the library", and the script # hardcodes its own copy of the path. The two are the same string on target # and the coupling is invisible until something moves one of them, at which # point this case reddens for a reason that has nothing to do with the guard. case "$ts_out" in *"is not installed"*"refusing to run"*) bst_ok tsocks/wrapper-refuses-names-library ;; *) bst_notok tsocks/wrapper-refuses-names-library \ "want the missing-library refusal, got:'$(_bst_1line "$ts_out")'" ;; esac case "$ts_out" in *tsocks-ran-the-program*) bst_notok tsocks/wrapper-did-not-exec \ "the wrapper ran the program anyway -- every connection would have gone direct" ;; *) bst_ok tsocks/wrapper-did-not-exec ;; esac # `off' and `show' are deliberately NOT guarded: they are meaningful with or # without the library, and `off' in particular has to keep working so a user # can undo an `on' from an earlier shell. check_out tsocks/wrapper-show 'LD_PRELOAD=' "$TS" show check_rc tsocks/wrapper-off 0 "$TS" off else bst_skip tsocks/wrapper "the tsocks wrapper script is not installed on this image" fi # --- the transport half, probed at run time --------------------------------- # Whether inspectsocks can NEGOTIATE with a SOCKS server is a property of the # network this guest is on, not of the port, so it is probed rather than # assumed. $BST_SOCKS_PROXY (host[:port]) names one when the operator has one; # with none reachable the case skips, which is the ticket's own instruction. # # The argument-rejection cases above already exercise inspectsocks' code up to # the point it opens a socket, so a skip here leaves the binary covered, not # unexercised. if [ ! -x "$IS" ]; then bst_skip tsocks/socks-negotiate "inspectsocks is not installed on this image" elif [ -z "${BST_SOCKS_PROXY:-}" ]; then bst_skip tsocks/socks-negotiate \ "no SOCKS proxy offered (set BST_SOCKS_PROXY=host[:port]); the offline half is covered by the inspect-* cases" else proxy_host=${BST_SOCKS_PROXY%%:*} proxy_port=${BST_SOCKS_PROXY#"$proxy_host"} proxy_port=${proxy_port#:} [ -n "$proxy_port" ] || proxy_port=1080 neg=$("$IS" "$proxy_host" "$proxy_port" 2>&1); neg_rc=$? printf '#diag: inspectsocks %s %s -> rc=%s out=[%s]\n' \ "$proxy_host" "$proxy_port" "$neg_rc" "$(_bst_1line "$neg")" case "$neg" in *"is a version 4 socks server"*|*"is a version 5 socks server"*) bst_ok tsocks/socks-negotiate ;; *) bst_notok tsocks/socks-negotiate \ "no SOCKS version reported by $BST_SOCKS_PROXY (rc=$neg_rc): $(_bst_1line "$neg")" ;; esac fi