. "$BST_LIB" # BST-TIMEOUT: 900 # tinyproxy 1.11.1 -- the small HTTP/HTTPS proxy daemon (meta-networking # recipes-support/tinyproxy). This image's FIRST FORWARD PROXY: it already has an # HTTP server (lighttpd, 88-lighttpd) and HTTP clients (wget, curl), and this is # the thing that goes between them. # # READ THIS FIRST, BECAUSE IT DECIDES HOW HALF THE FILE SCORES. # tinyproxy 1.11 HANDLES EVERY CONNECTION ON ITS OWN pthread. child.c:211 calls # pthread_create() for each accept(2) -- unconditionally, for the first connection # as much as the hundredth -- and there is NO threadless build mode to select: # configure.ac has no --disable-threads counterpart (its only TP_ARG_ENABLEs are # debug, xtinyproxy, filter, upstream, reverse, transparent and manpage_support) # and src/Makefile.am links -lpthread unconditionally. So the thread pipeline IS # the request path. # # That is the squashfs-tools / iperf3 shape exactly # (brintos/linux-in-the-browser#85, with brintos/hardwarejs#12 behind it), and it # means EVERY CASE THAT PUTS A REQUEST THROUGH THE PROXY -- the ticket's own # criterion included -- is downstream of that gap. Those cases are left FAILING # and attributed rather than skipped, so they flip green by themselves when the # per-task park slots land. What is green today is everything up to and including # accept(2): the daemon starts, reads its config, honours the Filter directive, # binds the port and appears as a LISTEN row in the kernel's own /proc/net/tcp. # # THE SIGNATURE IS NOT ASSERTED, DELIBERATELY. #85 has now presented four # different ways across its witnesses -- set-over-set at the create, glibc's # futex fatal one call later in pthread_join, orphan-at-clear, and a plain EAGAIN # from pthread_create with no trap at all (tiobench). A suite keyed on any one # string would read the other three as a fix. So each case below asserts the # ABSENCE OF SUCCESS and carries whatever the run actually printed in the red's # detail. tinyproxy's own handling adds a fifth possibility: child.c treats a # non-zero pthread_create as an OOM condition, closes the accepted fd and keeps # looping, so on the EAGAIN shape the client sees a clean connection reset and # the daemon survives. All of them are red here and none of them is matched on. # # NO PRIVILEGE DROP IS ATTEMPTED BY THE GENERATED CONFIG, so brintos/linux#19 # never engages in the cases that matter. main.c:383 calls change_user() only # when geteuid()==0 -- which is true here -- but change_user() is a no-op unless # config->user or config->group is set, and the generated config below sets # neither. The SHIPPED /etc/tinyproxy.conf sets both AND a PidFile under a # directory this image has no systemd unit to create, and cannot start as a # result; which of those two walls it reaches first is scored on its own terms # rather than avoided -- see tinyproxy/shipped-conf-refused-at-known-wall. # # OFFLINE AND NUMERIC BY CONSTRUCTION. The origin server, the proxy and the # client are all on 127.0.0.1 by address, so nothing traverses getaddrinfo(3)'s # dns leg -- independently broken on this image (linux-in-the-browser#96) and it # would otherwise turn a proxy result into a resolver result. The one hostname # that appears anywhere is the FILTERED destination, which tinyproxy refuses # BEFORE it resolves anything (reqs.c:475 runs the filter, reqs.c:485 answers # 403), so that case needs no resolver either. # # PORTS: 8090 for the origin lighttpd and 8888 for the proxy. Not 8080/8081 -- # 88-lighttpd holds those and nothing on this image can be killed # (brintos/linux#21), so its servers outlive its own suite. require /usr/bin/tinyproxy tinyproxy/present TP=/usr/bin/tinyproxy LT=/usr/sbin/lighttpd D=${TMPDIR:-/tmp}/bst-tinyproxy.$$ rm -rf "$D" mkdir -p "$D/www" "$D/upload" || { bst_notok tinyproxy/workdir "cannot create $D"; exit 0; } OPORT=8090 PPORT=8888 # The proxy under test must be reached through the variables this suite sets, # never inherited from the environment: an http_proxy already exported by the # runner would silently send every wget below through somebody else's proxy. http_proxy=; https_proxy=; no_proxy=; export http_proxy https_proxy no_proxy # bg : run something that MAY NEVER RETURN in the # background and poll up to for it to finish, without wait(1) -- waiting # on a process this kernel cannot kill would block until the suite bound. The rc # is carried out through a file rather than $?, which is what makes the poll # possible. Sets bg_done=1/0, and bg_rc/bg_out when bg_done=1. Every request in # this file goes through it, because "the connection is accepted and then nothing # ever answers" is the exact shape #85 produces here. 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 } # 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 } # listening : echo the LISTEN row for , empty if there is none. # ss/netstat read kernel state and cannot block, so this bounds a daemon's # startup without opening a connection a broken daemon could hold open. listening() { (ss -ltn 2>/dev/null || netstat -ltn 2>/dev/null) | grep ":$1"; } # waitlisten : poll for a listener, echo it when it appears. 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" } ## ## 1. The shipped artifacts ## out=$("$TP" -v 2>&1) if has "$out" "tinyproxy 1.11.1"; then bst_ok tinyproxy/version else bst_notok tinyproxy/version "$(_bst_1line "$out")"; fi check tinyproxy/shipped-conf-file test -f /etc/tinyproxy.conf check tinyproxy/default-error-page test -f /usr/share/tinyproxy/default.html check tinyproxy/stat-page test -f /usr/share/tinyproxy/stats.html # -h prints the features the recipe's five --enable switches compiled in, so this # reads them back OFF THE TARGET BINARY rather than trusting EXTRA_OECONF. A # recipe that lost a switch is a red here rather than a surprise in some later # port that wanted reverse-proxying. check_out tinyproxy/feature-xtinyproxy "XTinyproxy header" "$TP" -h check_out tinyproxy/feature-filtering "Filtering" "$TP" -h check_out tinyproxy/feature-transparent "Transparent proxy support" "$TP" -h check_out tinyproxy/feature-reverse "Reverse proxy support" "$TP" -h check_out tinyproxy/feature-upstream "Upstream proxy support" "$TP" -h # useradd.bbclass ran: the bbappend drops the TARGET shadow package from DEPENDS # and RDEPENDS, so what creates this account is shadow-NATIVE through pseudo at # do_install and tinyproxy's own preinst at rootfs assembly. If the drop ever # took the account with it, this is where it shows. if grep -q '^tinyproxy:' /etc/passwd 2>/dev/null; then bst_ok tinyproxy/useradd-ran else bst_notok tinyproxy/useradd-ran "no tinyproxy account in /etc/passwd" fi # No systemd on this image, so systemd.bbclass's rm_systemd_unitdir must have # taken the unit the recipe's do_install:append would otherwise have written. # # TINYPROXY'S OWN UNIT PATHS ONLY, AND THE NARROWING IS THE POINT. This first # read `[ ! -e /lib/systemd ] && ...', which asserts an IMAGE-WIDE property no # single port owns: /lib/systemd exists on this image because nuttcp ships # nuttcp.socket and nuttcp@.service into it, so a sibling package's packaging # reddened tinyproxy's case while tinyproxy's own unit was correctly absent. A # port's suite scores the port. (The stray nuttcp units are real and are that # package's to answer for, not this one's.) TP_UNIT_LEFT='' for _u in /lib/systemd/system/tinyproxy.service \ /usr/lib/systemd/system/tinyproxy.service \ /etc/systemd/system/tinyproxy.service; do if [ -e "$_u" ]; then TP_UNIT_LEFT="$TP_UNIT_LEFT $_u"; fi done if [ -z "$TP_UNIT_LEFT" ]; then bst_ok tinyproxy/no-systemd-unit else bst_notok tinyproxy/no-systemd-unit "a tinyproxy systemd unit survived onto a non-systemd image:$TP_UNIT_LEFT" fi ## ## 2. The config is read, and the Filter directive with it -- all offline ## # THE SHIPPED CONFIG CANNOT START ON THIS IMAGE, and it walks into TWO # independent walls. Which one it hits first is decided by main.c, and the order # is the opposite of what this file first assumed: # # main.c:374 pidfile_create (config->pidpath) -> EX_OSERR <-- FIRST # main.c:383 change_user (argv[0]) -> EX_NOPERM # # * THE PIDFILE WALL, which is the one that actually engages. The shipped # /etc/tinyproxy.conf sets PidFile "/var/run/tinyproxy/tinyproxy.pid". # /var/run -> ../run resolves fine here, but the tinyproxy/ SUBDIRECTORY # under it does not exist and pidfile_create() does not create it # (utils.c:189 goes straight to create_file_safely, no mkdir), so the open # is ENOENT and main exits "Could not create PID file." What would normally # have made that directory is the unit's RuntimeDirectory=, and the unit is # removed on this non-systemd image -- see tinyproxy/no-systemd-unit above. # * THE PRIVDROP WALL, which is real but unreachable behind the first. The # config sets User tinyproxy and Group nogroup, so change_user() would call # setgid(2) then setuid(2) -- both ENOSYS here (brintos/linux#19, the wall # mosquitto, atftp, pure-ftpd and dnsmasq already document) -- and exit # EX_NOPERM. # # An earlier revision of this file asserted the privdrop message alone and was # red for it: the daemon was refused exactly as predicted, one wall earlier than # predicted. BOTH are accepted below, and the red detail names which one ran, so # the case still fails loudly if some THIRD reason takes over -- which is the # whole reason it is split from tinyproxy/shipped-conf-refused. # # The port does NOT edit the shipped file and does NOT drop its User/Group lines. # Creating the account would not make it start -- the credential setters are # ENOSYS whatever the account is -- and rewriting a package's own default config # is a behavioural change BRINTOS-675, whose criterion is stated on a STAGED # config, gives a reviewer no way to check. So the fact is asserted instead of # hidden, and it flips green by itself the day #19 and BRINTOS-626 both close. bg 25 shipped setsid "$TP" -d -c /etc/tinyproxy.conf # THE EXIT REASON IS AT THE END OF THE OUTPUT AND _bst_1line KEEPS THE FIRST 100 # CHARACTERS. The shipped config trips three `WARNING: obsolete config item' # lines (MinSpareServers/MaxSpareServers/StartServers, 186/187/193 -- 1.11 runs # one thread per connection and no longer preforks), and those alone overrun the # budget: the run that sent this suite to the fix phase reported nothing but # them, so WHY the daemon exited was invisible in the very case whose whole job # is to report it. Drop the known noise and keep the tail. SHIPPED_WHY=$(printf '%s\n' "$bg_out" | grep -v 'obsolete config item' | tail -3) if [ "$bg_done" -eq 1 ] && [ "$bg_rc" != "0" ]; then bst_ok tinyproxy/shipped-conf-refused else bst_notok tinyproxy/shipped-conf-refused "the shipped /etc/tinyproxy.conf started (done=$bg_done rc=$bg_rc) -- $(_bst_1line "$SHIPPED_WHY")" fi # ... and it is refused at one of the two documented walls rather than for some # third reason. Split from the case above so that a change in WHY it fails is # visible: a green tinyproxy/shipped-conf-refused with this one red would mean # both walls are gone and something else took over. # # Renamed from tinyproxy/shipped-conf-refused-at-privdrop, which named an outcome # main.c cannot reach while the PidFile wall stands. if has "$bg_out" "Could not create PID file"; then bst_ok tinyproxy/shipped-conf-refused-at-known-wall elif has "$bg_out" "Unable to change to group" || has "$bg_out" "Unable to change to user"; then bst_ok tinyproxy/shipped-conf-refused-at-known-wall else bst_notok tinyproxy/shipped-conf-refused-at-known-wall "expected the PidFile refusal (no /run/tinyproxy) or the setgid/setuid refusal (brintos/linux#19) -- got $(_bst_1line "$SHIPPED_WHY")" fi PAYLOAD=BST-TINYPROXY-PAYLOAD-5f6a7b8c printf 'proxied %s\n' "$PAYLOAD" > "$D/www/fixture.txt" # A BINARY payload built from every byte value 1..255 (NUL excluded only because # it cannot survive shell command substitution), repeated to ~32 KB so a proxied # response crosses tinyproxy's relay buffer rather than fitting in one read. # Deterministic and generated ON TARGET, so the comparison is against the very # bytes this machine wrote and needs no oracle and no network. esc=''; i=1 while [ "$i" -lt 256 ]; do esc="$esc\\$(printf '%03o' "$i")"; i=$((i+1)); done printf "$esc" > "$D/block.bin" : > "$D/www/payload.bin" i=0 while [ "$i" -lt 128 ]; do cat "$D/block.bin" >> "$D/www/payload.bin"; i=$((i+1)); done BINSZ=$(wc -c < "$D/www/payload.bin" | tr -d ' ') check_eq tinyproxy/payload-built 32640 "$BINSZ" # The staged ACL, in tinyproxy's two distinct senses, because the ticket's # "denied destination" is only one of them and conflating them is easy: # Allow/Deny -- who may USE the proxy (the CLIENT's address). acl.c. # Filter -- where the proxy may GO (the DESTINATION host). filter.c, # compiled in by --enable-filter, answered with 403 "Filtered" # at reqs.c:485 BEFORE any name is resolved or any socket opened. # Both are staged here. The destination half is the one the ticket names. printf '^blocked\\.invalid$\n' > "$D/filter" cat > "$D/tinyproxy.conf" <filter and exit(EX_DATAERR)s when it cannot -- # and main.c calls it BEFORE child_listening_sockets(), so pointing Filter at a # file that does not exist must make tinyproxy die at startup with rc 65. A # tinyproxy built WITHOUT --enable-filter would ignore the directive entirely and # start normally, so this case is also the falsifiable proof that the switch took. sed -e "s#^Filter .*#Filter \"$D/no-such-filter-file\"#" "$D/tinyproxy.conf" > "$D/badfilter.conf" bg 25 badfilter setsid "$TP" -d -c "$D/badfilter.conf" if [ "$bg_done" -eq 1 ] && [ "$bg_rc" = "65" ]; then bst_ok tinyproxy/filter-file-required else bst_notok tinyproxy/filter-file-required "want rc=65 (EX_DATAERR) for an unreadable Filter file, got done=$bg_done rc=$bg_rc -- $(_bst_1line "$bg_out")" fi # The negative that keeps the config parser honest: an unknown directive must be # refused, or "it accepted our config" would say nothing about whether it read it. printf 'NoSuchDirective yes\n' > "$D/bogus.conf" cat "$D/tinyproxy.conf" >> "$D/bogus.conf" bg 25 bogus setsid "$TP" -d -c "$D/bogus.conf" if [ "$bg_done" -eq 1 ] && [ "$bg_rc" != "0" ]; then bst_ok tinyproxy/bad-directive-refused else bst_notok tinyproxy/bad-directive-refused "accepted a config carrying NoSuchDirective (done=$bg_done rc=$bg_rc)" fi ## ## 3. The origin server -- the lighttpd this image already ships, per the ticket ## # server.event-handler = "poll" because epoll_create/epoll_create1 are ENOSYS here # and linux-sysepoll is lighttpd's default, so fdevent_init would fail and the # server would never listen. server.upload-dirs because its default /var/tmp does # not resolve on this image. Both are 88-lighttpd's findings, reused rather than # rediscovered; neither is tinyproxy's business and neither is asserted here. # BRINTOS-WORKAROUND: brintos/linux#53 remove — drop this server.event-handler # line once epoll_create is wired. cat > "$D/lighttpd.conf" < "text/plain", ".bin" => "application/octet-stream" ) EOF if [ -x "$LT" ]; then "$LT" -D -f "$D/lighttpd.conf" > "$D/lighttpd.out" 2>&1 & OLST=$(waitlisten "$OPORT" 30) else OLST='' fi if [ -n "$OLST" ]; then ORIGIN_UP=1; bst_ok tinyproxy/origin-serves else ORIGIN_UP=0 bst_notok tinyproxy/origin-serves "no origin lighttpd on :$OPORT -- $(_bst_1line "$(cat "$D/lighttpd.out" "$D/lighttpd.err" 2>/dev/null)")" fi # The origin is reachable WITHOUT the proxy. This is the control: every proxied # case below is only meaningful if a direct fetch of the same bytes works, and # without it a red proxy case could not be told from a broken fixture. # # THE CONTROL HAS ITS OWN PLATFORM WALL, AND IT IS NOT TINYPROXY'S. /usr/bin/wget # is a 46,449,835-byte static binary, and late in a long multi-suite boot the # guest heap is fragmented enough that binfmt_wasm refuses to stage it: # # [k] binfmt_wasm: staging a 46449835-byte image needs more than 1024 segments # (heap too fragmented to hand out 4096-byte blocks; cap # WASM32_IMG_MAX_SEGS=1024) -- refusing -ENOMEM # # which surfaces to the shell as `wget: Cannot allocate memory' and rc=126 -- an # exec that never happened, not a fetch that failed. That is brintos/linux#44 # (execve of a large binary fails -ENOMEM under fragmentation), whose own # corroborating comment measures this exact shape across a long selftest sweep. # The red is left standing and attributed rather than skipped: it flips green by # itself when #44 closes, and matching the exec refusal here keeps it from # reading as a broken fixture. if [ "$ORIGIN_UP" -eq 1 ]; then bg 30 direct wget -q -O "$D/direct.txt" "http://127.0.0.1:$OPORT/fixture.txt" if [ "$bg_done" -eq 1 ] && [ "$bg_rc" -eq 0 ] && cmp -s "$D/www/fixture.txt" "$D/direct.txt"; then bst_ok tinyproxy/origin-direct-fetch elif has "$bg_out" "Cannot allocate memory"; then bst_notok tinyproxy/origin-direct-fetch "brintos/linux#44 (execve -ENOMEM under heap fragmentation): binfmt_wasm refused to stage the 46 MB wget image, so the control fetch never execed -- not a tinyproxy result (done=$bg_done rc=$bg_rc) -- $(_bst_1line "$bg_out")" else bst_notok tinyproxy/origin-direct-fetch "direct fetch of the fixture failed (done=$bg_done rc=$bg_rc) -- $(_bst_1line "$bg_out")" fi else bst_notok tinyproxy/origin-direct-fetch "origin never listened, so the control fetch did not run" fi ## ## 4. The proxy starts, and gets as far as accept(2) ## # setsid, and it is a workaround rather than tidiness: a #85 thread trap can # poison a lane that a LATER, UNRELATED exec then hits, and under # brintos-selftest's busy-polling supervisor that wedges the whole guest rather # than the process (measured for libc-bench, BRINTOS-347). Without the session # boundary the expected reds below would forfeit the rest of this batch's slice, # which is other packages' coverage. # BRINTOS-WORKAROUND: brintos/linux-in-the-browser#85 revisit — drop the setsid # once the per-task park slots land. setsid "$TP" -d -c "$D/tinyproxy.conf" > "$D/tinyproxy.out" 2>&1 & PLST=$(waitlisten "$PPORT" 30) if [ -n "$PLST" ]; then PROXY_UP=1; bst_ok tinyproxy/starts else PROXY_UP=0 bst_notok tinyproxy/starts "no listener on :$PPORT -- $(_bst_1line "$(cat "$D/tinyproxy.out" "$D/tinyproxy.log" 2>/dev/null)")" fi # The listener is where Listen put it and nowhere else -- that one directive is # all that stands between an open proxy and every interface this guest has. if [ -z "$PLST" ]; then bst_notok tinyproxy/listens-loopback-only "no TCP listener on :$PPORT" elif has "$PLST" "127.0.0.1:$PPORT"; then bst_ok tinyproxy/listens-loopback-only else bst_notok tinyproxy/listens-loopback-only "listener is not loopback-bound -- $(_bst_1line "$PLST")" fi check tinyproxy/pid-file test -s "$D/tinyproxy.pid" # The log the config asked for exists and carries the startup line, which is the # proof that setup_logging() ran -- it happens AFTER change_user(), so on a config # that had tried to drop privileges there would be no log at all. if grep -q "Accepting connections" "$D/tinyproxy.log" 2>/dev/null; then bst_ok tinyproxy/log-startup else bst_notok tinyproxy/log-startup "no 'Accepting connections' line -- $(_bst_1line "$(cat "$D/tinyproxy.log" "$D/tinyproxy.out" 2>/dev/null)")" fi ## ## 5. THE TICKET'S NAMED CRITERION -- a loopback proxy fetch ## # Everything in this section needs one pthread per connection and is therefore # held by brintos/linux-in-the-browser#85. Each case is bounded by bg() so that a # connection tinyproxy accepts and never answers costs this case a red rather # than costing the file its suite timeout, and each carries what actually # happened in its detail instead of matching a signature. # THE BOUND ON EACH REQUEST IS ADAPTIVE, AND THAT IS A COST DECISION RATHER THAN # a correctness one. Under #85 every one of the five requests below is accepted # and then never answered, so each one costs its FULL bound -- five fixed 60s # bounds spent 300s of this suite's 361s waiting for replies that #85 guarantees # will not come. That is the longest suite in its batch by a factor of two, and a # batch boot that ends before it finishes reports NONE of its 30 cases, because # the runner replays a suite's output only when the suite ends: the port is then # dropped for producing no results, which is what happened on 2026-08-21. # # So the FIRST request carries the long bound and doubles as the probe. If it # comes back, the thread pipeline works and every later request keeps the long # bound. If it times out, #85 is engaged, the remaining four are known-dead # before they start, and a long wait buys nothing that the first one did not # already buy -- they drop to REQ_HELD. The cases still run, still make real # requests and still assert the absence of success, so they flip green by # themselves when #85 closes, exactly as before; only the waiting is shortened. REQ_PROBE=20 REQ_HELD=8 REQB=$REQ_PROBE WALL85="brintos/linux-in-the-browser#85 (pthread_create does not produce a running thread on this arm): tinyproxy 1.11 creates one pthread per accepted connection (child.c:211) and has no threadless build mode, so no request can be served" # wget with http_proxy -- the ticket's own words. -O writes the BODY ALONE, so # cmp(1) compares served bytes against the source file with nothing to strip. if [ "$PROXY_UP" -eq 1 ] && [ "$ORIGIN_UP" -eq 1 ]; then bg $REQB proxied env http_proxy="http://127.0.0.1:$PPORT/" wget -q -O "$D/proxied.txt" "http://127.0.0.1:$OPORT/fixture.txt" # This is the probe: what it did decides what the other four wait for. [ "$bg_done" -eq 1 ] || REQB=$REQ_HELD if [ "$bg_done" -ne 1 ]; then bst_notok tinyproxy/proxied-fetch "$WALL85 -- wget never returned within ${REQ_PROBE}s (the accepted connection was never answered)" elif [ "$bg_rc" -ne 0 ]; then bst_notok tinyproxy/proxied-fetch "$WALL85 -- wget rc=$bg_rc $(_bst_1line "$bg_out"); daemon said $(_bst_1line "$(tail -3 "$D/tinyproxy.log" 2>/dev/null)")" elif cmp -s "$D/www/fixture.txt" "$D/proxied.txt"; then bst_ok tinyproxy/proxied-fetch else bst_notok tinyproxy/proxied-fetch "proxied body differs from the source: $(_bst_1line "$(cmp "$D/www/fixture.txt" "$D/proxied.txt" 2>&1)")" fi else bst_notok tinyproxy/proxied-fetch "did not run: proxy_up=$PROXY_UP origin_up=$ORIGIN_UP" fi # The same fetch at 32640 bytes, which is the one that would cross the relay # buffer rather than fitting in a single read. Kept separate from the small # fixture because "it proxies a line" and "it proxies a stream" are different # claims about the same daemon. if [ "$PROXY_UP" -eq 1 ] && [ "$ORIGIN_UP" -eq 1 ]; then bg $REQB proxiedbin env http_proxy="http://127.0.0.1:$PPORT/" wget -q -O "$D/proxied.bin" "http://127.0.0.1:$OPORT/payload.bin" if [ "$bg_done" -eq 1 ] && [ "$bg_rc" -eq 0 ] && cmp -s "$D/www/payload.bin" "$D/proxied.bin"; then bst_ok tinyproxy/proxied-fetch-byte-exact else bst_notok tinyproxy/proxied-fetch-byte-exact "$WALL85 -- done=$bg_done rc=$bg_rc got $(wc -c < "$D/proxied.bin" 2>/dev/null | tr -d ' ') of $BINSZ bytes $(_bst_1line "$bg_out")" fi else bst_notok tinyproxy/proxied-fetch-byte-exact "did not run: proxy_up=$PROXY_UP origin_up=$ORIGIN_UP" fi # The Via header tinyproxy adds is what distinguishes "the body arrived" from # "the body arrived THROUGH THIS PROXY" -- a wget that silently ignored # http_proxy and fetched the origin directly would pass the two cases above and # fail this one. ViaProxyName is set to a string nothing else on this image emits. if [ "$PROXY_UP" -eq 1 ] && [ "$ORIGIN_UP" -eq 1 ]; then bg $REQB viahdr env http_proxy="http://127.0.0.1:$PPORT/" wget -q -S -O /dev/null "http://127.0.0.1:$OPORT/fixture.txt" if [ "$bg_done" -eq 1 ] && has "$bg_out" "brintos-selftest"; then bst_ok tinyproxy/via-header else bst_notok tinyproxy/via-header "$WALL85 -- no Via: ... brintos-selftest in the response headers (done=$bg_done rc=$bg_rc) $(_bst_1line "$bg_out")" fi else bst_notok tinyproxy/via-header "did not run: proxy_up=$PROXY_UP origin_up=$ORIGIN_UP" fi ## ## 6. The staged ACL refuses a denied destination -- the ticket's second criterion ## # blocked.invalid matches the staged Filter file, so reqs.c:475 runs the filter # and reqs.c:485 answers 403 "Filtered" BEFORE resolving the name or opening any # socket. .invalid is reserved by RFC 2606 and can never resolve, which is the # point: a 403 here can only have come from the filter, and a request that # somehow escaped it could not succeed by accident. wget reports a 403 as rc 8 # and prints the status line with -S. if [ "$PROXY_UP" -eq 1 ]; then bg $REQB denied env http_proxy="http://127.0.0.1:$PPORT/" wget -q -S -O /dev/null "http://blocked.invalid/anything" if [ "$bg_done" -eq 1 ] && has "$bg_out" "403"; then bst_ok tinyproxy/filtered-destination-refused else bst_notok tinyproxy/filtered-destination-refused "$WALL85 -- expected a 403 from the staged Filter, got done=$bg_done rc=$bg_rc $(_bst_1line "$bg_out")" fi else bst_notok tinyproxy/filtered-destination-refused "did not run: the proxy never listened" fi # The refusal is logged as a filter decision, which is the server-side half of # the case above: a 403 could in principle come from somewhere else, but # "Proxying refused on filtered domain" (reqs.c:480) can only come from filter.c. if grep -q "refused on filtered" "$D/tinyproxy.log" 2>/dev/null; then bst_ok tinyproxy/filtered-destination-logged else bst_notok tinyproxy/filtered-destination-logged "$WALL85 -- no filter decision in the log $(_bst_1line "$(tail -3 "$D/tinyproxy.log" 2>/dev/null)")" fi # The client-side ACL, which is the OTHER thing "ACL" means in tinyproxy and is # staged by the Allow 127.0.0.1 line. With any Allow present the default action # is DENY, so the fact that the loopback requests above were even attempted is # downstream of that line. This case asserts the negative: an address outside the # Allow list must be refused. 127.0.0.2 is still loopback on this kernel, so no # route and no interface is needed -- only a source address the ACL does not # list. wget's --bind-address picks it. if [ "$PROXY_UP" -eq 1 ]; then bg $REQB acldeny env http_proxy="http://127.0.0.1:$PPORT/" wget -q -S --bind-address=127.0.0.2 -O /dev/null "http://127.0.0.1:$OPORT/fixture.txt" if [ "$bg_done" -eq 1 ] && [ "$bg_rc" -ne 0 ]; then bst_ok tinyproxy/client-acl-denies-unlisted else bst_notok tinyproxy/client-acl-denies-unlisted "$WALL85 -- a client outside the staged Allow 127.0.0.1 was served (done=$bg_done rc=$bg_rc) $(_bst_1line "$bg_out")" fi else bst_notok tinyproxy/client-acl-denies-unlisted "did not run: the proxy never listened" fi ## ## 7. The platform walls, scored rather than avoided ## # THE THREAD GAP ITSELF, asserted once and directly rather than only inferred # from the reds above. tinyproxy logs every connection at LogLevel Info # ("Connect (file descriptor N)", reqs.c), and that line is written by the CHILD # THREAD -- child_thread() -> handle_connection(). So a log carrying the startup # line but no Connect line is the on-target statement that accept(2) works and # the thread after it does not, which is exactly what #85 says. It flips green by # itself when the per-task park slots land. if grep -q "Connect (file descriptor" "$D/tinyproxy.log" 2>/dev/null; then bst_ok tinyproxy/child-thread-runs else bst_notok tinyproxy/child-thread-runs "$WALL85 -- the daemon listened and accepted, but no child thread ever reached handle_connection to log a Connect line $(_bst_1line "$(tail -5 "$D/tinyproxy.log" 2>/dev/null)")" fi # SIGTERM is tinyproxy's documented shutdown (main.c's takesig sets config->quit) # and it does not arrive: this kernel delivers no signal at all, SIGKILL # included. Asserted rather than skipped because "the proxy cannot be stopped" is # a real property of running tinyproxy on this image. NEVER wait(1) on the pid. tppid=$(cat "$D/tinyproxy.pid" 2>/dev/null) if [ -n "$tppid" ] && kill -0 "$tppid" 2>/dev/null; then kill -TERM "$tppid" 2>/dev/null i=0 while [ "$i" -lt 5 ] && kill -0 "$tppid" 2>/dev/null; do i=$((i+1)); sleep 1; done if kill -0 "$tppid" 2>/dev/null; then kill -KILL "$tppid" 2>/dev/null sleep 1 if kill -0 "$tppid" 2>/dev/null; then bst_notok tinyproxy/sigterm-stops "brintos/linux#21 (signal delivery does not work at all): the proxy survived SIGTERM for ${i}s and then SIGKILL, so no signal reached it" else bst_notok tinyproxy/sigterm-stops "ignored SIGTERM for ${i}s but died on SIGKILL -- that is tinyproxy's handler, not the kernel" fi else bst_ok tinyproxy/sigterm-stops fi else bst_notok tinyproxy/sigterm-stops "no live pid to signal (pidfile said '$tppid')" fi rm -rf "$D"