. "$BST_LIB" # BST-TIMEOUT: 900 # uftp 5.0.3 -- encrypted UDP file transfer with multicast, from meta-networking # recipes-support. Four programs: uftp (the sender/server), uftpd (the receiving # client), uftpproxyd (the proxy) and uftp_keymgt (the key store). # # THE PROTOCOL RUNS FOR REAL, ON ONE HOST, ENCRYPTED. uftp -M makes the # "public" address the announce is sent to; when that address is NOT multicast, # server_init.c sets receive_dest = listen_dest and the whole exchange is plain # unicast. uftpd started with no -M joins no group and simply binds the port. So # `uftp -M 127.0.0.1 -H ` against a local `uftpd` is a complete # ANNOUNCE -> REGISTER -> keyexchange -> FILEINFO -> COMPLETE -> DONE run with # the default AES256-GCM keytype, no network and no second host. # # uftpd NEEDS -I. client_init.c collects only NON-loopback interfaces into # m_interface; this guest has only `lo`, so interface_count stays 0 and uftpd # falls back to gethostname() + getaddrinfo(). The hostname here is the literal # "(none)" and /etc/hosts has no entry for it, so that lookup fails and uftpd # exits before binding. `-I lo` populates m_interface directly and the fallback # is never reached. That is asserted in its own right below (uftp/uftpd-needs-if) # rather than left as a comment, because it is the single thing that makes the # difference between a working receiver and no receiver at all. # # WHAT CANNOT COMPLETE HERE, AND WHY IT IS A SKIP RATHER THAN A RED. # # 1. The BULK DATA phase of a regular file. server_phase.c's transfer_phase() # creates a sender thread with pthread_create() and returns ERR_DROPPED if # that fails. On this image the hardwarejs Worker refuses a thread-mode # clone at the FIRST thread -- the coro substrate is single-lane per # process and our images are not lane-stamped -- so pthread_create() gets # EAGAIN and uftp reports "Failed to create sender thread: Resource # temporarily unavailable", exit 9. Note this is NOT the six-thread ceiling # of brintos/linux#68: the ceiling here is zero. The refusal is observed by # uftp/probe-no-threads below, and the skip rests on that probe. # # 2. MULTICAST DELIVERY. IP_ADD_MEMBERSHIP is accepted -- uftpd -M 230.4.4.1 # joins and stays up, and multicast_join() exits ERR_SOCKET when it does # not -- but nothing sent to that group is ever received: the server sends # its full run of ANNOUNCEs and times out. wasm32_defconfig sets no # CONFIG_IP_MULTICAST (the same missing line brintos/linux#111 reports for # the routing API). The UNICAST CONTROL is what makes this a finding rather # than a guess: the same uftpd process, in the same run, completes a # unicast transaction immediately after the multicast one times out. # # EVERY OTHER FILE TYPE DELIVERS END TO END, because transfer_phase() returns # ERR_NONE before it ever creates the thread when every client is already # DEST_DONE -- which is what the client's early_complete() at FILEINFO produces # for a symlink, a directory, an @DELETE or @FREESPACE metafile, and for a # sync-mode file the receiver already has. Those are real, complete, encrypted # UFTP transactions and they are scored as ordinary passes. # # NO uftpd IS STARTED WITH -T. handle_fileinfo() guards move_to_backup() with # `if (!tempfile)`, so a temp-dir receiver does not touch the destination at # FILEINFO and the @DELETE metafile becomes a no-op that still answers COMPLETE. # That is upstream behaviour, not a wasm32 property; the suite avoids it. # # NOTHING HERE USES timeout(1): coreutils arms its bound with alarm(2), which is # unwired on wasm32 (brintos/linux#77). Every bound below is wall-clock, read # from date(1). require uftp uftp/present W=${TMPDIR:-/tmp}/bst-uftp.$$ rm -rf "$W"; mkdir -p "$W/src" "$W/dest" "$W/keys" || { bst_notok uftp/workdir "cannot create $W"; exit 0; } bst_ok uftp/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 } # rc : exit status must equal ; stderr kept for the detail. rc() { _id=$1; _want=$2; shift 2 _o=$("$@" 2>&1); _r=$? if [ "$_r" -eq "$_want" ]; then bst_ok "$_id" elif bst_execfail "$_o"; then bst_unrun "$_id" "$_o" else bst_notok "$_id" "rc=$_r want=$_want out:'$(_bst_1line "$_o")'"; fi } # rcsay : both the status and the message. rcsay() { _id=$1; _want=$2; _n=$3; shift 3 _o=$("$@" 2>&1); _r=$? case "$_o" in *"$_n"*) _hit=1 ;; *) _hit=0 ;; esac if [ "$_r" -eq "$_want" ] && [ "$_hit" -eq 1 ]; then bst_ok "$_id" elif bst_execfail "$_o"; then bst_unrun "$_id" "$_o" else bst_notok "$_id" "rc=$_r want=$_want needle:'$_n' out:'$(_bst_1line "$_o")'"; fi } # waitup : wall-clock settle, then report whether it is alive. waitup() { _t0=$(date +%s) while [ $(( $(date +%s) - _t0 )) -lt "$2" ]; do sleep 1; done kill -0 "$1" 2>/dev/null } # ---------------------------------------------------------------- presence --- for t in uftpd uftpproxyd uftp_keymgt; do if command -v "$t" >/dev/null 2>&1; then bst_ok "uftp/present-$t" else bst_notok "uftp/present-$t" "$t not installed"; fi done for m in uftp uftpd uftpproxyd uftp_keymgt; do if [ -s "/usr/share/man/man1/$m.1" ]; then bst_ok "uftp/man-$m" else bst_notok "uftp/man-$m" "/usr/share/man/man1/$m.1 missing or empty"; fi done # ------------------------------------------------------- usage and validation --- # Each tool prints its own USAGE to stderr and exits ERR_PARAM (1). The needles # are the first line of the USAGE macro in _config.h, so a usage string # that silently changes shape is caught rather than merely "some output". rcsay uftp/usage-noargs 1 "uftp [ -R txrate ]" uftp rcsay uftp/usage-badopt 1 "uftp [ -R txrate ]" uftp -8 rcsay uftp/usage-uftpd 1 "uftpd [ -d ] [ -p port ]" uftpd -8 rcsay uftp/usage-uftpproxyd 1 "uftpproxyd { -s { dest" uftpproxyd -8 rcsay uftp/proxy-needs-mode 1 "Either -s, -c, or -r must be specified" uftpproxyd rcsay uftp/reject-rate 1 "Invalid rate" uftp -R 0 /dev/null rcsay uftp/reject-txweight 1 "Invalid txweight" uftp -W 1 /dev/null rcsay uftp/reject-nak-pct 1 "Invalid max_nak_pct" uftp -N 101 /dev/null rcsay uftp/reject-restart-sync 1 "restart mode and sync mode together" uftp -f -z -H 127.0.0.1 /dev/null rcsay uftp/reject-listfile 1 "Couldn't open file list" uftp -i "$W/nosuch" -H 127.0.0.1 rcsay uftp/reject-rcvbuf 1 "Invalid buffer size" uftpd -B 1 rcsay uftp/reject-interface 1 "Invalid name/address" uftpd -I nosuchif0 # uftp_keymgt's validation is its own -- it does not share process_args(). rcsay uftp/key-reject-spec 1 "Invalid key specification" uftp_keymgt -g nosuch:1 "$W/keys/x" rcsay uftp/key-reject-size 1 "Invalid key size" uftp_keymgt -g rsa:512 "$W/keys/x" rcsay uftp/key-reject-curve 1 "Invalid curve" uftp_keymgt -g ec:nosuchcurve "$W/keys/x" rcsay uftp/key-reject-g-and-d 1 "Can't specify both -g and -d" uftp_keymgt -g rsa:1024 -d "$W/keys/x" rcsay uftp/key-reject-nofile 1 "No keyfile specified" uftp_keymgt -g rsa:1024 # ------------------------------------------------------------ key management --- # Real OpenSSL key generation, storage and read-back. The fingerprint is the # thing that has to survive the round trip: it is computed from the stored blob, # so a key that generated but did not persist correctly prints a different one. K1=$W/keys/rsa1024 o=$(uftp_keymgt -g rsa:1024 "$K1" 2>&1) say uftp/key-gen-rsa "RSA, 1024 bits, fingerprint:" "$o" FP1=${o##*fingerprint: } if [ -s "$K1" ]; then bst_ok uftp/key-file-written else bst_notok uftp/key-file-written "$K1 is empty or absent"; fi case "$FP1" in ??:??:??:??:??:??:??:??:??:??:??:??:??:??:??:??:??:??:??:??) bst_ok uftp/key-fingerprint-shape ;; *) bst_notok uftp/key-fingerprint-shape "not 20 colon-separated hex octets: '$(_bst_1line "$FP1")'" ;; esac o2=$(uftp_keymgt "$K1" 2>&1) check_eq uftp/key-readback "$FP1" "${o2##*fingerprint: }" K2=$W/keys/ec256 o=$(uftp_keymgt -g ec:prime256v1 "$K2" 2>&1) say uftp/key-gen-ec "ECDSA, curve prime256v1, fingerprint:" "$o" FP2=${o##*fingerprint: } o2=$(uftp_keymgt "$K2" 2>&1) check_eq uftp/key-readback-ec "$FP2" "${o2##*fingerprint: }" if [ "$FP1" != "$FP2" ]; then bst_ok uftp/key-distinct else bst_notok uftp/key-distinct "RSA and EC keys share a fingerprint"; fi K3=$W/keys/rsa2048 o=$(uftp_keymgt -g rsa:2048 "$K3" 2>&1) say uftp/key-gen-rsa2048 "RSA, 2048 bits, fingerprint:" "$o" K4=$W/keys/ec384 o=$(uftp_keymgt -g ec:secp384r1 "$K4" 2>&1) say uftp/key-gen-ec384 "ECDSA, curve secp384r1, fingerprint:" "$o" say uftp/key-no-such-key "no such key" "$(uftp_keymgt "$W/keys/absent" 2>&1)" # ----------------------------------------------------- the receiver, and -I --- # Without -I, uftpd has no non-loopback interface to use and falls through to # gethostname(); this image answers "(none)", which does not resolve. Asserted # here so the -I in every run below is evidence-backed rather than a habit. rcsay uftp/uftpd-needs-if 1 "Can't get address of hostname" uftpd -d -p 41190 -D "$W/dest" uftpd -d -p 41100 -x 2 -I lo -U 0x0A0A0A01 -D "$W/dest" > "$W/recv.log" 2>&1 & RECV=$! if waitup "$RECV" 6; then bst_ok uftp/uftpd-runs else bst_notok uftp/uftpd-runs "uftpd -I lo exited: $(_bst_1line "$(head -c 200 "$W/recv.log")")"; fi # uftpproxyd needs one of -s/-c/-r; -c is client (response) mode and needs no peer. uftpproxyd -c -p 41101 -x 2 -I lo > "$W/proxy.log" 2>&1 & PROXY=$! if waitup "$PROXY" 6; then bst_ok uftp/uftpproxyd-runs else bst_notok uftp/uftpproxyd-runs "uftpproxyd -c exited: $(_bst_1line "$(head -c 200 "$W/proxy.log")")"; fi kill -KILL "$PROXY" 2>/dev/null # --------------------------------------------------- end-to-end transactions --- S="-M 127.0.0.1 -p 41100 -x 1 -I lo -H 0x0A0A0A01 -U 0x0B0B0B01" printf 'uftp-payload-%s\n' "$$" > "$W/src/note" ln -s /etc/hosts "$W/src/link1" mkdir -p "$W/src/subdir" # A symlink is delivered whole at FILEINFO: the receiver calls symlink(2) with # the target the server sent, so the content that crosses is compared exactly. rc uftp/xfer-symlink 0 uftp $S "$W/src/link1" if [ -L "$W/dest/link1" ]; then check_eq uftp/xfer-symlink-target "/etc/hosts" "$(readlink "$W/dest/link1")" else bst_notok uftp/xfer-symlink-target "$W/dest/link1 is not a symlink" fi rc uftp/xfer-directory 0 uftp $S "$W/src/subdir" if [ -d "$W/dest/subdir" ]; then bst_ok uftp/xfer-directory-landed else bst_notok uftp/xfer-directory-landed "$W/dest/subdir was not created"; fi # -D renames on the receiving side. rc uftp/xfer-rename 0 uftp $S -D renamed "$W/src/link1" if [ -L "$W/dest/renamed" ]; then bst_ok uftp/xfer-rename-landed else bst_notok uftp/xfer-rename-landed "$W/dest/renamed absent"; fi # The @DELETE metafile is a command, not a file: the receiver removes the named # path out of its own destination directory and answers COMPLETE. : > "$W/dest/victim" rc uftp/meta-delete 0 uftp $S "@DELETE:victim" if [ -e "$W/dest/victim" ]; then bst_notok uftp/meta-delete-took-effect "victim survived @DELETE" else bst_ok uftp/meta-delete-took-effect; fi rc uftp/meta-freespace 0 uftp $S "@FREESPACE" # Sync mode: the receiver already holds an identical copy (same mtime, same # size), declines it, and the run completes with nothing transferred. cp -p "$W/src/note" "$W/dest/note" rc uftp/sync-skip 0 uftp $S -z "$W/src/note" say uftp/sync-skip-logged "skipping file, in sync" "$(tail -c 2000 "$W/recv.log")" # Sync preview transmits nothing by design, for any file, and must leave the # destination untouched. rm -f "$W/dest/note" rc uftp/sync-preview 0 uftp $S -Z "$W/src/note" if [ -e "$W/dest/note" ]; then bst_notok uftp/sync-preview-no-write "-Z created $W/dest/note" else bst_ok uftp/sync-preview-no-write; fi # ------------------------------------------------- documented exit statuses --- # uftp.h: 1 ERR_PARAM, 7 ERR_NO_REGISTER, 9 ERR_DROPPED, 10 ERR_NO_FILES. rcsay uftp/exit-no-files 10 "No such file or directory" uftp $S "$W/src/absent" rc uftp/exit-no-register 7 uftp -M 127.0.0.1 -p 41109 -x 0 -I lo -H 0x0C0C0C01 -s 10 -r 0.1 -U 0x0B0B0B02 "$W/src/note" # ------------------------------------------------- the thread refusal, probed --- # The bulk-data phase, run for real. It is expected to fail, and the assertion # is on the EXACT failure: exit 9 with pthread_create's EAGAIN in the message. # This is what uftp/xfer-regular-bytes rests on -- without it that skip would be # an assumption. o=$(uftp -M 127.0.0.1 -p 41100 -x 2 -I lo -H 0x0A0A0A01 -U 0x0B0B0B01 "$W/src/note" 2>&1); r=$? if [ "$r" -eq 0 ]; then bst_ok uftp/probe-no-threads-cleared if cmp "$W/src/note" "$W/dest/note" >/dev/null 2>&1; then bst_ok uftp/xfer-regular-bytes else bst_notok uftp/xfer-regular-bytes "transfer reported success but the bytes differ"; fi elif [ "$r" -eq 9 ]; then case "$o" in *"Failed to create sender thread"*) bst_ok uftp/probe-no-threads bst_skip uftp/xfer-regular-bytes "pthread_create returns EAGAIN at the first thread (the coro substrate is single-lane and this image is not lane-stamped), so transfer_phase() cannot start its sender thread -- probed by uftp/probe-no-threads" ;; *) bst_notok uftp/probe-no-threads "exit 9 for some other reason: $(_bst_1line "$o")" ;; esac else bst_notok uftp/probe-no-threads "rc=$r, expected 0 or 9: $(_bst_1line "$o")" fi # The run above still reaches FILEINFO, which is the evidence that everything # ahead of the data phase -- announce, register, key exchange -- works. say uftp/reaches-fileinfo "Sending FILEINFO" "$o" # ------------------------------------------------------ multicast, and control --- kill -KILL "$RECV" 2>/dev/null uftpd -d -p 41102 -x 2 -I lo -M 230.4.4.1 -U 0x0A0A0A02 -D "$W/dest" > "$W/mrecv.log" 2>&1 & MRECV=$! # multicast_join() exits ERR_SOCKET on a refused IP_ADD_MEMBERSHIP, so a uftpd # that is still up has joined the group. if waitup "$MRECV" 6; then bst_ok uftp/mcast-join-accepted else bst_notok uftp/mcast-join-accepted "uftpd -M 230.4.4.1 exited: $(_bst_1line "$(head -c 200 "$W/mrecv.log")")"; fi M="-p 41102 -x 1 -I lo -H 0x0A0A0A02 -U 0x0B0B0B03" mo=$(uftp -M 230.4.4.1 $M -s 10 -r 0.1 "$W/src/link1" 2>&1); mr=$? # The control: same receiver, same file, same run, unicast instead of multicast. uo=$(uftp -M 127.0.0.1 $M "$W/src/link1" 2>&1); ur=$? if [ "$ur" -eq 0 ]; then bst_ok uftp/mcast-unicast-control else bst_notok uftp/mcast-unicast-control "the unicast control did not complete (rc=$ur), so the multicast result below says nothing: $(_bst_1line "$uo")" fi if [ "$mr" -eq 0 ]; then bst_ok uftp/mcast-delivered elif [ "$ur" -ne 0 ]; then bst_notok uftp/mcast-delivered "unicast control also failed -- the receiver, not multicast, is what is broken" else case "$mo" in *"Announce timed out"*) bst_ok uftp/probe-mcast-undelivered bst_skip uftp/mcast-delivered "the group join is accepted but nothing sent to it is delivered -- wasm32_defconfig sets no CONFIG_IP_MULTICAST (see brintos/linux#111); the unicast control on this same receiver completed in the same run" ;; *) bst_notok uftp/mcast-delivered "rc=$mr, not an announce timeout: $(_bst_1line "$mo")" ;; esac fi kill -KILL "$MRECV" 2>/dev/null rm -rf "$W"