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 # 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" # 6. Cleanup: kill in reverse order # On Windows (Git Bash), kill might need -INT or taskkill fallback kill "$CONNECTOR_PID" 2>/dev/null || true kill "$LISTENER_PID" 2>/dev/null || true kill "$TARGET_PID" 2>/dev/null || true sleep 1 # Force kill any remaining processes on the ports for PORT in $SOCKS_PORT $LISTEN_PORT $TARGET_PORT; do lsof -tiTCP:$PORT -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true # Windows fallback: use PowerShell to find and kill by port netstat -ano 2>/dev/null | grep ":$PORT " | grep LISTENING | awk '{print $5}' | xargs -I{} taskkill //PID {} //F 2>/dev/null || true done wait "$CONNECTOR_PID" 2>/dev/null || true wait "$LISTENER_PID" 2>/dev/null || true wait "$TARGET_PID" 2>/dev/null || true echo "E2E test passed on ${{ matrix.os }}"