diff --git a/examples/portfwd/portfwd.c b/examples/portfwd/portfwd.c index e406368dc..f71b9a1c8 100644 --- a/examples/portfwd/portfwd.c +++ b/examples/portfwd/portfwd.c @@ -432,6 +432,7 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) int ch; int appFdSet = 0; int appFdHalfClosed = 0; + int appEof = 0; int reverse = 0; int fwdFromPortSet = 0; PortfwdState fwdState; @@ -728,8 +729,12 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) appBuffer + appBufferUsed, appBufferSz - appBufferUsed, 0); if (rxd > 0) appBufferUsed += rxd; - else - break; + else { + /* Local end-of-input. Stop polling the socket and leave once + * what is buffered has gone out; leaving now would drop it. */ + appEof = 1; + FD_CLR(appFd, &templateFds); + } } if (FD_ISSET(sshFd, &rxFds)) { word32 channelId = 0; @@ -868,6 +873,11 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) #endif } } + /* Nothing left to hand over, or nothing left to hand it to: without + * the channel check a failed open would leave the buffer forever + * unsendable and the loop with no way out. */ + if (appEof && (appBufferUsed == 0 || fwdChannel == NULL)) + break; } if (reverse) { diff --git a/scripts/fwd-bulk.test b/scripts/fwd-bulk.test new file mode 100755 index 000000000..4004329c9 --- /dev/null +++ b/scripts/fwd-bulk.test @@ -0,0 +1,332 @@ +#!/bin/sh + +# Bulk data through a local direct-tcpip forward. +# +# [nc client] --plain--> :$entry_port [portfwd] +# | +# SSH +# | +# [echoserver] --plain--> :$target_port [nc server] +# +# scripts/fwd.test moves a few hundred bytes, so nothing in the suite +# exercises a forward past its first window. Phase 1 pushes a payload several +# windows long and compares the bytes that come out. Phase 2 ends a transfer +# one window plus a short tail in, the point where the tail is still sitting +# in portfwd's buffer waiting on window credit when the local socket reports +# end-of-input. +# +# The listening nc takes its stdin from a long sleep on purpose. Reading +# end-of-input on stdin makes nc close the connection, which truncates the +# transfer at whatever point the close lands and looks exactly like a +# forwarding stall. + +no_pid=-1 +hold_pid=$no_pid +nc_server_pid=$no_pid +server_pid=$no_pid +portfwd_pid=$no_pid +nc_client_pid=$no_pid +tail_hold_pid=$no_pid +tail_nc_server_pid=$no_pid +tail_server_pid=$no_pid +tail_portfwd_pid=$no_pid +tail_nc_client_pid=$no_pid +work_dir="`pwd`/wolfssh_fwd_bulk$$" +ready_file="$work_dir/ready" +fwd_ready_file="$work_dir/fwd_ready" +payload="$work_dir/payload" +hold_fifo="$work_dir/hold" +received="$work_dir/received" +server_log="$work_dir/server.log" +portfwd_log="$work_dir/portfwd.log" +tail_payload="$work_dir/tail_payload" +tail_received="$work_dir/tail_received" +tail_server_log="$work_dir/tail_server.log" +tail_portfwd_log="$work_dir/tail_portfwd.log" +# Several times the 128K default window, so the transfer cannot finish +# without the window being credited back at least once. The size the +# receiver is held to is read back from the file dd made. +payload_blocks=2000 +payload_size=0 +entry_port=0 +target_port=0 +port=0 +counter=0 +# Phase 2. One default window plus a tail short enough to be read in a single +# pass, so the tail is what portfwd is holding when end-of-input arrives. +# Whether the credit for the window beats the tail is a race, won here about +# half the time and never on some hosts, so the transfer is repeated. A build +# that overrides DEFAULT_WINDOW_SZ just moves bytes and proves nothing. +tail_window_size=131072 +tail_size=2000 +tail_attempts=6 +tail_payload_size=0 +tail_entry_port=0 +tail_target_port=0 +tail_got=0 +# Seconds to wait for the payload. Generous: the point is that a stalled +# forward fails this test instead of hanging make check. +transfer_limit=90 +# Consecutive seconds with no new bytes before calling it stalled. +stall_limit=15 + +[ ! -x "`command -v nc`" ] && echo "nc doesn't exist, skipping" && exit 77 +[ ! -x ./examples/echoserver/echoserver ] \ + && echo "echoserver doesn't exist, skipping" && exit 77 +./examples/echoserver/echoserver '-?' 2>&1 | grep -q "^echoserver " \ + || { echo "echoserver doesn't run, skipping"; exit 77; } +[ ! -x ./examples/portfwd/portfwd ] \ + && echo "portfwd doesn't exist, skipping" && exit 77 +./examples/portfwd/portfwd '-?' 2>&1 | grep -q "does not exist" \ + && { echo "forwarding not compiled in, skipping"; exit 77; } + +# A WOLFSSH_TEST_BLOCK build fails writes at random, which stalls the +# echoserver regardless of what the peer does. The other echoserver scripts +# skip it for the same reason. +WOLFSSH_OPTIONS=`./apps/wolfssh-options` || { + echo "fail: could not run ./apps/wolfssh-options" + exit 1 +} +echo "$WOLFSSH_OPTIONS" | grep -qx "TEST_BLOCK" \ + && { echo "macro WOLFSSH_TEST_BLOCK was used, skipping"; exit 77; } + +do_cleanup() { + for pid in $nc_client_pid $portfwd_pid $server_pid $nc_server_pid \ + $hold_pid $tail_nc_client_pid $tail_portfwd_pid \ + $tail_server_pid $tail_nc_server_pid $tail_hold_pid + do + if [ "$pid" != "$no_pid" ] + then + kill -9 "$pid" 2>/dev/null + wait "$pid" 2>/dev/null + fi + done + rm -rf "$work_dir" +} + +# The failure here is usually just a byte count; the logs name the cause. +do_dump_logs() { + for log in "$server_log" "$portfwd_log" "$tail_server_log" \ + "$tail_portfwd_log" + do + [ -f "$log" ] || continue + echo "--- `basename "$log"` ---" + cat "$log" 2>/dev/null + done +} + +do_fail() { + echo "$1" + do_dump_logs + do_cleanup + exit 1 +} + +do_trap() { + echo "got trap" + do_cleanup + exit 1 +} + +trap do_trap INT TERM + +do_reap_tail() { + for pid in $tail_nc_client_pid $tail_portfwd_pid $tail_server_pid \ + $tail_nc_server_pid $tail_hold_pid + do + if [ "$pid" != "$no_pid" ] + then + kill -9 "$pid" 2>/dev/null + wait "$pid" 2>/dev/null + fi + done + tail_nc_client_pid=$no_pid + tail_portfwd_pid=$no_pid + tail_server_pid=$no_pid + tail_nc_server_pid=$no_pid + tail_hold_pid=$no_pid +} + +# One window-plus-tail transfer through a forward of its own, leaving the +# bytes that arrived in tail_got. Each attempt takes its own ports: the +# previous one's are in TIME_WAIT. +do_tail_attempt() { + tail_entry_port=`expr 16000 + $1 \* 1000 + \( $$ % 1000 \)` + tail_target_port=`expr 22000 + $1 \* 1000 + \( $$ % 1000 \)` + tail_fifo="$work_dir/tail_hold.$1" + tail_ready_file="$work_dir/tail_ready.$1" + tail_fwd_ready_file="$work_dir/tail_fwd_ready.$1" + tail_got=0 + rm -f "$tail_received" + + mkfifo "$tail_fifo" || do_fail "couldn't make the fifo" + sleep 300 > "$tail_fifo" 2>/dev/null & + tail_hold_pid=$! + nc -l $tail_target_port < "$tail_fifo" > "$tail_received" 2>/dev/null & + tail_nc_server_pid=$! + + sleep 0.2 + kill -0 "$tail_nc_server_pid" 2>/dev/null \ + || do_fail "couldn't listen on port $tail_target_port, is it in use?" + + ./examples/echoserver/echoserver -1 -f -R "$tail_ready_file" \ + > "$tail_server_log" 2>&1 & + tail_server_pid=$! + + counter=0 + while [ ! -s "$tail_ready_file" ] && [ "$counter" -lt 20 ]; do + sleep 0.1 + counter=`expr $counter + 1` + done + [ -s "$tail_ready_file" ] \ + || do_fail "no ready file, echoserver didn't start" + + ./examples/portfwd/portfwd -u jill -P upthehill \ + -p `cat "$tail_ready_file"` \ + -f $tail_entry_port -t $tail_target_port -R "$tail_fwd_ready_file" \ + > "$tail_portfwd_log" 2>&1 & + tail_portfwd_pid=$! + + counter=0 + while [ ! -s "$tail_fwd_ready_file" ] && [ "$counter" -lt 20 ]; do + sleep 0.1 + counter=`expr $counter + 1` + done + [ -s "$tail_fwd_ready_file" ] \ + || do_fail "no ready file, portfwd didn't start" + + nc 127.0.0.1 $tail_entry_port < "$tail_payload" > /dev/null 2>&1 & + tail_nc_client_pid=$! + + # A dropped tail leaves the count one tail short of the payload, so stop + # as soon as it stops moving instead of waiting the whole limit out. + counter=0 + last=-1 + while [ "$tail_got" -lt "$tail_payload_size" ] && [ "$counter" -lt 24 ] + do + sleep 0.5 + counter=`expr $counter + 1` + tail_got=`wc -c < "$tail_received" 2>/dev/null | tr -d ' '` + [ -z "$tail_got" ] && tail_got=0 + [ "$tail_got" = "$last" ] && [ "$counter" -gt 4 ] && break + last=$tail_got + done + + do_reap_tail +} + +mkdir -p "$work_dir" || { echo "couldn't make the work directory"; exit 1; } + +# portfwd and nc bind what they are told, so the plaintext ports have to be +# picked here. Stamped with the pid so concurrent runs of the suite do not +# collide, kept below the ephemeral range so an outgoing connection cannot +# take one first, and clear of the ports fwd.test.expect hardcodes. +entry_port=`expr 14000 + \( $$ % 1000 \)` +target_port=`expr 15000 + \( $$ % 1000 \)` + +dd if=/dev/urandom of="$payload" bs=1000 count=$payload_blocks 2>/dev/null \ + || { echo "couldn't make the payload"; do_cleanup; exit 1; } +payload_size=`wc -c < "$payload" | tr -d ' '` +[ "$payload_size" -gt 0 ] \ + || { echo "couldn't make the payload"; do_cleanup; exit 1; } + +# nc closes the connection when it reads end-of-input on stdin, which +# truncates the transfer. Hold stdin open with a sleep on the far side of a +# fifo, so both ends have a pid this script can reap. +mkfifo "$hold_fifo" || { echo "couldn't make the fifo"; do_cleanup; exit 1; } +sleep 300 > "$hold_fifo" 2>/dev/null & +hold_pid=$! +nc -l $target_port < "$hold_fifo" > "$received" 2>/dev/null & +nc_server_pid=$! + +# A port already in use makes nc exit at once, and the echoserver's connect +# would then be refused. Catch that here, or it reports as a stall. +sleep 0.2 +if ! kill -0 "$nc_server_pid" 2>/dev/null +then + nc_server_pid=$no_pid + echo "couldn't listen on port $target_port, is it in use?" + do_cleanup + exit 1 +fi + +./examples/echoserver/echoserver -1 -f -R "$ready_file" \ + > "$server_log" 2>&1 & +server_pid=$! + +counter=0 +while [ ! -s "$ready_file" ] && [ "$counter" -lt 20 ]; do + sleep 0.1 + counter=`expr $counter + 1` +done +[ -s "$ready_file" ] || do_fail "no ready file, echoserver didn't start" +port=`cat "$ready_file"` + +./examples/portfwd/portfwd -u jill -P upthehill -p "$port" \ + -f $entry_port -t $target_port -R "$fwd_ready_file" \ + > "$portfwd_log" 2>&1 & +portfwd_pid=$! + +counter=0 +while [ ! -s "$fwd_ready_file" ] && [ "$counter" -lt 20 ]; do + sleep 0.1 + counter=`expr $counter + 1` +done +[ -s "$fwd_ready_file" ] || do_fail "no ready file, portfwd didn't start" + +nc 127.0.0.1 $entry_port < "$payload" > /dev/null 2>&1 & +nc_client_pid=$! + +counter=0 +got=0 +last=0 +stalled=0 +while [ "$got" -lt "$payload_size" ] && [ "$counter" -lt "$transfer_limit" ] +do + sleep 1 + counter=`expr $counter + 1` + got=`wc -c < "$received" 2>/dev/null | tr -d ' '` + [ -z "$got" ] && got=0 + # Give up early once the byte count stops moving, so a stalled forward + # reports in seconds instead of burning the whole limit. + if [ "$got" -eq "$last" ] + then + stalled=`expr $stalled + 1` + [ "$stalled" -ge "$stall_limit" ] && break + else + stalled=0 + last=$got + fi +done + +[ "$got" -eq "$payload_size" ] \ + || do_fail "forward stalled: sent $payload_size bytes, received $got" + +cmp -s "$payload" "$received" \ + || do_fail "forwarded data does not match what was sent" + +echo "moved $payload_size bytes through a local forward" + +# --- Phase 2: end the transfer one window in -------------------------------- +dd if=/dev/urandom of="$tail_payload" bs=4096 count=32 2>/dev/null \ + && dd if=/dev/urandom bs=$tail_size count=1 2>/dev/null \ + >> "$tail_payload" \ + || { echo "couldn't make the payload"; do_cleanup; exit 1; } +tail_payload_size=`wc -c < "$tail_payload" | tr -d ' '` +[ "$tail_payload_size" -eq `expr $tail_window_size + $tail_size` ] \ + || { echo "couldn't make the payload"; do_cleanup; exit 1; } + +attempt=0 +while [ "$attempt" -lt "$tail_attempts" ] +do + do_tail_attempt $attempt + [ "$tail_got" -eq "$tail_payload_size" ] \ + || do_fail "dropped the tail: sent $tail_payload_size, got $tail_got" + cmp -s "$tail_payload" "$tail_received" \ + || do_fail "the tail transfer does not match what was sent" + attempt=`expr $attempt + 1` +done + +echo "ended $tail_attempts transfers on a window boundary" +do_cleanup +exit 0 diff --git a/scripts/include.am b/scripts/include.am index d1fa5d8c0..785b7a7cd 100644 --- a/scripts/include.am +++ b/scripts/include.am @@ -17,3 +17,4 @@ dist_noinst_SCRIPTS+= scripts/sshclient.test dist_noinst_SCRIPTS+= scripts/fwd.test EXTRA_DIST += scripts/fwd.test.expect +dist_noinst_SCRIPTS+= scripts/fwd-bulk.test