From 470a68dad895593aefc7b6cfa2c632833309f21c Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Thu, 5 Feb 2026 12:42:33 +0000 Subject: [PATCH 1/2] Add contention testing In theory these tests will find more non-blocking issues. --- .github/workflows/network-contention-test.yml | 218 +++++++++++++++ .../workflows/paramiko-contention-test.yml | 252 ++++++++++++++++++ scripts/scp.test | 33 ++- 3 files changed, 490 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/network-contention-test.yml create mode 100644 .github/workflows/paramiko-contention-test.yml diff --git a/.github/workflows/network-contention-test.yml b/.github/workflows/network-contention-test.yml new file mode 100644 index 000000000..e43183671 --- /dev/null +++ b/.github/workflows/network-contention-test.yml @@ -0,0 +1,218 @@ +name: wolfSSH Network Contention Test + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + create_matrix: + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.json.outputs.versions }} + steps: + - name: Create wolfSSL version matrix + id: json + run: | + current=`curl -s https://api.github.com/repos/wolfssl/wolfssl/releases | grep tag_name | cut -d : -f 2,3 | tr -d \" | tr -d , | tr -d ' ' | head -1` + last=`curl -s https://api.github.com/repos/wolfssl/wolfssl/releases | grep tag_name | cut -d : -f 2,3 | tr -d \" | tr -d , | tr -d ' ' | head -2 | tail -1` + VERSIONS=$(echo "[ \"master\", \"$current\", \"$last\" ]") + echo "wolfSSL versions found: $VERSIONS" + echo "versions=$VERSIONS" >> $GITHUB_OUTPUT + + build_wolfssl: + needs: create_matrix + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + wolfssl: ${{ fromJson(needs.create_matrix.outputs['versions']) }} + name: Build wolfssl + runs-on: ${{ matrix.os }} + timeout-minutes: 4 + steps: + - name: Checking cache for wolfssl + uses: actions/cache@v4 + id: cache-wolfssl + with: + path: build-dir/ + key: wolfssh-contention-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }} + lookup-only: true + + - name: Checkout, build, and install wolfssl + if: steps.cache-wolfssl.outputs.cache-hit != 'true' + uses: wolfSSL/actions-build-autotools-project@v1 + with: + repository: wolfssl/wolfssl + ref: ${{ matrix.wolfssl }} + path: wolfssl + configure: --enable-ssh + check: false + install: true + + test_sftp_contention: + needs: + - build_wolfssl + - create_matrix + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + wolfssl: ${{ fromJson(needs.create_matrix.outputs['versions']) }} + block_prob: [ 30, 50, 70 ] + name: SFTP contention test (prob=${{ matrix.block_prob }}%) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + steps: + - name: Checking cache for wolfssl + uses: actions/cache@v4 + with: + path: build-dir/ + key: wolfssh-contention-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }} + fail-on-cache-miss: true + + - uses: actions/checkout@v4 + with: + path: wolfssh/ + + - name: autogen + working-directory: ./wolfssh/ + run: ./autogen.sh + + - name: configure with TEST_BLOCK + working-directory: ./wolfssh/ + run: | + ./configure --enable-sftp \ + LDFLAGS="-L${{ github.workspace }}/build-dir/lib" \ + CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_TEST_BLOCK -DWOLFSSH_BLOCK_PROB=${{ matrix.block_prob }} -DWOLFSSH_NO_FPKI" + + - name: make + working-directory: ./wolfssh/ + run: make + + - name: Setup network delay with tc/netem + run: | + sudo tc qdisc add dev lo root netem delay 10ms 5ms loss 0.1% + echo "Network delay configured:" + tc qdisc show dev lo + + - name: Run SFTP contention tests + working-directory: ./wolfssh/ + timeout-minutes: 10 + run: ./scripts/sftp.test + + - name: Create large test files + run: | + dd if=/dev/urandom of=/tmp/test_1kb.dat bs=1K count=1 + dd if=/dev/urandom of=/tmp/test_2mb.dat bs=1M count=2 + dd if=/dev/urandom of=/tmp/test_10mb.dat bs=1M count=10 + md5sum /tmp/test_*.dat > /tmp/test_checksums.md5 + echo "Test files created:" + ls -la /tmp/test_*.dat + + - name: Run extended SFTP file transfer tests + working-directory: ./wolfssh/ + timeout-minutes: 10 + run: | + # Start echoserver in non-blocking mode + ./examples/echoserver/echoserver -N -f & + SERVER_PID=$! + sleep 2 + + # Test 1KB file transfer + echo "Testing 1KB file transfer..." + echo "get /tmp/test_1kb.dat /tmp/recv_1kb.dat" | ./examples/sftpclient/wolfsftp -N -h 127.0.0.1 -p 22222 -u jill -P upthehill + if ! cmp -s /tmp/test_1kb.dat /tmp/recv_1kb.dat; then + echo "FAILED: 1KB file integrity check" + kill $SERVER_PID 2>/dev/null || true + exit 1 + fi + echo "1KB file transfer: PASSED" + + # Test 2MB file transfer + echo "Testing 2MB file transfer..." + echo "get /tmp/test_2mb.dat /tmp/recv_2mb.dat" | ./examples/sftpclient/wolfsftp -N -h 127.0.0.1 -p 22222 -u jill -P upthehill + if ! cmp -s /tmp/test_2mb.dat /tmp/recv_2mb.dat; then + echo "FAILED: 2MB file integrity check" + kill $SERVER_PID 2>/dev/null || true + exit 1 + fi + echo "2MB file transfer: PASSED" + + # Test 10MB file transfer + echo "Testing 10MB file transfer..." + echo "get /tmp/test_10mb.dat /tmp/recv_10mb.dat" | ./examples/sftpclient/wolfsftp -N -h 127.0.0.1 -p 22222 -u jill -P upthehill + if ! cmp -s /tmp/test_10mb.dat /tmp/recv_10mb.dat; then + echo "FAILED: 10MB file integrity check" + kill $SERVER_PID 2>/dev/null || true + exit 1 + fi + echo "10MB file transfer: PASSED" + + kill $SERVER_PID 2>/dev/null || true + echo "All extended SFTP tests PASSED" + + - name: Cleanup network delay + if: always() + run: sudo tc qdisc del dev lo root netem || true + + test_scp_contention: + needs: + - build_wolfssl + - create_matrix + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + wolfssl: ${{ fromJson(needs.create_matrix.outputs['versions']) }} + block_prob: [ 30, 50, 70 ] + name: SCP contention test (prob=${{ matrix.block_prob }}%) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + steps: + - name: Checking cache for wolfssl + uses: actions/cache@v4 + with: + path: build-dir/ + key: wolfssh-contention-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }} + fail-on-cache-miss: true + + - uses: actions/checkout@v4 + with: + path: wolfssh/ + + - name: autogen + working-directory: ./wolfssh/ + run: ./autogen.sh + + - name: configure with TEST_BLOCK + working-directory: ./wolfssh/ + run: | + ./configure --enable-scp \ + LDFLAGS="-L${{ github.workspace }}/build-dir/lib" \ + CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_TEST_BLOCK -DWOLFSSH_BLOCK_PROB=${{ matrix.block_prob }} -DWOLFSSH_NO_FPKI" + + - name: make + working-directory: ./wolfssh/ + run: make + + - name: Setup network delay with tc/netem + run: | + sudo tc qdisc add dev lo root netem delay 10ms 5ms loss 0.1% + echo "Network delay configured:" + tc qdisc show dev lo + + - name: Run SCP contention tests + working-directory: ./wolfssh/ + timeout-minutes: 10 + run: ./scripts/scp.test + + - name: Cleanup network delay + if: always() + run: sudo tc qdisc del dev lo root netem || true diff --git a/.github/workflows/paramiko-contention-test.yml b/.github/workflows/paramiko-contention-test.yml new file mode 100644 index 000000000..471642ed9 --- /dev/null +++ b/.github/workflows/paramiko-contention-test.yml @@ -0,0 +1,252 @@ +name: wolfSSH Paramiko Contention Test + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build_wolfssl: + name: Build wolfssl + runs-on: ubuntu-latest + timeout-minutes: 4 + steps: + - name: Checking cache for wolfssl + uses: actions/cache@v4 + id: cache-wolfssl + with: + path: build-dir/ + key: wolfssh-paramiko-contention-wolfssl-ubuntu-latest + lookup-only: true + + - name: Checkout, build, and install wolfssl + if: steps.cache-wolfssl.outputs.cache-hit != 'true' + uses: wolfSSL/actions-build-autotools-project@v1 + with: + repository: wolfssl/wolfssl + ref: master + path: wolfssl + configure: --enable-all + check: false + install: true + + paramiko_contention_test: + needs: build_wolfssl + strategy: + fail-fast: false + matrix: + block_prob: [ 30, 50, 70 ] + name: Paramiko Contention Test (prob=${{ matrix.block_prob }}%) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checking cache for wolfssl + uses: actions/cache@v4 + with: + path: build-dir/ + key: wolfssh-paramiko-contention-wolfssl-ubuntu-latest + fail-on-cache-miss: true + + - uses: actions/checkout@v4 + with: + path: wolfssh/ + + - name: autogen + working-directory: ./wolfssh/ + run: ./autogen.sh + + - name: configure with TEST_BLOCK + working-directory: ./wolfssh/ + run: | + ./configure --enable-all \ + LDFLAGS="-L${{ github.workspace }}/build-dir/lib" \ + CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_TEST_BLOCK -DWOLFSSH_BLOCK_PROB=${{ matrix.block_prob }} -DWOLFSSH_NO_FPKI" + + - name: make + working-directory: ./wolfssh/ + run: make + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y python3-pip openssh-client + python3 -m pip install paramiko + + - name: Create test directories + run: | + mkdir -p /tmp/sftp_upload + mkdir -p /tmp/sftp_download + + - name: Create test files + run: | + dd if=/dev/urandom of=/tmp/sftp_upload/test_1kb.dat bs=1K count=1 + dd if=/dev/urandom of=/tmp/sftp_upload/test_2mb.dat bs=1M count=2 + dd if=/dev/urandom of=/tmp/sftp_upload/test_20mb.dat bs=1M count=20 + echo "Created test files:" + ls -la /tmp/sftp_upload/ + md5sum /tmp/sftp_upload/*.dat + + - name: Configure wolfSSHd + working-directory: ./wolfssh/ + run: | + # Create a minimal sshd_config file + cat > sshd_config.txt << EOF + Port 22222 + HostKey ./keys/server-key.pem + PasswordAuthentication yes + Subsystem sftp internal-sftp + EOF + + # Set proper permissions for keys + chmod 600 ./keys/server-key.pem + + # Print debug info + echo "Contents of sshd_config.txt:" + cat sshd_config.txt + + - name: Setup network delay with tc/netem + run: | + sudo tc qdisc add dev lo root netem delay 10ms 5ms loss 0.1% + echo "Network delay configured:" + tc qdisc show dev lo + + - name: Start wolfSSHd + working-directory: ./wolfssh/ + run: | + # Create a test user with known password + echo "Creating test user..." + sudo useradd -m testuser + echo "testuser:testpassword" | sudo chpasswd + + # Start wolfSSHd with debug output + echo "Starting wolfSSHd with TEST_BLOCK enabled (prob=${{ matrix.block_prob }}%)..." + sudo ./apps/wolfsshd/wolfsshd -f sshd_config.txt -h ./keys/server-key.pem -p 22222 -d & + echo "Started wolfSSHd on port 22222" + sleep 5 # Give the server time to start + + # Check if server is running + if ! nc -z 127.0.0.1 22222; then + echo "Error: wolfSSHd failed to start" + exit 1 + fi + + # Print debug info + echo "wolfSSHd process info:" + ps aux | grep wolfsshd + + - name: Create Paramiko contention test script + run: | + cat > /tmp/paramiko_contention_test.py << 'EOF' + import paramiko + import os + import time + import sys + import hashlib + + def get_file_hash(filepath): + """Calculate MD5 hash of a file.""" + hash_md5 = hashlib.md5() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + + def run_sftp_test(): + # Create SSH client + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Connect to server using password authentication with testuser + print("Connecting to wolfSSHd server...") + max_retries = 3 + for attempt in range(max_retries): + try: + ssh.connect('127.0.0.1', port=22222, username='testuser', + password='testpassword', timeout=60) + break + except Exception as e: + print(f"Connection attempt {attempt + 1} failed: {e}") + if attempt < max_retries - 1: + time.sleep(2) + else: + raise + + # Open SFTP session + print("Opening SFTP session...") + sftp = ssh.open_sftp() + + test_files = [ + ('test_1kb.dat', '1KB'), + ('test_2mb.dat', '2MB'), + ('test_20mb.dat', '20MB'), + ] + + for filename, size_label in test_files: + src_path = f'/tmp/sftp_upload/{filename}' + remote_path = f'/tmp/{filename}' + dst_path = f'/tmp/sftp_download/{filename}' + + # Upload test + print(f"Uploading {size_label} test file...") + start_time = time.time() + sftp.put(src_path, remote_path) + upload_time = time.time() - start_time + print(f"Upload completed in {upload_time:.2f} seconds") + + # Download test + print(f"Downloading {size_label} test file...") + start_time = time.time() + sftp.get(remote_path, dst_path) + download_time = time.time() - start_time + print(f"Download completed in {download_time:.2f} seconds") + + # Verify integrity + src_hash = get_file_hash(src_path) + dst_hash = get_file_hash(dst_path) + if src_hash != dst_hash: + print(f"FAILED: {size_label} file integrity check") + print(f" Source hash: {src_hash}") + print(f" Downloaded hash: {dst_hash}") + sftp.close() + ssh.close() + return False + print(f"{size_label} file integrity check: PASSED") + + # Close connections + sftp.close() + ssh.close() + + print("SFTP session closed") + print("All contention tests PASSED") + return True + + if __name__ == "__main__": + try: + success = run_sftp_test() + sys.exit(0 if success else 1) + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + EOF + + - name: Run Paramiko contention test + timeout-minutes: 10 + run: | + python3 /tmp/paramiko_contention_test.py + + - name: Cleanup network delay + if: always() + run: sudo tc qdisc del dev lo root netem || true + + - name: Stop wolfSSHd + if: always() + run: | + sudo pkill wolfsshd || true diff --git a/scripts/scp.test b/scripts/scp.test index 84e228fab..7080d4b32 100755 --- a/scripts/scp.test +++ b/scripts/scp.test @@ -6,6 +6,7 @@ no_pid=-1 server_pid=$no_pid ready_file=`pwd`/wolfssh_scp_ready$$ counter=0 +nonblockingOnly=0 [ ! -x ./examples/scpclient/wolfscp ] && echo -e "\n\nwolfscp client doesn't exist" && exit 1 @@ -13,9 +14,15 @@ counter=0 ./examples/client/client -h | grep WOLFSSH_TEST_BLOCK if [ $? -eq 0 ] then - echo "macro NO_WOLFSSH_CLIENT was used" - echo "skipping for now" - exit 77 + echo "WOLFSSH_TEST_BLOCK detected - running in non-blocking mode" + nonblockingOnly=1 +fi + +# Set non-blocking flag based on mode +if [ $nonblockingOnly -eq 1 ]; then + NB_FLAG="-N" +else + NB_FLAG="" fi create_port() { @@ -66,10 +73,10 @@ trap do_trap INT TERM [ ! -x ./examples/scpclient/wolfscp ] && echo -e "\n\nClient doesn't exist" && exit 1 echo "Test basic copy from server to local" -./examples/echoserver/echoserver -1 -R $ready_file & +./examples/echoserver/echoserver $NB_FLAG -1 -R $ready_file & server_pid=$! create_port -./examples/scpclient/wolfscp -u jill -P upthehill -p $port -S $PWD/scripts/scp.test:$PWD/scp.test +./examples/scpclient/wolfscp $NB_FLAG -u jill -P upthehill -p $port -S $PWD/scripts/scp.test:$PWD/scp.test RESULT=$? remove_ready_file @@ -82,10 +89,10 @@ else fi echo "Test basic copy from local to server" -./examples/echoserver/echoserver -1 -R $ready_file & +./examples/echoserver/echoserver $NB_FLAG -1 -R $ready_file & server_pid=$! create_port -./examples/scpclient/wolfscp -u jill -P upthehill -p $port -L $PWD/scripts/scp.test:$PWD/scp.test +./examples/scpclient/wolfscp $NB_FLAG -u jill -P upthehill -p $port -L $PWD/scripts/scp.test:$PWD/scp.test RESULT=$? remove_ready_file @@ -99,10 +106,10 @@ fi echo "Test of getting empty file" touch $PWD/scripts/empty -./examples/echoserver/echoserver -1 -R $ready_file & +./examples/echoserver/echoserver $NB_FLAG -1 -R $ready_file & server_pid=$! create_port -./examples/scpclient/wolfscp -u jill -P upthehill -p $port -S $PWD/scripts/empty:$PWD/empty +./examples/scpclient/wolfscp $NB_FLAG -u jill -P upthehill -p $port -S $PWD/scripts/empty:$PWD/empty RESULT=$? remove_ready_file rm -f $PWD/scripts/empty @@ -117,10 +124,10 @@ fi echo "Test of sending empty file" touch $PWD/scripts/empty -./examples/echoserver/echoserver -1 -R $ready_file & +./examples/echoserver/echoserver $NB_FLAG -1 -R $ready_file & server_pid=$! create_port -./examples/scpclient/wolfscp -u jill -P upthehill -p $port -L $PWD/scripts/empty:$PWD/empty +./examples/scpclient/wolfscp $NB_FLAG -u jill -P upthehill -p $port -L $PWD/scripts/empty:$PWD/empty RESULT=$? remove_ready_file rm -f $PWD/scripts/empty @@ -135,10 +142,10 @@ fi echo "Test of sending a file that does not exist" touch $PWD/scripts/empty -./examples/echoserver/echoserver -1 -R $ready_file & +./examples/echoserver/echoserver $NB_FLAG -1 -R $ready_file & server_pid=$! create_port -./examples/scpclient/wolfscp -u jill -P upthehill -p $port -L $PWD/does-not-exist:$PWD/empty +./examples/scpclient/wolfscp $NB_FLAG -u jill -P upthehill -p $port -L $PWD/does-not-exist:$PWD/empty RESULT=$? remove_ready_file rm -f $PWD/scripts/empty From 74c6a7e8ed84aac83b7bec199651ce783431982d Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Thu, 5 Feb 2026 13:01:16 +0000 Subject: [PATCH 2/2] Fix contention tests: only SFTP supports non-blocking mode The test failures revealed that only echoserver (-N flag) and wolfsftp (-N flag) properly support non-blocking I/O handling. wolfscp and wolfsshd do not have the retry logic to handle WS_WANT_READ/WS_WANT_WRITE returns from TEST_BLOCK. Changes: - scp.test: Revert to skip (exit 77) when TEST_BLOCK is detected since wolfscp doesn't support non-blocking mode - network-contention-test.yml: Remove SCP tests, fix SFTP tests to use expect instead of echo pipe (which was causing "fgets error") - Remove paramiko-contention-test.yml: wolfsshd doesn't support TEST_BLOCK (hangs during SSH handshake) The remaining SFTP contention tests properly exercise the non-blocking code paths using echoserver -N and wolfsftp -N. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/network-contention-test.yml | 82 ++---- .../workflows/paramiko-contention-test.yml | 252 ------------------ scripts/scp.test | 36 ++- 3 files changed, 39 insertions(+), 331 deletions(-) delete mode 100644 .github/workflows/paramiko-contention-test.yml diff --git a/.github/workflows/network-contention-test.yml b/.github/workflows/network-contention-test.yml index e43183671..f5c45e3f7 100644 --- a/.github/workflows/network-contention-test.yml +++ b/.github/workflows/network-contention-test.yml @@ -107,6 +107,9 @@ jobs: timeout-minutes: 10 run: ./scripts/sftp.test + - name: Install expect for extended tests + run: sudo apt-get update && sudo apt-get install -y expect + - name: Create large test files run: | dd if=/dev/urandom of=/tmp/test_1kb.dat bs=1K count=1 @@ -125,9 +128,27 @@ jobs: SERVER_PID=$! sleep 2 + # Create expect script for file transfers + cat > /tmp/sftp_test.exp << 'EXPECTEOF' + #!/usr/bin/expect -f + set timeout 120 + set testfile [lindex $argv 0] + set outfile [lindex $argv 1] + + spawn ./examples/sftpclient/wolfsftp -N -h 127.0.0.1 -p 22222 -u jill + expect "Password:" + send "upthehill\r" + expect "wolfSSH sftp>" + send "get $testfile $outfile\r" + expect "wolfSSH sftp>" + send "exit\r" + expect eof + EXPECTEOF + chmod +x /tmp/sftp_test.exp + # Test 1KB file transfer echo "Testing 1KB file transfer..." - echo "get /tmp/test_1kb.dat /tmp/recv_1kb.dat" | ./examples/sftpclient/wolfsftp -N -h 127.0.0.1 -p 22222 -u jill -P upthehill + /tmp/sftp_test.exp /tmp/test_1kb.dat /tmp/recv_1kb.dat if ! cmp -s /tmp/test_1kb.dat /tmp/recv_1kb.dat; then echo "FAILED: 1KB file integrity check" kill $SERVER_PID 2>/dev/null || true @@ -137,7 +158,7 @@ jobs: # Test 2MB file transfer echo "Testing 2MB file transfer..." - echo "get /tmp/test_2mb.dat /tmp/recv_2mb.dat" | ./examples/sftpclient/wolfsftp -N -h 127.0.0.1 -p 22222 -u jill -P upthehill + /tmp/sftp_test.exp /tmp/test_2mb.dat /tmp/recv_2mb.dat if ! cmp -s /tmp/test_2mb.dat /tmp/recv_2mb.dat; then echo "FAILED: 2MB file integrity check" kill $SERVER_PID 2>/dev/null || true @@ -147,7 +168,7 @@ jobs: # Test 10MB file transfer echo "Testing 10MB file transfer..." - echo "get /tmp/test_10mb.dat /tmp/recv_10mb.dat" | ./examples/sftpclient/wolfsftp -N -h 127.0.0.1 -p 22222 -u jill -P upthehill + /tmp/sftp_test.exp /tmp/test_10mb.dat /tmp/recv_10mb.dat if ! cmp -s /tmp/test_10mb.dat /tmp/recv_10mb.dat; then echo "FAILED: 10MB file integrity check" kill $SERVER_PID 2>/dev/null || true @@ -161,58 +182,3 @@ jobs: - name: Cleanup network delay if: always() run: sudo tc qdisc del dev lo root netem || true - - test_scp_contention: - needs: - - build_wolfssl - - create_matrix - strategy: - fail-fast: false - matrix: - os: [ ubuntu-latest ] - wolfssl: ${{ fromJson(needs.create_matrix.outputs['versions']) }} - block_prob: [ 30, 50, 70 ] - name: SCP contention test (prob=${{ matrix.block_prob }}%) - runs-on: ${{ matrix.os }} - timeout-minutes: 15 - steps: - - name: Checking cache for wolfssl - uses: actions/cache@v4 - with: - path: build-dir/ - key: wolfssh-contention-wolfssl-${{ matrix.wolfssl }}-${{ matrix.os }} - fail-on-cache-miss: true - - - uses: actions/checkout@v4 - with: - path: wolfssh/ - - - name: autogen - working-directory: ./wolfssh/ - run: ./autogen.sh - - - name: configure with TEST_BLOCK - working-directory: ./wolfssh/ - run: | - ./configure --enable-scp \ - LDFLAGS="-L${{ github.workspace }}/build-dir/lib" \ - CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_TEST_BLOCK -DWOLFSSH_BLOCK_PROB=${{ matrix.block_prob }} -DWOLFSSH_NO_FPKI" - - - name: make - working-directory: ./wolfssh/ - run: make - - - name: Setup network delay with tc/netem - run: | - sudo tc qdisc add dev lo root netem delay 10ms 5ms loss 0.1% - echo "Network delay configured:" - tc qdisc show dev lo - - - name: Run SCP contention tests - working-directory: ./wolfssh/ - timeout-minutes: 10 - run: ./scripts/scp.test - - - name: Cleanup network delay - if: always() - run: sudo tc qdisc del dev lo root netem || true diff --git a/.github/workflows/paramiko-contention-test.yml b/.github/workflows/paramiko-contention-test.yml deleted file mode 100644 index 471642ed9..000000000 --- a/.github/workflows/paramiko-contention-test.yml +++ /dev/null @@ -1,252 +0,0 @@ -name: wolfSSH Paramiko Contention Test - -on: - push: - branches: [ 'master', 'main', 'release/**' ] - pull_request: - branches: [ '*' ] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build_wolfssl: - name: Build wolfssl - runs-on: ubuntu-latest - timeout-minutes: 4 - steps: - - name: Checking cache for wolfssl - uses: actions/cache@v4 - id: cache-wolfssl - with: - path: build-dir/ - key: wolfssh-paramiko-contention-wolfssl-ubuntu-latest - lookup-only: true - - - name: Checkout, build, and install wolfssl - if: steps.cache-wolfssl.outputs.cache-hit != 'true' - uses: wolfSSL/actions-build-autotools-project@v1 - with: - repository: wolfssl/wolfssl - ref: master - path: wolfssl - configure: --enable-all - check: false - install: true - - paramiko_contention_test: - needs: build_wolfssl - strategy: - fail-fast: false - matrix: - block_prob: [ 30, 50, 70 ] - name: Paramiko Contention Test (prob=${{ matrix.block_prob }}%) - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checking cache for wolfssl - uses: actions/cache@v4 - with: - path: build-dir/ - key: wolfssh-paramiko-contention-wolfssl-ubuntu-latest - fail-on-cache-miss: true - - - uses: actions/checkout@v4 - with: - path: wolfssh/ - - - name: autogen - working-directory: ./wolfssh/ - run: ./autogen.sh - - - name: configure with TEST_BLOCK - working-directory: ./wolfssh/ - run: | - ./configure --enable-all \ - LDFLAGS="-L${{ github.workspace }}/build-dir/lib" \ - CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_TEST_BLOCK -DWOLFSSH_BLOCK_PROB=${{ matrix.block_prob }} -DWOLFSSH_NO_FPKI" - - - name: make - working-directory: ./wolfssh/ - run: make - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y python3-pip openssh-client - python3 -m pip install paramiko - - - name: Create test directories - run: | - mkdir -p /tmp/sftp_upload - mkdir -p /tmp/sftp_download - - - name: Create test files - run: | - dd if=/dev/urandom of=/tmp/sftp_upload/test_1kb.dat bs=1K count=1 - dd if=/dev/urandom of=/tmp/sftp_upload/test_2mb.dat bs=1M count=2 - dd if=/dev/urandom of=/tmp/sftp_upload/test_20mb.dat bs=1M count=20 - echo "Created test files:" - ls -la /tmp/sftp_upload/ - md5sum /tmp/sftp_upload/*.dat - - - name: Configure wolfSSHd - working-directory: ./wolfssh/ - run: | - # Create a minimal sshd_config file - cat > sshd_config.txt << EOF - Port 22222 - HostKey ./keys/server-key.pem - PasswordAuthentication yes - Subsystem sftp internal-sftp - EOF - - # Set proper permissions for keys - chmod 600 ./keys/server-key.pem - - # Print debug info - echo "Contents of sshd_config.txt:" - cat sshd_config.txt - - - name: Setup network delay with tc/netem - run: | - sudo tc qdisc add dev lo root netem delay 10ms 5ms loss 0.1% - echo "Network delay configured:" - tc qdisc show dev lo - - - name: Start wolfSSHd - working-directory: ./wolfssh/ - run: | - # Create a test user with known password - echo "Creating test user..." - sudo useradd -m testuser - echo "testuser:testpassword" | sudo chpasswd - - # Start wolfSSHd with debug output - echo "Starting wolfSSHd with TEST_BLOCK enabled (prob=${{ matrix.block_prob }}%)..." - sudo ./apps/wolfsshd/wolfsshd -f sshd_config.txt -h ./keys/server-key.pem -p 22222 -d & - echo "Started wolfSSHd on port 22222" - sleep 5 # Give the server time to start - - # Check if server is running - if ! nc -z 127.0.0.1 22222; then - echo "Error: wolfSSHd failed to start" - exit 1 - fi - - # Print debug info - echo "wolfSSHd process info:" - ps aux | grep wolfsshd - - - name: Create Paramiko contention test script - run: | - cat > /tmp/paramiko_contention_test.py << 'EOF' - import paramiko - import os - import time - import sys - import hashlib - - def get_file_hash(filepath): - """Calculate MD5 hash of a file.""" - hash_md5 = hashlib.md5() - with open(filepath, "rb") as f: - for chunk in iter(lambda: f.read(4096), b""): - hash_md5.update(chunk) - return hash_md5.hexdigest() - - def run_sftp_test(): - # Create SSH client - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - # Connect to server using password authentication with testuser - print("Connecting to wolfSSHd server...") - max_retries = 3 - for attempt in range(max_retries): - try: - ssh.connect('127.0.0.1', port=22222, username='testuser', - password='testpassword', timeout=60) - break - except Exception as e: - print(f"Connection attempt {attempt + 1} failed: {e}") - if attempt < max_retries - 1: - time.sleep(2) - else: - raise - - # Open SFTP session - print("Opening SFTP session...") - sftp = ssh.open_sftp() - - test_files = [ - ('test_1kb.dat', '1KB'), - ('test_2mb.dat', '2MB'), - ('test_20mb.dat', '20MB'), - ] - - for filename, size_label in test_files: - src_path = f'/tmp/sftp_upload/{filename}' - remote_path = f'/tmp/{filename}' - dst_path = f'/tmp/sftp_download/{filename}' - - # Upload test - print(f"Uploading {size_label} test file...") - start_time = time.time() - sftp.put(src_path, remote_path) - upload_time = time.time() - start_time - print(f"Upload completed in {upload_time:.2f} seconds") - - # Download test - print(f"Downloading {size_label} test file...") - start_time = time.time() - sftp.get(remote_path, dst_path) - download_time = time.time() - start_time - print(f"Download completed in {download_time:.2f} seconds") - - # Verify integrity - src_hash = get_file_hash(src_path) - dst_hash = get_file_hash(dst_path) - if src_hash != dst_hash: - print(f"FAILED: {size_label} file integrity check") - print(f" Source hash: {src_hash}") - print(f" Downloaded hash: {dst_hash}") - sftp.close() - ssh.close() - return False - print(f"{size_label} file integrity check: PASSED") - - # Close connections - sftp.close() - ssh.close() - - print("SFTP session closed") - print("All contention tests PASSED") - return True - - if __name__ == "__main__": - try: - success = run_sftp_test() - sys.exit(0 if success else 1) - except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() - sys.exit(1) - EOF - - - name: Run Paramiko contention test - timeout-minutes: 10 - run: | - python3 /tmp/paramiko_contention_test.py - - - name: Cleanup network delay - if: always() - run: sudo tc qdisc del dev lo root netem || true - - - name: Stop wolfSSHd - if: always() - run: | - sudo pkill wolfsshd || true diff --git a/scripts/scp.test b/scripts/scp.test index 7080d4b32..5af3c173c 100755 --- a/scripts/scp.test +++ b/scripts/scp.test @@ -6,23 +6,17 @@ no_pid=-1 server_pid=$no_pid ready_file=`pwd`/wolfssh_scp_ready$$ counter=0 -nonblockingOnly=0 [ ! -x ./examples/scpclient/wolfscp ] && echo -e "\n\nwolfscp client doesn't exist" && exit 1 -# test for nonblocking only +# test for nonblocking only - wolfscp does not support -N flag for non-blocking +# mode, so we must skip when TEST_BLOCK is enabled ./examples/client/client -h | grep WOLFSSH_TEST_BLOCK if [ $? -eq 0 ] then - echo "WOLFSSH_TEST_BLOCK detected - running in non-blocking mode" - nonblockingOnly=1 -fi - -# Set non-blocking flag based on mode -if [ $nonblockingOnly -eq 1 ]; then - NB_FLAG="-N" -else - NB_FLAG="" + echo "WOLFSSH_TEST_BLOCK detected" + echo "wolfscp client does not support non-blocking mode, skipping test" + exit 77 fi create_port() { @@ -73,10 +67,10 @@ trap do_trap INT TERM [ ! -x ./examples/scpclient/wolfscp ] && echo -e "\n\nClient doesn't exist" && exit 1 echo "Test basic copy from server to local" -./examples/echoserver/echoserver $NB_FLAG -1 -R $ready_file & +./examples/echoserver/echoserver -1 -R $ready_file & server_pid=$! create_port -./examples/scpclient/wolfscp $NB_FLAG -u jill -P upthehill -p $port -S $PWD/scripts/scp.test:$PWD/scp.test +./examples/scpclient/wolfscp -u jill -P upthehill -p $port -S $PWD/scripts/scp.test:$PWD/scp.test RESULT=$? remove_ready_file @@ -89,10 +83,10 @@ else fi echo "Test basic copy from local to server" -./examples/echoserver/echoserver $NB_FLAG -1 -R $ready_file & +./examples/echoserver/echoserver -1 -R $ready_file & server_pid=$! create_port -./examples/scpclient/wolfscp $NB_FLAG -u jill -P upthehill -p $port -L $PWD/scripts/scp.test:$PWD/scp.test +./examples/scpclient/wolfscp -u jill -P upthehill -p $port -L $PWD/scripts/scp.test:$PWD/scp.test RESULT=$? remove_ready_file @@ -106,10 +100,10 @@ fi echo "Test of getting empty file" touch $PWD/scripts/empty -./examples/echoserver/echoserver $NB_FLAG -1 -R $ready_file & +./examples/echoserver/echoserver -1 -R $ready_file & server_pid=$! create_port -./examples/scpclient/wolfscp $NB_FLAG -u jill -P upthehill -p $port -S $PWD/scripts/empty:$PWD/empty +./examples/scpclient/wolfscp -u jill -P upthehill -p $port -S $PWD/scripts/empty:$PWD/empty RESULT=$? remove_ready_file rm -f $PWD/scripts/empty @@ -124,10 +118,10 @@ fi echo "Test of sending empty file" touch $PWD/scripts/empty -./examples/echoserver/echoserver $NB_FLAG -1 -R $ready_file & +./examples/echoserver/echoserver -1 -R $ready_file & server_pid=$! create_port -./examples/scpclient/wolfscp $NB_FLAG -u jill -P upthehill -p $port -L $PWD/scripts/empty:$PWD/empty +./examples/scpclient/wolfscp -u jill -P upthehill -p $port -L $PWD/scripts/empty:$PWD/empty RESULT=$? remove_ready_file rm -f $PWD/scripts/empty @@ -142,10 +136,10 @@ fi echo "Test of sending a file that does not exist" touch $PWD/scripts/empty -./examples/echoserver/echoserver $NB_FLAG -1 -R $ready_file & +./examples/echoserver/echoserver -1 -R $ready_file & server_pid=$! create_port -./examples/scpclient/wolfscp $NB_FLAG -u jill -P upthehill -p $port -L $PWD/does-not-exist:$PWD/empty +./examples/scpclient/wolfscp -u jill -P upthehill -p $port -L $PWD/does-not-exist:$PWD/empty RESULT=$? remove_ready_file rm -f $PWD/scripts/empty