. "$BST_LIB" # BST-TIMEOUT: 900 # dante 1.4.4 (meta-networking recipes-protocols/dante) -- Inferno Nettverk's # SOCKS 4/5 server. What ships is /usr/bin/sockd and the /etc/sockd.conf this # port bundles; the client side (libdsocks.so and its `socksify' wrapper) is not # on this image and its absence is asserted rather than passed over. # # READ THIS FIRST, BECAUSE IT DECIDES HOW TWO THIRDS OF THE FILE SCORES. # sockd's serverinit() calls shmem_setup() (sockd/sockd.c:1584) BEFORE it reads # a line of configuration -- "needs to be before config file read, as parsing # functions may [put] things in shmem", says the comment above the call -- and # shmem_setup()'s first act is # # sockd_mmap(NULL, sizeof(*sockscf.shmeminfo), # PROT_READ | PROT_WRITE, MAP_SHARED, sockscf.shmemfd, 1) # # with `serr("failed to mmap shmeminfo")' -- a fatal -- on MAP_FAILED. A # file-backed MAP_SHARED mapping is refused on this arm whenever PROT_WRITE is # asked for, ENODEV, and it is refused on tmpfs as well as on the virtiofs root: # brintos/linux#30, measured by 77-gdbm (which works around it with # gdbmtool --no-mmap), by 95-openldap (LMDB, which cannot) and by 57-libmad. The # mapping's file lands under $TMPDIR or /tmp (lib/util.c:920, socks_mklock), so # there is no directory this suite could point it at that would answer. # # THAT MEANS sockd CANNOT START HERE, AND CANNOT EVEN VERIFY A CONFIG. Every # case from section 3 down is downstream of that one call, including both halves # of the criterion this port was taken on. They are left FAILING and attributed # rather than skipped, so they flip green by themselves the day #30 lands -- # nothing in this file or in the recipe needs changing then. # # IT IS NOT PATCHED AROUND, AND THAT IS A DECISION RATHER THAN AN OMISSION. # sockd is a mother-and-children design: the negotiate, request and io children # are fork(2)ed and share session counts, bandwidth state and the host cache # through that mapping. Every other route to memory shared with a forked child # is held by a gap too -- shmget/semget are ENOSYS although CONFIG_SYSVIPC=y # (brintos/linux#31, pinned by 41-luaposix), shm_open + mmap(MAP_SHARED) is # ENOSYS (79-libqb), and an ANONYMOUS writable MAP_SHARED is #30 as well # (78-liburcu, which leaves urcu-bp undriven for it). Falling back to MAP_PRIVATE # would give each child its own copy and break session accounting silently, # which is a different daemon rather than a port; 84-dbench reached the same # conclusion about the same family and said so. # # TWO EARLIER WALLS WERE REAL AND ARE FIXED IN THE RECIPE, so they are not what # a red below means. crypt(3) reached wasmld as a phantom import and refused the # link (brintos/hwjs-cc#33); libxcrypt's archive is now named by full path. # sched_getscheduler(2) is ENOSYS (brintos/linux#88, pinned by 41-luaposix) and # killed serverinit six lines before shmem_setup(); the recipe answers # ac_cv_func_sched_setscheduler=no, which compiles that block out. Both are in # recipes-protocols/dante/dante_%.bbappend with the measurements. # # NO SIGNATURE IS ASSERTED FOR THE WALL. The cases below assert the ABSENCE OF # SUCCESS and carry whatever the run actually printed in the red's detail. A # suite keyed on "failed to mmap shmeminfo" would read a change in the failure # -- a different errno, an earlier serr, a kernel that maps but does not share # -- as a fix. # # OFFLINE AND NUMERIC BY CONSTRUCTION. The origin, the proxy and the client are # all 127.0.0.1 by address, so nothing traverses getaddrinfo(3)'s dns leg, which # is independently broken here (brintos/linux-in-the-browser#96) and would # otherwise turn a proxy result into a resolver result. # # NOTHING IS KILLED AND NOTHING IS LEFT WAITED ON. kill(2) reports success and # delivers nothing on this image (brintos/linux#21), and `cmd & wait' hangs # (99-inotify-tools), so every request goes through bg() -- background, then # poll a file for the rc -- and the origin is left running when the suite ends. # # PORTS: 1080 for sockd (dante's own default, and the port the bundled config # names), 8099 for the socat origin the rule set passes, 8098 for the # destination it refuses. No other suite on this image binds any of the three. # # CALIBRATED AGAINST A NATIVE x86_64 BUILD of this same tarball, driven green # before it ever ran here -- the shipped config verified, the proxied fetch # byte-exact over both SOCKS4 and SOCKS5, and the refused destination answering # curl exit 97 with reply code (2). So a red here is a target-only divergence # and not a miscalibrated assertion. require /usr/bin/sockd dante/present SOCKD=/usr/bin/sockd CONF=/etc/sockd.conf PPORT=1080 OPORT=8099 DPORT=8098 WANT="dante-origin-payload-0123456789abcdef" D=${TMPDIR:-/tmp}/bst-dante.$$ rm -rf "$D" mkdir -p "$D" || { bst_notok dante/workdir "cannot create $D"; exit 0; } # A proxy already exported by the runner would silently send the control fetch # through somebody else's server. http_proxy=; https_proxy=; no_proxy=; ALL_PROXY=; SOCKS_CONF= export http_proxy https_proxy no_proxy ALL_PROXY SOCKS_CONF T="" if command -v timeout >/dev/null 2>&1; then T=1; else bst_notok dante/timeout-available "no timeout(1) on this image; bounded cases run under the suite bound alone" fi has() { _h=$1; shift; for _n in "$@"; do case "$_h" in *"$_n"*) ;; *) return 1 ;; esac; done; return 0; } # rb -- run bounded, merging both streams; sets $out and $rc. # sockd logs every diagnostic to stderr (the bundled config's `logoutput: # stderr'), so the two are read together. rb() { _t=$1; shift if [ -n "$T" ]; then out=$(timeout "$_t" "$@" 2>&1); else out=$("$@" 2>&1); fi rc=$? } # bg -- run something that MAY NEVER RETURN and poll for # it, without wait(1). The rc travels out through a file, which is what makes # the poll possible; sets bg_done, and bg_rc/bg_out when it finished. Every # request in this file goes through it, because "the connection is accepted and # then nothing ever answers" is a shape this platform produces. is a bare # name, never a case id: the ids here carry a slash and would not be a filename. bg() { _t=$1; _tag=$2; shift 2 rm -f "${D}/${_tag}.rc" "${D}/${_tag}.out" ( "$@" > "${D}/${_tag}.out" 2>&1; echo $? > "${D}/${_tag}.rc" ) & _i=0; bg_done=0 while [ "$_i" -lt "$_t" ]; do if [ -f "${D}/${_tag}.rc" ]; then bg_done=1; break; fi _i=$((_i+1)); sleep 1 done if [ "$bg_done" -eq 1 ]; then bg_rc=$(cat "${D}/${_tag}.rc" 2>/dev/null); bg_out=$(cat "${D}/${_tag}.out" 2>/dev/null) else bg_rc=""; bg_out=$(cat "${D}/${_tag}.out" 2>/dev/null) fi } # listening -- the LISTEN row for , empty if there is none. ss and # netstat read kernel state and cannot block, so this bounds a daemon's startup # without opening a connection a dead daemon could hold open. listening() { (ss -ltn 2>/dev/null || netstat -ltn 2>/dev/null) | grep ":$1"; } waitlisten() { _p=$1; _n=$2; _i=0; _l='' while [ "$_i" -lt "$_n" ]; do _l=$(listening "$_p"); [ -n "$_l" ] && break _i=$((_i+1)); sleep 1 done printf '%s' "$_l" } # The one message every red past section 2 carries. Stated as the cause, not as # a prediction: what makes these reds attributable is the call that fails, not # that this file expected them. W30="brintos/linux#30 (file-backed MAP_SHARED with PROT_WRITE is ENODEV, tmpfs included): sockd's shmem_setup() maps its shared state before it reads any config, so nothing past that call is reachable" ## ## 1. What the package put on the image ## check dante/inventory test -x "$SOCKD" check dante/conf-shipped test -f "$CONF" # The bundled config is asserted BY CONTENT, because upstream's example -- which # the stock recipe installs to this exact path -- is a file sockd rejects: # checkconfig() answers "no internal address given for server to listen for # clients on" and mother exits 1, measured on the native build. A regression to # it would leave every case below red for a reason that is the packaging's and # not the platform's, and these two are what tell the difference. conf=$(cat "$CONF" 2>/dev/null) if has "$conf" "internal: 127.0.0.1 port = 1080" "external: 127.0.0.1"; then bst_ok dante/conf-is-the-bundled-one else bst_notok dante/conf-is-the-bundled-one "no active internal/external directive in $CONF -- upstream's all-commented example installs at this path and sockd refuses it" fi if has "$conf" "socks pass" "socks block" "port = 8099"; then bst_ok dante/conf-carries-the-rule-set else bst_notok dante/conf-carries-the-rule-set "$CONF has no pass/block pair over port 8099, so neither half of the criterion has a subject" fi # The preload interposer is a -shared link this arm refuses (brintos/hwjs-cc#76, # the wall tsocks documents), so it is not built and the wrapper that exists to # LD_PRELOAD it is not shipped. That absence is asserted rather than assumed: an # LD_PRELOAD naming a missing object is IGNORED by the loader, so a shipped # `socksify' would run its argument with no interception at all and every # connection would go DIRECT -- a SOCKS wrapper failing open. check dante/no-socksify-wrapper test ! -e /usr/bin/socksify check dante/no-libdsocks-so test ! -e /usr/lib/libdsocks.so # The archives are correct parts of the package and belong in -staticdev, which # this image does not install. check dante/no-archives-on-image test ! -e /usr/lib/libsocks.a ## ## 2. The one runtime fact that lives before the shared mapping ## ## -v is answered in serverinit() at the `if (sockscf.option.versiononly)' exit ## (sockd.c:1523), six lines before the sched block and sixty before ## shmem_setup(). So this case is the control that makes every red below "this ## call" rather than "sockd cannot run at all" -- and it is what proves the two ## walls the recipe DID fix are fixed, since a phantom crypt import would have ## refused the link and an ENOSYS sched_getscheduler would have killed the ## process before this line. ## rb 20 "$SOCKD" -v if [ "$rc" -eq 0 ] && has "$out" "Dante v1.4.4"; then bst_ok dante/version elif bst_execfail "$out"; then bst_unrun dante/version "$out" else bst_notok dante/version "rc=$rc want:'Dante v1.4.4' got:'$(_bst_1line "$out")'"; fi ## ## 3. Configuration verification -- the first thing behind the mapping ## ## -V parses the config and exits without binding anything (`if ## (!sockscf.option.verifyonly)' guards bindinternal), so on a working kernel ## these three are cheap and say a great deal: the first that the SHIPPED file ## is accepted, the other two that the parser genuinely read what it was given ## rather than ignoring the file. All three sit behind shmem_setup(). ## rb 30 "$SOCKD" -V -f "$CONF" if [ "$rc" -eq 0 ]; then bst_ok dante/config-verify-accepts-bundled elif bst_execfail "$out"; then bst_unrun dante/config-verify-accepts-bundled "$out" else bst_notok dante/config-verify-accepts-bundled "$W30 -- rc=$rc $(_bst_1line "$out")"; fi # A config with every directive commented out is upstream's example exactly, and # checkconfig() must refuse it BY NAME. Asserting the message rather than a bare # non-zero is what makes this a parser result: a sockd that died before reading # the file also exits non-zero. printf 'logoutput: stderr\n' > "${D}/no-internal.conf" rb 30 "$SOCKD" -V -f "${D}/no-internal.conf" if [ "$rc" -ne 0 ] && has "$out" "no internal address given"; then bst_ok dante/config-verify-rejects-no-internal elif bst_execfail "$out"; then bst_unrun dante/config-verify-rejects-no-internal "$out" else bst_notok dante/config-verify-rejects-no-internal "$W30 -- rc=$rc want the parser's own 'no internal address given', got:'$(_bst_1line "$out")'" fi # ...and a directive that is not a directive is refused by the scanner, which # proves the file was tokenised rather than merely opened. printf 'logoutput: stderr\nnosuchkeyword: 1\n' > "${D}/bad.conf" rb 30 "$SOCKD" -V -f "${D}/bad.conf" if [ "$rc" -ne 0 ] && has "$out" "nosuchkeyword"; then bst_ok dante/config-verify-rejects-bad-directive elif bst_execfail "$out"; then bst_unrun dante/config-verify-rejects-bad-directive "$out" else bst_notok dante/config-verify-rejects-bad-directive "$W30 -- rc=$rc want the scanner to name 'nosuchkeyword', got:'$(_bst_1line "$out")'" fi ## ## 4. The origin -- a control with no dante in it ## ## socat's EXEC: address is the fork+exec half of brintos/linux#14, which works; ## SYSTEM: is the half that does not (system(3) is ENOSYS), so it is not used. ## This case is what separates "the proxy did not serve the fetch" from "there ## was nothing to fetch": it uses the same curl, the same file and the same ## port, and only the proxy is missing from the path. ## ORIGIN_UP=0 OLST='' if command -v socat >/dev/null 2>&1 && [ -f "$BST_DATA/dante-response.http" ]; then socat TCP4-LISTEN:$OPORT,bind=127.0.0.1,reuseaddr,fork \ EXEC:"/bin/cat $BST_DATA/dante-response.http" > "${D}/origin.err" 2>&1 & OLST=$(waitlisten $OPORT 15) [ -n "$OLST" ] && ORIGIN_UP=1 check dante/origin-listens test -n "$OLST" else bst_notok dante/origin-listens "no socat, or no $BST_DATA/dante-response.http -- the origin the criterion fetches through cannot be stood up" fi if [ "$ORIGIN_UP" -eq 1 ] && command -v curl >/dev/null 2>&1; then bg 20 direct curl -sS --max-time 10 "http://127.0.0.1:$OPORT/" if [ "$bg_done" -eq 1 ] && [ "$bg_rc" = 0 ] && [ "$bg_out" = "$WANT" ]; then bst_ok dante/origin-direct-fetch else bst_notok dante/origin-direct-fetch "the origin itself does not serve: done=$bg_done rc=$bg_rc got:'$(_bst_1line "$bg_out")'" fi else bst_notok dante/origin-direct-fetch "no origin (or no curl) to fetch from" fi ## ## 5. The server ## SOCKD_UP=0 "$SOCKD" -N 1 -f "$CONF" > "${D}/sockd.log" 2>&1 & PLST=$(waitlisten $PPORT 20) slog=$(cat "${D}/sockd.log" 2>/dev/null) if [ -n "$PLST" ]; then SOCKD_UP=1; bst_ok dante/starts elif bst_execfail "$slog"; then bst_unrun dante/starts "$slog" else bst_notok dante/starts "$W30 -- no listener on $PPORT after 20s; sockd said: $(_bst_1line "$slog")"; fi # The bundled config's `internal: 127.0.0.1 port = 1080' is the one directive # that decides where the listener goes, so this is an assertion about the config # having been honoured and not only about the daemon being alive. if has "$PLST" "127.0.0.1:$PPORT"; then bst_ok dante/listens-loopback-only else bst_notok dante/listens-loopback-only "$W30 -- no 127.0.0.1:$PPORT LISTEN row; got:'$(_bst_1line "$PLST")'"; fi ## ## 6. The criterion: a proxied fetch that passes, and a request that is refused ## ## curl carries both SOCKS4 and SOCKS5 request builders in the shipped build, so ## both are driven -- the ticket names SOCKS 4/5, and a server that answered ## only one of them would be a half port. ## ## request -- drive one proxied fetch, or report that there ## was nothing to drive. Returns 0 when the request was actually attempted, so a ## caller scoring two cases off one run emits exactly one result per case. request() { _tag=$1; _flag=$2; _p=$3 [ "$SOCKD_UP" -eq 1 ] && [ "$ORIGIN_UP" -eq 1 ] || return 1 bg 25 "$_tag" curl -sS --max-time 15 "$_flag" "127.0.0.1:$PPORT" "http://127.0.0.1:${_p}/" return 0 } NOTRUN="$W30 -- not attempted (sockd listening=$SOCKD_UP, origin listening=$ORIGIN_UP)" if request s5 --socks5 $OPORT; then if [ "$bg_done" -eq 1 ] && [ "$bg_rc" = 0 ]; then bst_ok dante/proxied-fetch-socks5 else bst_notok dante/proxied-fetch-socks5 "$W30 -- done=$bg_done rc=$bg_rc $(_bst_1line "$bg_out")"; fi # The same run, scored again on the bytes: a proxy that answered 200 with a # mangled body would pass the case above and fail this one. if [ "$bg_done" -eq 1 ] && [ "$bg_out" = "$WANT" ]; then bst_ok dante/proxied-fetch-byte-exact else bst_notok dante/proxied-fetch-byte-exact "$W30 -- want:'$WANT' got:'$(_bst_1line "$bg_out")'"; fi else bst_notok dante/proxied-fetch-socks5 "$NOTRUN" bst_notok dante/proxied-fetch-byte-exact "$NOTRUN" fi if request s4 --socks4 $OPORT; then if [ "$bg_done" -eq 1 ] && [ "$bg_rc" = 0 ] && [ "$bg_out" = "$WANT" ]; then bst_ok dante/proxied-fetch-socks4 else bst_notok dante/proxied-fetch-socks4 "$W30 -- done=$bg_done rc=$bg_rc got:'$(_bst_1line "$bg_out")'" fi else bst_notok dante/proxied-fetch-socks4 "$NOTRUN" fi # The refused half. The bundled rule set passes 127.0.0.1:8099 and nothing else, # so a CONNECT to 8098 must be answered with SOCKS5 reply 0x02, "connection not # allowed by ruleset" -- which curl reports as exit 97 and prints with the code # in parentheses. Both are scored, separately: the exit status says the request # failed, the reply code says it failed for the reason the rule set gives. That # distinction earns its case here, because a server which merely dropped the # connection would look like an enforced rule from the exit status alone. if request deny --socks5 $DPORT; then if [ "$bg_done" -eq 1 ] && [ "$bg_rc" = 97 ]; then bst_ok dante/denied-destination-refused else bst_notok dante/denied-destination-refused "$W30 -- want curl exit 97, done=$bg_done rc=$bg_rc $(_bst_1line "$bg_out")"; fi if [ "$bg_done" -eq 1 ] && has "$bg_out" "SOCKS5" "(2)"; then bst_ok dante/denied-destination-reply-code else bst_notok dante/denied-destination-reply-code "$W30 -- want the SOCKS5 reply code (2) in curl's diagnostic, got:'$(_bst_1line "$bg_out")'" fi else bst_notok dante/denied-destination-refused "$NOTRUN" bst_notok dante/denied-destination-reply-code "$NOTRUN" fi ## ## 7. What the server wrote about it ## ## The log is the server's own account of the two decisions above, and it is a ## different witness from curl's exit status: it names WHICH rule matched. On ## the native build the two lines are ## info: pass(1): tcp/connect [: 127.0.0.1.N 127.0.0.1.1080 -> ... 127.0.0.1.8099 ## info: block(2): tcp/connect ]: 127.0.0.1.N 127.0.0.1.1080 -> ... 127.0.0.1.8098 ## so the rule NUMBER is asserted, not merely the verdict. ## slog=$(cat "${D}/sockd.log" 2>/dev/null) if has "$slog" "pass(1)" "127.0.0.1.$OPORT"; then bst_ok dante/log-names-the-passing-rule else bst_notok dante/log-names-the-passing-rule "$W30 -- no pass(1) line for port $OPORT in sockd's log; got:'$(_bst_1line "$slog")'"; fi if has "$slog" "block(2)" "127.0.0.1.$DPORT"; then bst_ok dante/log-names-the-blocking-rule else bst_notok dante/log-names-the-blocking-rule "$W30 -- no block(2) line for port $DPORT in sockd's log; got:'$(_bst_1line "$slog")'"; fi