. "$BST_LIB" # BST-TIMEOUT: 900 # ncp 1.2.4 -- second half: the transfers. BOTH HALVES OF THE PAIR RUN HERE, SO # THE COPY IS REAL. Split from 88-ncp.test so each half gets its own guest; the # reason, and the header describing the port as a whole, are in that file. # # NAMING THE PEER SKIPS THE RENDEZVOUS, and that is what makes a same-host copy # over `lo' a genuine end-to-end transfer rather than a probe. npush binds TCP # 8002 and npoll connects to it. Every case below compares the DELIVERED BYTES # with cmp(1) rather than trusting an exit status: ncp's own BUGS section is # about errors the peer never sees, so a status check is exactly the wrong # instrument here. The transfer is run in all three address spellings, which are # three different paths through libowfat's noipv6 fallback and must all land on # connect4(127.0.0.1), and in both modes -- stdin/stdout, which needs nothing but # libowfat and the socket layer, and file mode, which is tar on both sides. # # WHAT ncp DOES NOT VALIDATE IS PART OF THE CRITERION. ncp never opens a source # file: it execs tar after the connection is up, so a missing file is GNU tar's # `Cannot stat' and GNU tar's exit 2, delivered on the SENDER's stderr with -- # as npush(1) BUGS puts it -- "The peer will see no error messages!". That is # asserted literally: the sender's status and text, and the receiver's silence. # # NOTHING HERE USES timeout(1). On this image GNU timeout never expires -- # coreutils 9.4 arms its bound with alarm(2), unwired on wasm32 # (brintos/linux#77) and failing silently, and there is no busybox applet to fall # back to. Signals themselves are fine: a direct `kill -TERM' reaps a peerless # npush in 0 s. So every invocation that can outlive its usefulness -- npush # waiting for a peer, npoll waiting for an announcement -- is started in the # BACKGROUND and stopped with kill. An npoll GIVEN AN ADDRESS needs no bound at # all: with no listener it is ECONNREFUSED and dies at once. The full measurement # is in 88-ncp.test's header. require ncp ncp/present D=${TMPDIR:-/tmp}/bst-ncpnet.$$ rm -rf "$D"; mkdir -p "$D/src/sub" "$D/dst" || { bst_notok ncpnet/workdir "cannot create $D"; exit 0; } bst_ok ncpnet/workdir say() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "want '$2' in: $(_bst_1line "$3")" ;; esac } nosay() { case "$3" in *"$2"*) bst_notok "$1" "unexpected '$2' in: $(_bst_1line "$3")" ;; *) bst_ok "$1" ;; esac } # --- the payloads ------------------------------------------------------------- # 48000 bytes of fixed text: many transfer blocks, so a short copy is visible. awk 'BEGIN{for(n=1;n<=800;n++) printf "%04d the quick brown fox jumps over the lazy dog 0123456789\n", n}' > "$D/txt.src" # Every byte value 0..255. LC_ALL=C is load-bearing: under a UTF-8 locale awk's # %c emits the UTF-8 ENCODING of 128..255 and the payload silently stops covering # the high half. One awk process, not 256 forks -- fork is expensive on this # guest. LC_ALL=C awk 'BEGIN{for(i=0;i<256;i++) printf "%c", i}' > "$D/bin.src" check_eq ncpnet/payload-text 48000 "$(wc -c < "$D/txt.src" 2>/dev/null | tr -d ' ')" check_eq ncpnet/payload-binary 256 "$(wc -c < "$D/bin.src" 2>/dev/null | tr -d ' ')" cp "$D/txt.src" "$D/src/a.txt" printf 'nested payload\n' > "$D/src/sub/b.txt" # --- the transfer harness ----------------------------------------------------- # npush is backgrounded WITHOUT a subshell so that $! is npush itself and a kill # reaches it; a `( ... ) &' wrapper would leave it orphaned on port 8002 and # poison the next case. A listener with no filesystem artifact cannot be waited # on, so the client retries -- a small budget, because a refused connect returns # at once and a large one would only spend the suite bound on the failure path. TRIES=6 RT_PUSH_RC=""; RT_POLL_RC="" # listener_up -- poll for npush's TCP listener on 8002 before the # first npoll. A fixed sleep is not a readiness signal: staging a 3 MB wasm exec # takes seconds on a guest late in a run, and an npoll that runs first reports # `Connection refused' -- a red whose cause is the harness, not the port. # /proc/net/tcp is the real signal (0x1F42 = 8002, st 0A = LISTEN); npush's own # first diagnostic is the fallback, because a kernel that grows CONFIG_IPV6 would # stop printing it and a readiness poll that quietly stops working is worse than # none. LISTENER_SEEN=no listener_up() { _s=0 while [ "$_s" -lt 20 ]; do if [ -r /proc/net/tcp ]; then awk '$2 ~ /:1F42$/ && $4 == "0A" { f=1 } END { exit !f }' /proc/net/tcp 2>/dev/null && { LISTENER_SEEN=yes; return 0; } fi [ -s "$1" ] && return 0 _s=$((_s+1)); sleep 1 done return 1 } # settle -- harvest npush. On the success path it exits by itself and wait # gives its status, which several cases assert. On the failure path it is still # looping, so TERM it first; RT_PUSH_RC is then meaningless and is left empty. settle() { if [ "$RT_POLL_RC" -eq 0 ]; then wait "$1"; RT_PUSH_RC=$? else kill -TERM "$1" 2>/dev/null; wait "$1" 2>/dev/null; RT_PUSH_RC=""; fi sleep 1 } # rt_stdin rt_stdin() { rm -f "$2" # The `&' forks before the redirect, and $1 is always a caller's $D/* path. # BST-REDIRECT-OK: background command, and $1 is under $TMPDIR, never the rootfs. npush < "$1" > "$D/rt.push.out" 2> "$D/rt.push.err" & _rp=$! listener_up "$D/rt.push.err" || true _i=0; RT_POLL_RC=1 while [ "$_i" -lt "$TRIES" ]; do npoll "$3" > "$2" 2> "$D/rt.poll.err"; RT_POLL_RC=$? [ "$RT_POLL_RC" -eq 0 ] && break _i=$((_i+1)); sleep 1 done settle "$_rp" } # rt_files ... -- file mode: tar on both sides. The `sh # -c ... exec npush' makes $! npush again, so settle's kill still reaches it. rt_files() { _sd=$1; _dd=$2; shift 2 rm -rf "$_dd"; mkdir -p "$_dd" sh -c "cd '$_sd' && exec npush $*" > "$D/rf.push.out" 2> "$D/rf.push.err" & _rp=$! listener_up "$D/rf.push.err" || true _i=0; RT_POLL_RC=1 while [ "$_i" -lt "$TRIES" ]; do ( cd "$_dd" && npoll 127.0.0.1 ) > "$D/rf.poll.out" 2> "$D/rf.poll.err"; RT_POLL_RC=$? [ "$RT_POLL_RC" -eq 0 ] && break _i=$((_i+1)); sleep 1 done settle "$_rp" } # delivered -- the byte-for-byte verdict this suite # exists for, kept apart from the rc cases: a transfer that exits 0 having moved # nothing is precisely what npush(1) BUGS warns about, and no status check can # see it. delivered() { if [ ! -e "$3" ]; then bst_notok "$1" "no output at $3 -- $(_bst_1line "$4")" elif cmp -s "$2" "$3"; then bst_ok "$1" else bst_notok "$1" "$(wc -c < "$2" 2>/dev/null | tr -d ' ') bytes in, $(wc -c < "$3" 2>/dev/null | tr -d ' ') out -- $(_bst_1line "$4")"; fi } # --- 1. stdin/stdout mode: ncp's own transfer, no tar on either side ---------- # With no filenames npush writes a leading NUL and then stdin; npoll reads that # byte, recognises the mode and copies the rest to stdout. rt_stdin "$D/txt.src" "$D/dst/txt.out" 127.0.0.1 # npush's contract is a TCP listener on 8002; assert it was actually observed # rather than inferred from the transfer having worked. check_eq ncpnet/push-listener-observed yes "$LISTENER_SEEN" check_eq ncpnet/rt-stdin-push-rc 0 "$RT_PUSH_RC" check_eq ncpnet/rt-stdin-poll-rc 0 "$RT_POLL_RC" delivered ncpnet/rt-stdin-text "$D/txt.src" "$D/dst/txt.out" "$(cat "$D/rt.push.err" "$D/rt.poll.err" 2>/dev/null)" # npush prints its announcement diagnostics on the FIRST loop iteration, before # any peer arrives, so the transfer above has already captured them -- which is # why they are read from its stderr rather than from an npush nothing connects # to. With no CONFIG_IPV6 (brintos/linux#54) libowfat's socket_udp6() falls back # to AF_INET and latches its `noipv6' global, after which socket_send6() sets # EPROTONOSUPPORT ITSELF for any address that is neither v4-mapped nor ::1 -- and # ncp's group is ff02::...:6e63:7030. Deterministic, so it is an `ok' pinning the # failure MODE, not a skip: it reds the day the fallback changes. push_diag=$(cat "$D/rt.push.err" 2>/dev/null) say ncpnet/mc-v6-refused-in-library "IPv6 multicast sendmsg failed: Protocol not supported" "$push_diag" rt_stdin "$D/bin.src" "$D/dst/bin.out" 127.0.0.1 delivered ncpnet/rt-stdin-binary "$D/bin.src" "$D/dst/bin.out" "$(cat "$D/rt.push.err" "$D/rt.poll.err" 2>/dev/null)" rt_stdin "$D/txt.src" "$D/dst/v6.out" ::1 delivered ncpnet/rt-v6-loopback-literal "$D/txt.src" "$D/dst/v6.out" "$(cat "$D/rt.push.err" "$D/rt.poll.err" 2>/dev/null)" rt_stdin "$D/txt.src" "$D/dst/v4m.out" ::ffff:127.0.0.1 delivered ncpnet/rt-v4mapped-literal "$D/txt.src" "$D/dst/v4m.out" "$(cat "$D/rt.push.err" "$D/rt.poll.err" 2>/dev/null)" # `npush -b' forces the IPv4 broadcast arm on as well (npush(1): "Use the -b # option to npush to make sure that clients before version 1.0 can see the # advertisements"). Run as a real transfer so it ends by itself; the flag's # observable effect on this machine is the third diagnostic line. rt_stdin_b() { rm -f "$2" # BST-REDIRECT-OK: background command, and $1 is under $TMPDIR, never the rootfs. npush -b < "$1" > "$D/rt.push.out" 2> "$D/rt.push.err" & _rp=$! listener_up "$D/rt.push.err" || true _i=0; RT_POLL_RC=1 while [ "$_i" -lt "$TRIES" ]; do npoll 127.0.0.1 > "$2" 2> "$D/rt.poll.err"; RT_POLL_RC=$? [ "$RT_POLL_RC" -eq 0 ] && break _i=$((_i+1)); sleep 1 done settle "$_rp" } rt_stdin_b "$D/txt.src" "$D/dst/bc.out" delivered ncpnet/rt-broadcast-flag "$D/txt.src" "$D/dst/bc.out" "$(cat "$D/rt.push.err" "$D/rt.poll.err" 2>/dev/null)" # The flag's observable effect is a DIFFERENTIAL, not a line: without -b, # v4bcsock() is only reached when both multicast sockets failed (they do not # here), so nothing ever mentions the broadcast arm; with -b it is opened # unconditionally and its send to 255.255.255.255 has nowhere to go. Asserting # both directions is what makes this a test of the flag rather than of npush. bc_diag=$(cat "$D/rt.push.err" 2>/dev/null) say ncpnet/bc-flag-opens-broadcast-arm "IPv4 broadcast" "$bc_diag" nosay ncpnet/bc-absent-without-flag "IPv4 broadcast" "$push_diag" # --- 2. file mode: tar cpvvf on the sender, tar xvvpf on the receiver --------- rt_files "$D/src" "$D/fdst" a.txt sub/b.txt check_eq ncpnet/rt-files-push-rc 0 "$RT_PUSH_RC" check_eq ncpnet/rt-files-poll-rc 0 "$RT_POLL_RC" delivered ncpnet/rt-files-a "$D/src/a.txt" "$D/fdst/a.txt" "$(cat "$D/rf.push.err" "$D/rf.poll.err" 2>/dev/null)" delivered ncpnet/rt-files-sub "$D/src/sub/b.txt" "$D/fdst/sub/b.txt" "$(cat "$D/rf.push.err" "$D/rf.poll.err" 2>/dev/null)" # --- 3. the documented missing-file behaviour --------------------------------- rt_files "$D/src" "$D/mdst" a.txt no-such-file.txt check_eq ncpnet/rt-missing-push-rc 2 "$RT_PUSH_RC" check_eq ncpnet/rt-missing-poll-rc 0 "$RT_POLL_RC" mf_push=$(cat "$D/rf.push.err" 2>/dev/null) mf_peer=$(cat "$D/rf.poll.err" "$D/rf.poll.out" 2>/dev/null) say ncpnet/rt-missing-sender-names-file "no-such-file.txt" "$mf_push" say ncpnet/rt-missing-sender-cannot-stat "Cannot stat" "$mf_push" nosay ncpnet/rt-missing-peer-silent "no-such-file" "$mf_peer" nosay ncpnet/rt-missing-peer-no-stat-error "Cannot stat" "$mf_peer" delivered ncpnet/rt-missing-partial-delivered "$D/src/a.txt" "$D/mdst/a.txt" "$(_bst_1line "$mf_push")" # --- 4. ncp's own two modes: server (no args) and client (host + files) ------- # The third dispatch arm, and the one ncp(1) documents. The server execs # `tar xvvpf -' on the accepted connection; the client execs `tar cpvvf -' after # skipping the hostname argument. Server mode serves exactly ONE connection and # then IS tar (execvp replaces the process), so the server is restarted inside # the retry loop -- a second client attempt against a server already consumed # would red for the wrong reason. # srv_ready -- poll for ncp's own `server mode.' line, which is the # first statement in ncpserver(). A fixed sleep is not a readiness signal: on a # guest this late in a run an exec can take seconds to stage, and a client that # runs before the server exists reports `Connection refused' -- a red whose cause # is the harness rather than the port. srv_ready() { _s=0 while [ "$_s" -lt 15 ]; do grep -q 'server mode' "$1" 2>/dev/null && return 0 _s=$((_s+1)); sleep 1 done return 1 } i=0; nrc=1; nsrv="" while [ "$i" -lt 3 ]; do rm -rf "$D/ndst"; mkdir -p "$D/ndst" : > "$D/n.srv.err" ( cd "$D/ndst" && exec ncp ) > "$D/n.srv.out" 2> "$D/n.srv.err" & npid=$! srv_ready "$D/n.srv.err" || sleep 1 ( cd "$D/src" && ncp 127.0.0.1 a.txt ) > "$D/n.cli.out" 2> "$D/n.cli.err"; nrc=$? if [ "$nrc" -eq 0 ]; then wait "$npid" 2>/dev/null else kill -TERM "$npid" 2>/dev/null; wait "$npid" 2>/dev/null; fi nsrv=$(cat "$D/n.srv.err" 2>/dev/null) [ "$nrc" -eq 0 ] && break i=$((i+1)); sleep 1 done sleep 1 check_eq ncpnet/server-client-rc 0 "$nrc" say ncpnet/server-mode-announces "server mode." "$nsrv" # An empty $nsrv means ncp never reached ncpserver()'s first statement, so the # red above is an exec that did not stage rather than a server that misbehaved. # Say which, at the case, instead of leaving a reader to re-derive it. [ -n "$nsrv" ] || bst_notok ncpnet/server-exec-staged "ncp printed nothing at all, so the process never reached ncpserver() -- an exec that did not stage, not a server fault ($BST_UNRUN_TAG class, $BST_UNRUN_ISSUE)" [ -n "$nsrv" ] && bst_ok ncpnet/server-exec-staged delivered ncpnet/server-client-delivered "$D/src/a.txt" "$D/ndst/a.txt" "$(cat "$D/n.srv.err" "$D/n.cli.err" 2>/dev/null)" # A client given a host and NO files is the other half of the same BUGS # sentence: tar is exec'd anyway and refuses, so the status is tar's 2. i=0; erc=1; eout="" while [ "$i" -lt 3 ]; do rm -rf "$D/edst"; mkdir -p "$D/edst" : > "$D/e.srv.err" ( cd "$D/edst" && exec ncp ) > "$D/e.srv.out" 2> "$D/e.srv.err" & epid=$! srv_ready "$D/e.srv.err" || sleep 1 eout=$(ncp 127.0.0.1 2>&1); erc=$? kill -TERM "$epid" 2>/dev/null; wait "$epid" 2>/dev/null [ "$erc" -ne 111 ] && break i=$((i+1)); sleep 1 done sleep 1 check_eq ncpnet/client-no-files-rc 2 "$erc" say ncpnet/client-no-files-msg "empty archive" "$eout" # --- 5. the UDP rendezvous: attempted for real, then probed ------------------ # npush announcing, npoll listening for the announcement with no hostname given. # Both are backgrounded and killed, because npoll's no-hostname mode blocks in # socket_recv6() forever when nothing arrives and there is no working bound on # this image to wrap it in. rm -f "$D/dst/mc.out" # BST-REDIRECT-OK: background command; the source is under $TMPDIR, not the rootfs. npush < "$D/bin.src" > "$D/mc.push.out" 2> "$D/mc.push.err" & mpid=$! sleep 1 npoll > "$D/dst/mc.out" 2> "$D/mc.poll.err" & qpid=$! w=0 while [ "$w" -lt 12 ]; do cmp -s "$D/bin.src" "$D/dst/mc.out" && break w=$((w+1)); sleep 1 done kill -TERM "$qpid" 2>/dev/null; kill -TERM "$mpid" 2>/dev/null wait "$qpid" 2>/dev/null; wait "$mpid" 2>/dev/null sleep 1 # The evidence a skip has to rest on, gathered from the machine itself. With no # CONFIG_IP_MULTICAST (brintos/linux#111) the IPv4 group cannot be joined and, # with only `lo' up, a datagram to 224.110.99.112 or 255.255.255.255 has no # route. These two cases are asserted rather than assumed, and both turn red the # day this kernel grows a multicast-capable interface -- which is exactly when # the rendezvous stops being legitimately skippable. mc_ifs=$(ip -o link show 2>/dev/null | grep -c MULTICAST) [ -n "$mc_ifs" ] || mc_ifs=0 mc_igmp=no; [ -r /proc/net/igmp ] && mc_igmp=yes lo_flags=$(ip -o link show lo 2>/dev/null | sed -n 's/.*<\([^>]*\)>.*/\1/p') say ncpnet/mc-probe-lo-up "UP" "$lo_flags" nosay ncpnet/mc-probe-lo-not-multicast "MULTICAST" "$lo_flags" mc_evidence="multicast-capable-ifs=$mc_ifs /proc/net/igmp=$mc_igmp lo=<$lo_flags>; npush: $(_bst_1line "$(cat "$D/mc.push.err" 2>/dev/null)")" if cmp -s "$D/bin.src" "$D/dst/mc.out"; then bst_ok ncpnet/multicast-rendezvous elif [ "$mc_ifs" -eq 0 ]; then bst_skip ncpnet/multicast-rendezvous "brintos/linux#111 (CONFIG_IP_MULTICAST unset) + #54 (CONFIG_IPV6 unset): no MULTICAST-capable interface on this image, so there is no group to announce to or join -- $mc_evidence" else bst_notok ncpnet/multicast-rendezvous "$mc_ifs interface(s) DO carry MULTICAST, so the absence is not confirmed -- $mc_evidence; poll: $(_bst_1line "$(cat "$D/mc.poll.err" 2>/dev/null)")" fi rm -rf "$D"