net-io 0.6.0

Network IO substrate for Rust: TCP, UDP and local-socket listeners, connection lifecycle, limits, graceful drain, PROXY protocol, and a TLS seam. Protocol-free and dependency-free.
Documentation
name: CI

on:
  push:
    branches: [main, dev]
  pull_request:
    branches: [main, dev]

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: "-D warnings"
  # Run every JS action on Node.js 24 ahead of GitHub's forced migration,
  # silencing "Node.js 20 actions are deprecated" runner warnings.
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

jobs:
  test:
    name: Test (${{ matrix.os }} / ${{ matrix.rust }})
    runs-on: ${{ matrix.os }}
    timeout-minutes: 40
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        rust: [stable, "1.85"]
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ matrix.rust }}
          components: clippy, rustfmt
      - uses: Swatinem/rust-cache@v2
      - name: Format
        run: cargo fmt --all -- --check
      - name: Clippy (default features)
        run: cargo clippy --all-targets -- -D warnings
      - name: Clippy (all features)
        run: cargo clippy --all-targets --all-features -- -D warnings
      - name: Clippy (no default features)
        run: cargo clippy --all-targets --no-default-features -- -D warnings
      - name: Test (default features)
        run: cargo test --no-fail-fast
      - name: Test (all features)
        run: cargo test --all-features --no-fail-fast
      - name: Test (no default features)
        run: cargo test --no-default-features --no-fail-fast
      - name: Build examples
        run: cargo build --examples
      - name: Run self-contained examples
        run: |
          cargo run --example kill_and_drain
          cargo run --example gate
          cargo run --example unix_socket
          cargo run --example offload_completer
      - name: Doc (all features)
        run: cargo doc --no-deps --all-features
        env:
          RUSTDOCFLAGS: "-D warnings"

  shim:
    name: Fault injection (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    env:
      RUSTFLAGS: "-D warnings --cfg net_io_shim"
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Shim tests (faults and system calls per round trip)
        run: cargo test --test shim -- --test-threads=1 --nocapture

  blocking:
    name: Blocking driver forced (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    env:
      RUSTFLAGS: "-D warnings --cfg net_io_force_blocking"
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Every suite on the blocking pool
        run: cargo test --all-features --tests --no-fail-fast

  idle:
    name: 10,000 idle connections (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Build
        run: cargo test --release --all-features --test idle --no-run
      - name: Raise the descriptor limit (macOS)
        if: runner.os == 'macOS'
        run: sudo sysctl -w kern.maxfiles=100000 kern.maxfilesperproc=60000
      - name: Idle connections
        shell: bash
        run: |
          ulimit -n 30000 || true
          cargo test --release --all-features --test idle -- --nocapture
        env:
          NET_IO_IDLE_CONNECTIONS: "10000"

  freebsd:
    name: FreeBSD
    runs-on: ubuntu-latest
    timeout-minutes: 45
    steps:
      - uses: actions/checkout@v5
      - name: Test in a FreeBSD VM
        uses: vmactions/freebsd-vm@v1
        with:
          release: "14.3"
          usesh: true
          prepare: pkg install -y rust
          run: |
            cargo test --all-features --no-fail-fast

  sanitizers:
    name: ${{ matrix.name }}
    runs-on: ubuntu-latest
    timeout-minutes: 40
    strategy:
      fail-fast: false
      matrix:
        include:
          - name: AddressSanitizer
            sanitizer: address
            build_std: ""
          - name: ThreadSanitizer
            sanitizer: thread
            build_std: "-Zbuild-std"
    env:
      RUSTFLAGS: "-Zsanitizer=${{ matrix.sanitizer }}"
      TSAN_OPTIONS: "report_signal_unsafe=0"
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@nightly
        with:
          components: rust-src
      - name: Unit tests
        # A few parser tests leak on purpose, to get `&'static` input.
        run: cargo test ${{ matrix.build_std }} --target x86_64-unknown-linux-gnu --all-features --lib
        env:
          ASAN_OPTIONS: detect_leaks=0
      - name: Session and driver suites
        run: |
          cargo test ${{ matrix.build_std }} --target x86_64-unknown-linux-gnu --all-features \
            --test server --test edge --test close_reasons --test tls --test proxy --test unix \
            --test reactor --test driver --test interleave

  bench:
    name: Benchmarks (not a gate)
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Echo
        run: cargo bench --bench echo
      - name: Lifecycle
        run: |
          ulimit -n 30000
          cargo bench --bench lifecycle
      - name: Timers and security costs
        run: cargo test --release --lib bench_ -- --ignored --nocapture

  compare:
    name: Against other stacks (not a gate)
    runs-on: ubuntu-latest
    timeout-minutes: 45
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: compare
      - name: The table
        working-directory: compare
        run: |
          ulimit -n 300000 || ulimit -n 30000
          cargo run --release

  loom:
    name: Loom models
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      RUSTFLAGS: "-D warnings --cfg net_io_loom"
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Loom
        run: cargo test --release --lib loom_

  miri:
    name: Miri (pure modules)
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@nightly
        with:
          components: miri
      - name: Miri
        run: |
          cargo miri test --lib -- addr:: buf:: rate:: session:: completer:: limits:: id:: control:: events:: error:: time:: info:: trusted:: wheel::

  leak:
    name: Descriptor leak, 100,000 connections
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Leak test
        run: cargo test --release --test leak
        env:
          NET_IO_LEAK_CONNECTIONS: "100000"

  unsafe-location:
    name: unsafe only in src/sys
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v5
      - name: Check
        run: |
          if grep -rnE '\bunsafe[[:space:]]*(\{|fn|impl|extern|trait)' src --include='*.rs' | grep -v '^src/sys/'; then
            echo "unsafe outside src/sys (see dev/DIRECTIVES.md section 2)"
            exit 1
          fi

  interop:
    name: Behind HAProxy and nginx (PROXY v1 and v2, TCP and Unix)
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Install HAProxy and nginx
        run: |
          sudo apt-get update -q
          sudo apt-get install -y -q haproxy nginx libnginx-mod-stream
      - name: Start net-io behind both proxies
        run: |
          cargo build --example behind_proxy
          mkdir -p /tmp/nio /tmp/nginx
          chmod 0700 /tmp/nio
          target/debug/examples/behind_proxy 127.0.0.1:7002 /tmp/nio/proxy.sock > /tmp/nio/server.log 2>&1 &
          cat > /tmp/nio/haproxy.cfg <<'CFG'
          global
            maxconn 256
          defaults
            mode tcp
            timeout connect 5s
            timeout client 10s
            timeout server 10s
          listen tcp_v1
            bind 127.0.0.1:8001
            server s 127.0.0.1:7002 send-proxy
          listen tcp_v2
            bind 127.0.0.1:8002
            server s 127.0.0.1:7002 send-proxy-v2
          listen unix_v1
            bind 127.0.0.1:8003
            server s /tmp/nio/proxy.sock send-proxy
          listen unix_v2
            bind 127.0.0.1:8004
            server s /tmp/nio/proxy.sock send-proxy-v2
          CFG
          haproxy -f /tmp/nio/haproxy.cfg -D -p /tmp/nio/haproxy.pid
          module=$(ls /usr/lib/nginx/modules/ngx_stream_module.so 2>/dev/null || true)
          cat > /tmp/nginx/nginx.conf <<CFG
          ${module:+load_module $module;}
          pid /tmp/nginx/nginx.pid;
          error_log /tmp/nginx/error.log;
          events {}
          stream {
            server { listen 127.0.0.1:8005; proxy_pass 127.0.0.1:7002; proxy_protocol on; }
            server { listen 127.0.0.1:8006; proxy_pass unix:/tmp/nio/proxy.sock; proxy_protocol on; }
          }
          CFG
          nginx -p /tmp/nginx -c /tmp/nginx/nginx.conf
          sleep 1
      - name: The real client address arrives through every path
        run: |
          python3 - <<'PY'
          import socket, sys
          paths = {8001: "HAProxy v1 to TCP", 8002: "HAProxy v2 to TCP", 8003: "HAProxy v1 to Unix",
                   8004: "HAProxy v2 to Unix", 8005: "nginx v1 to TCP", 8006: "nginx v1 to Unix"}
          failed = False
          for port, name in paths.items():
              s = socket.create_connection(("127.0.0.1", port), timeout=10)
              mine = s.getsockname()
              line = s.makefile().readline().strip()
              want = f"client={mine[0]}:{mine[1]}"
              ok = line.startswith(want + " ")
              failed |= not ok
              print(("ok  " if ok else "BAD ") + f"{name}: {line} (expected {want})")
          sys.exit(1 if failed else 0)
          PY
      - name: Logs
        if: always()
        run: |
          cat /tmp/nio/server.log || true
          cat /tmp/nginx/error.log || true

  fuzz:
    name: Fuzz (30 s per target)
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@nightly
      - uses: taiki-e/install-action@v2
        with:
          tool: cargo-fuzz
      - name: Fuzz
        working-directory: fuzz
        run: |
          for t in proxy manifest systemd_env cidr trusted; do
            # The prebuilt cargo-fuzz is a musl binary; name the target explicitly.
            cargo fuzz run --target x86_64-unknown-linux-gnu "$t" -- -max_total_time=30 -rss_limit_mb=1024
          done
        env:
          RUSTFLAGS: ""

  security:
    name: Security (audit + deny)
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Install cargo-audit and cargo-deny
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-audit,cargo-deny
      - name: Generate lockfile
        run: cargo generate-lockfile
      - name: Audit
        run: cargo audit
      - name: Deny
        run: cargo deny check