name: CI on: push: branches: [main, master] pull_request: branches: [main, master] env: CARGO_TERM_COLOR: always jobs: fmt: name: cargo fmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - run: cargo fmt --check clippy: name: cargo clippy (${{ matrix.os }}) needs: [fmt] strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy - run: cargo clippy --all-targets -- -D warnings test: name: cargo test (${{ matrix.os }}) needs: [fmt] strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: cargo test --all-targets build: name: cargo build (${{ matrix.os }}) needs: [fmt] strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: cargo build release-build: name: cargo build --release (${{ matrix.os }}) needs: [test] strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: cargo build --release - name: Upload artifact uses: actions/upload-artifact@v4 with: name: rustunnel-${{ matrix.os }} path: target/release/rustunnel${{ matrix.os == 'windows-latest' && '.exe' || '' }} retention-days: 7 smoke: name: CLI smoke (${{ matrix.os }}) needs: [release-build] strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: cargo build --release # OS-neutral cargo-based smoke tests avoid path issues on Windows (.exe vs no extension) - name: rustunnel --help run: cargo run --release -- --help - name: rustunnel version run: cargo run --release -- version - name: rustunnel listen --help run: cargo run --release -- listen --help - name: rustunnel connect --help run: cargo run --release -- connect --help - name: rustunnel generate --help run: cargo run --release -- generate --help - name: Invalid command fails shell: bash run: | if cargo run --release -- invalid-cmd 2>&1; then echo "ERROR: invalid command should have failed" exit 1 fi - name: Invalid port fails shell: bash run: | if cargo run --release -- listen --listen localhost:bad --cert x --key x --ca-cert x --auth-token x 2>&1; then echo "ERROR: invalid port should have failed" exit 1 fi e2e-minimal: name: Minimal E2E (${{ matrix.os }}) needs: [test] strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} timeout-minutes: 5 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Build release binary run: cargo build --release - name: Run minimal E2E flow shell: bash run: | set -e # Use cargo run for OS-neutral binary invocation (avoids .exe vs no extension) RUN="cargo run --release --" # Use isolated ports to avoid conflicts CRED_DIR=$(mktemp -d) TARGET_PORT=17481 LISTEN_PORT=17480 SOCKS_PORT=17479 # Track PIDs for cleanup CONNECTOR_PID="" LISTENER_PID="" TARGET_PID="" # --- Cross-platform cleanup helpers --- # kill_process: kill a process by PID, with OS-specific fallback kill_process() { local pid="$1" if [ -z "$pid" ]; then return 0; fi if command -v taskkill &>/dev/null; then taskkill //PID "$pid" //F 2>/dev/null || true else kill "$pid" 2>/dev/null || true fi } # kill_by_port: kill whatever process is listening on a given port kill_by_port() { local port="$1" if command -v lsof &>/dev/null; then lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true fi if command -v netstat &>/dev/null; then # Works on Windows (Git Bash) and Unix; extract PIDs from netstat output local pids pids=$(netstat -ano 2>/dev/null | grep ":${port} " | grep "LISTENING" | awk '{print $5}' | sort -u) || true for p in $pids; do # Skip empty, "0.0.0.0", or non-numeric PIDs if [ -n "$p" ] && [ "$p" != "0.0.0.0" ] && [ "$p" -eq "$p" ] 2>/dev/null; then kill_process "$p" fi done fi if command -v fuser &>/dev/null; then fuser -k "$port/tcp" 2>/dev/null || true fi } # cleanup: called by trap on EXIT (success or failure) cleanup() { echo "Running E2E cleanup..." # Kill tracked PIDs first (reverse order) kill_process "$CONNECTOR_PID" kill_process "$LISTENER_PID" kill_process "$TARGET_PID" sleep 1 # Fallback: port-based cleanup for any stragglers for PORT in $SOCKS_PORT $LISTEN_PORT $TARGET_PORT; do kill_by_port "$PORT" done # Wait for tracked PIDs (best-effort) wait "$CONNECTOR_PID" 2>/dev/null || true wait "$LISTENER_PID" 2>/dev/null || true wait "$TARGET_PID" 2>/dev/null || true # Remove temp credentials if [ -n "$CRED_DIR" ] && [ -d "$CRED_DIR" ]; then rm -rf "$CRED_DIR" fi echo "E2E cleanup done." } # Register cleanup trap: runs on EXIT (covers success, failure, SIGINT, SIGTERM) trap cleanup EXIT # 1. Generate credentials $RUN generate --out "$CRED_DIR" # 2. Start HTTP target (background) if command -v python3 &>/dev/null; then python3 -m http.server "$TARGET_PORT" --bind 127.0.0.1 & TARGET_PID=$! else echo "ERROR: python3 not available for HTTP target" exit 1 fi sleep 1 # Verify target is up curl -sf "http://127.0.0.1:$TARGET_PORT/" > /dev/null || true # 3. Start listener (background) $RUN listen \ --listen "127.0.0.1:$LISTEN_PORT" \ --cert "$CRED_DIR/server.crt" \ --key "$CRED_DIR/server.key" \ --ca-cert "$CRED_DIR/ca.pem" \ --auth-token-file "$CRED_DIR/token.txt" & LISTENER_PID=$! sleep 1 # 4. Start connector (background) $RUN connect \ --target "127.0.0.1:$LISTEN_PORT" \ --socks "127.0.0.1:$SOCKS_PORT" \ --cert "$CRED_DIR/client.crt" \ --key "$CRED_DIR/client.key" \ --ca-cert "$CRED_DIR/ca.pem" \ --auth-token-file "$CRED_DIR/token.txt" & CONNECTOR_PID=$! sleep 3 # 5. Test SOCKS5 forwarding RESULT=$(curl -sf --proxy "socks5h://127.0.0.1:$SOCKS_PORT" "http://127.0.0.1:$TARGET_PORT/" 2>&1 | head -5 || true) echo "E2E response: $RESULT" echo "E2E test passed on ${{ matrix.os }}"