. "$BST_LIB" # BST-TIMEOUT: 600 # GNU inetutils 2.5 -- second half: the ticket's loopback round trips. The telnet # and tftp CLIENTS complete against in.telnetd and in.tftpd, offline, with no # external peer. Split from 88-inetutils.test so each half gets its own guest; # the reason, and the header describing the port as a whole, are in that file. # # NEITHER SERVER HAS A STANDALONE LISTEN MODE -- in.telnetd and in.tftpd both # expect an already-accepted socket on fd 0 -- so "their own servers" here means # what it means everywhere else: run them under a super-server. inetutils' own # inetd is that server, and it is a deliverable of this port in its own right. # # inetd goes where xinetd could not without two patches (see 90-xinetd.test): it # calls no getrlimit at all, and it guards setgid/initgroups/setuid behind # `if (pwd->pw_uid)` (src/inetd.c:453), so with `user root` the ENOSYS credential # wall -- brintos/linux#19, the same one that reds ping in the other half -- is # never touched rather than being hit and fatal. Nothing here is patched around. # # EACH TFTP REQUEST GETS ITS OWN inetd, ON ITS OWN PORT, AND THAT IS A PLATFORM # WORKAROUND -- measured, not assumed. A dgram/`wait` service is re-armed only # from inetd's SIGCHLD handler (reapchild(), which logs "reaped" then "restored # , fd N"), and signal delivery on this kernel is unreliable # (brintos/linux#21). When that SIGCHLD is lost, the socket never returns to the # select set and the service is SINGLE-SHOT for the life of that inetd. A # throwaway probe pinned it exactly: against a FRESH inetd a put succeeds # (8400/8400 bytes) and a get succeeds; against an inetd that has already served # once, BOTH a put and a get are never dispatched, and that inetd's log shows the # execl with no "reaped" and no "restored" after it. So this is not put-vs-get # and not a tftpd defect -- it is one lost signal. The single-shot behaviour is # asserted below in a case of its own, so the gap is surfaced rather than merely # routed around; the other cases give each request a fresh server so that they # test inetutils instead of re-testing brintos/linux#21. # # NOTHING HERE WAITS ON A PROCESS, AND timeout(1) IS NOT USED, for the same # reason: with signals unreliable, `timeout N cmd` is decorative and a blocked # client blocks forever. An earlier version of this file learned that the # expensive way -- `timeout 30 tftp ... put` ran out the whole 600 s bound with # two cases still to go. Every case below spawns its client in the background and # POLLS FOR ITS OWN EVIDENCE: the transferred bytes, or inetd's debug log. require /usr/bin/inetd.inetutils inetutils-net/present TELNET=/usr/bin/telnet.inetutils TFTP=/usr/bin/tftp.inetutils INETD=/usr/bin/inetd.inetutils TELNETD=/usr/sbin/in.telnetd TFTPD=/usr/sbin/in.tftpd D=${TMPDIR:-/tmp}/bst-inetutils-net.$$ rm -rf "$D"; mkdir -p "$D" || { bst_notok inetutils-net/workdir "cannot create $D"; exit 0; } CLI="tclsh $BST_DATA/xinetd-client.tcl" TPORT=9123 # ilog -- tail of one inetd's own debug log, for a failure detail: it is # the only place that says whether the super-server ever staged the server, and # whether it reaped and re-armed afterwards. ilog() { tail -6 "$D/inetd.$1.log" 2>/dev/null | tr '\n' '|'; } # start_inetd -- launch one inetd in the foreground (-d, so # this shell owns it and its commentary goes to a file, never to stdout, which # carries the BST protocol) and wait on its OWN log for the bind. Readiness read # with shell builtins: polling with the tcl client instead would spawn a # multi-MB tclsh per attempt for no extra information. start_inetd() { printf '%s\n' "$2" > "$D/inetd.$1.conf" "$INETD" -d -p "$D/inetd.$1.pid" "$D/inetd.$1.conf" >"$D/inetd.$1.log" 2>&1 & _pid=$! _i=0 while [ $_i -lt 40 ]; do if [ -f "$D/inetd.$1.log" ]; then while IFS= read -r _ln; do case "$_ln" in *"registered "*) return 0 ;; esac done < "$D/inetd.$1.log" fi _i=$((_i+1)); sleep 1 done return 1 } # tftp_bg -- spawn the client in the background on its # own log. Never waited on. tftp_bg() { ( printf '%s' "$3" | "$TFTP" 127.0.0.1 "$2" >"$D/cli.$1.log" 2>&1 ) & } # transferred -- the byte-for-byte verdict, polled. # Split from an exit-status check deliberately: a transfer that "succeeds" having # moved nothing is the failure a status check cannot see. transferred() { _i=0 while [ $_i -lt 60 ]; do [ -s "$3" ] && cmp -s "$2" "$3" && break _i=$((_i+1)); sleep 1 done if cmp -s "$2" "$3"; then bst_ok "inetutils-net/$1" else bst_notok "inetutils-net/$1" "$(wc -c < "$2" | tr -d ' ') bytes offered, $(wc -c < "$3" 2>/dev/null | tr -d ' ') moved -- cli=[$(_bst_1line "$(cat "$D/cli.$1.log" 2>/dev/null)")] inetd=[$(ilog "$4")]"; fi } # ~8 KB of fixed text: seventeen 512-byte TFTP blocks, so a short transfer is # visible while the transfer stays affordable. octet mode throughout, so a path # that is not eight-bit clean shows up too. awk 'BEGIN{for(n=1;n<=140;n++) printf "%04d the quick brown fox jumps over the lazy dog 0123456789\n", n}' > "$D/payload" chmod 644 "$D/payload" # --- telnet: inetd + in.telnetd, a stream/nowait service ---------------------- # telnetd's login program is overridden with -E and pointed at a SCRIPT rather # than an inline command line: inetd.conf has no quoting, so `-E "/bin/echo M"` # would split across two argv slots and telnetd would exec /bin/echo with no # argument. execv() handles the shebang. cat > "$D/greet.sh" <<'EOS' #!/bin/sh echo INETUTILS-TELNETD-OK EOS chmod 755 "$D/greet.sh" # node:service, so getaddrinfo() takes the port NUMERICALLY: nothing here depends # on /etc/services and nothing binds a privileged port. `user root` is what keeps # inetd out of the setuid path (see the header). if start_inetd t "127.0.0.1:$TPORT stream tcp nowait root $TELNETD in.telnetd -h -E $D/greet.sh" then bst_ok inetutils-net/inetd-registered else bst_notok inetutils-net/inetd-registered "inetd never bound $TPORT: $(ilog t)"; fi # inetd accepted, forked, and exec'd in.telnetd, which sent option negotiation # before any login program ran. Split from the round trip below on purpose: it # separates "the super-server could not stage the server at all" from "the server # ran and the client could not talk to it". neg=$($CLI $TPORT "" 3000 2>&1) case "$neg" in *"RESULT ok len=0"*) bst_notok inetutils-net/telnetd-negotiates "connected but in.telnetd sent nothing: $(ilog t)" ;; *"RESULT ok"*) bst_ok inetutils-net/telnetd-negotiates ;; *) bst_notok inetutils-net/telnetd-negotiates "$(_bst_1line "$neg")" ;; esac # THE FIRST ROUND TRIP THE TICKET NAMES: the telnet CLIENT, against in.telnetd on # loopback, receiving a marker produced by a program the SERVER launched on a pty # it allocated. stdin is a pipe held open by a sleep, because the client exits on # EOF and would otherwise race the server's first write. ( sleep 6 | "$TELNET" 127.0.0.1 $TPORT >"$D/telnet.out" 2>&1 ) & i=0 while [ $i -lt 40 ]; do grep -q INETUTILS-TELNETD-OK "$D/telnet.out" 2>/dev/null && break i=$((i+1)); sleep 1 done tout=$(cat "$D/telnet.out" 2>/dev/null) case "$tout" in *INETUTILS-TELNETD-OK*) bst_ok inetutils-net/telnet-server-roundtrip ;; *) if bst_execfail "$tout"; then bst_unrun inetutils-net/telnet-server-roundtrip "$tout" else bst_notok inetutils-net/telnet-server-roundtrip "no marker relayed from the pty: $(_bst_1line "$tout") -- $(ilog t)"; fi ;; esac # telnet is stream/nowait -- inetd accepts and forks per connection and never # needs to re-arm -- so a SECOND connection must work. Asserted, because it is # what separates the dgram single-shot gap below from a general inetd failure. neg2=$($CLI $TPORT "" 3000 2>&1) case "$neg2" in *"RESULT ok len=0"*) bst_notok inetutils-net/telnetd-serves-again "second connection got nothing: $(ilog t)" ;; *"RESULT ok"*) bst_ok inetutils-net/telnetd-serves-again ;; *) bst_notok inetutils-net/telnetd-serves-again "$(_bst_1line "$neg2")" ;; esac # --- tftp: one fresh inetd per request ---------------------------------------- # THE SECOND ROUND TRIP THE TICKET NAMES: retrieve, compared byte for byte. # tftpd with no directory list serves an ABSOLUTE path if it is world-readable, # and accepts a write only into a file that already exists and is world-writable # (validate_access(), src/tftpd.c:531) -- hence the two chmods. start_inetd f1 "127.0.0.1:9169 dgram udp wait root $TFTPD in.tftpd" tftp_bg tftp-get 9169 "$(printf 'binary\nget %s %s\nquit\n' "$D/payload" "$D/got")" transferred tftp-get "$D/payload" "$D/got" f1 start_inetd f2 "127.0.0.1:9170 dgram udp wait root $TFTPD in.tftpd" : > "$D/uploaded"; chmod 666 "$D/uploaded" tftp_bg tftp-put 9170 "$(printf 'binary\nput %s %s\nquit\n' "$D/payload" "$D/uploaded")" transferred tftp-put "$D/payload" "$D/uploaded" f2 # A read of a file that is not there must be REFUSED, not answered with an empty # one -- without this, the get case would pass just as well against a server that # silently produces nothing. start_inetd f3 "127.0.0.1:9171 dgram udp wait root $TFTPD in.tftpd" tftp_bg tftp-nak 9171 "$(printf 'binary\nget %s %s\nquit\n' "$D/no-such-file" "$D/absent")" i=0 while [ $i -lt 40 ]; do grep -qiE "not found|no such|error code" "$D/cli.tftp-nak.log" 2>/dev/null && break i=$((i+1)); sleep 1 done nak=$(cat "$D/cli.tftp-nak.log" 2>/dev/null) case "$nak" in *"not found"*|*"No such"*|*"Error code"*) bst_ok inetutils-net/tftp-nak-missing ;; *) bst_notok inetutils-net/tftp-nak-missing "no NAK for a missing file: $(_bst_1line "$nak") -- $(ilog f3)" ;; esac # --- the gap itself, asserted rather than only worked around ------------------ # A second request to a dgram/wait inetd that has already served one. This is # expected to fail today and is kept for exactly that reason: when # brintos/linux#21 is fixed it turns green on its own, and the per-request inetd # instances above can then collapse back into one. inetd f1 has served its get. tftp_bg tftp-second 9169 "$(printf 'binary\nget %s %s\nquit\n' "$D/payload" "$D/got2")" i=0 while [ $i -lt 20 ]; do [ -s "$D/got2" ] && cmp -s "$D/payload" "$D/got2" && break i=$((i+1)); sleep 1 done if cmp -s "$D/payload" "$D/got2" 2>/dev/null; then bst_ok inetutils-net/tftp-wait-service-second-request else bst_notok inetutils-net/tftp-wait-service-second-request \ "brintos/linux#21 (signal delivery unreliable): inetd re-arms a dgram/wait socket only from its SIGCHLD handler, so a lost SIGCHLD makes the service single-shot -- no 'reaped'/'restored' follows the execl: $(ilog f1)" fi rm -rf "$D"