netwatch-sdk 0.4.0

Shared wire-format types and collectors for NetWatch Cloud — the SDK consumed by netwatch-agent and the NetWatch Cloud server. Parses /proc, ss, lsof, nettop, and libpcap events into a common Snapshot payload.
Documentation
name: CI

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

env:
  CARGO_TERM_COLOR: always

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: cargo check
        run: cargo check --all-targets
      - name: cargo test
        run: cargo test --lib
      - name: cargo fmt --check
        run: cargo fmt --all -- --check
        continue-on-error: true
      - name: cargo clippy
        run: cargo clippy --all-targets -- -D warnings
        continue-on-error: true

  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - uses: Swatinem/rust-cache@v2
      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@cargo-llvm-cov
      # Single instrumented run; subsequent `report` invocations reuse the data
      # so we don't pay for re-compiling tests just to change the output format.
      # Threshold (--fail-under-lines) is set just below the current baseline so
      # a regression fails CI but day-to-day improvements don't trip an alert.
      - name: Run coverage
        run: cargo llvm-cov --lib --no-report
      - name: Coverage summary
        run: cargo llvm-cov report --summary-only --fail-under-lines 83
      - name: Generate LCOV
        run: cargo llvm-cov report --lcov --output-path lcov.info
      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-lcov
          path: lcov.info

  ebpf-build:
    # Builds the BPF programs with the pinned nightly + bpf-linker, stages
    # the artifact via scripts/build-ebpf.sh, then rebuilds the SDK with
    # `--features ebpf` so the embedded include_bytes! resolves to the
    # real object. This proves Phase 1 wires up end-to-end without needing
    # a privileged container or actually loading the program in the kernel
    # (that's the future ebpf-integration job, not this one).
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Stable toolchain for the userspace SDK build.
      - uses: dtolnay/rust-toolchain@stable

      # Nightly toolchain for the BPF crate. The channel here MUST match
      # crates/ebpf-programs/rust-toolchain.toml. When you bump that file,
      # bump this `toolchain:` value in lockstep.
      #
      # Note: bpfel-unknown-none is a tier-3 target — there is no
      # rustup-installable rust-std for it. We use `-Z build-std=core`
      # (configured in crates/ebpf-programs/.cargo/config.toml) which
      # builds `core` from `rust-src`, so we install rust-src here but
      # NOT bpfel-unknown-none as a `target`.
      - name: Install nightly toolchain for BPF crate
        uses: dtolnay/rust-toolchain@nightly
        with:
          toolchain: nightly-2026-01-15
          components: rust-src,rustc-dev,llvm-tools-preview

      - uses: Swatinem/rust-cache@v2
        with:
          # Separate cache from the test job so the nightly artifacts don't
          # poison the stable build's cache.
          shared-key: ebpf-build

      - name: Install bpf-linker
        # Builds against the host LLVM (`apt install` provides LLVM 18 on
        # ubuntu-24.04 / 14 on ubuntu-22.04). bpf-linker is published on
        # crates.io.
        run: cargo install bpf-linker --locked

      - name: Build BPF programs
        run: scripts/build-ebpf.sh

      - name: Confirm BPF object was produced
        run: |
          test -s target/bpf/netwatch_sdk_ebpf.o
          file target/bpf/netwatch_sdk_ebpf.o
          ls -la target/bpf/

      - name: Build SDK with --features ebpf
        run: cargo build --features ebpf

      - name: Run SDK tests with --features ebpf
        run: cargo test --features ebpf --lib

  ebpf-integration:
    # Loads the compiled BPF program in the host kernel and asserts an
    # end-to-end round-trip: a userspace TCP connect triggers the kprobe,
    # the kernel writes an event into the ring buffer, and userspace
    # decodes it via EventSource.
    #
    # GitHub-hosted ubuntu-latest runners allow sudo and have modern
    # kernels with BTF, so this works without a self-hosted runner or
    # `--privileged` Docker gymnastics. The build-and-run-as-root dance
    # is:
    #   1. Build the BPF object with the pinned nightly + bpf-linker.
    #   2. Build the integration test binary as the runner user so
    #      cargo's paths resolve normally.
    #   3. Locate the emitted test binary, run it under sudo.
    needs: ebpf-build
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
      - uses: dtolnay/rust-toolchain@nightly
        with:
          toolchain: nightly-2026-01-15
          components: rust-src,rustc-dev,llvm-tools-preview
      # No rust-cache here — the build.rs staging of target/bpf/…
      # into OUT_DIR doesn't play well with cache restoration, which
      # can resurrect a stale (empty or malformed) embedded BPF object.
      # The BPF build is cheap; the clean build is worth the
      # determinism.

      - name: Install bpf-linker
        run: cargo install bpf-linker --locked

      - name: Build BPF programs
        run: scripts/build-ebpf.sh

      - name: Inspect the staged BPF object
        run: |
          ls -la target/bpf/
          file target/bpf/netwatch_sdk_ebpf.o
          sha256sum target/bpf/netwatch_sdk_ebpf.o
          head -c 64 target/bpf/netwatch_sdk_ebpf.o | xxd
          echo "size=$(stat -c%s target/bpf/netwatch_sdk_ebpf.o) bytes"
          echo
          echo "=== readelf -h ==="
          readelf -h target/bpf/netwatch_sdk_ebpf.o
          echo "=== readelf -SW ==="
          readelf -SW target/bpf/netwatch_sdk_ebpf.o
          echo "=== bpf-linker version ==="
          bpf-linker --version || true
          echo "=== aya crate version ==="
          grep '^name = "aya"' -A 1 Cargo.lock | head -4

      - name: Build integration test binary (unprivileged)
        run: cargo test --features ebpf --test ebpf_integration --no-run

      - name: Inspect what build.rs embedded into OUT_DIR
        run: |
          embedded=$(find target -name netwatch_sdk_ebpf.o -path '*/build/*/out/*' | head -1)
          if [ -n "$embedded" ]; then
            ls -la "$embedded"
            sha256sum "$embedded"
            head -c 16 "$embedded" | xxd
          else
            echo "!! no embedded BPF object found in build/out/"
            find target -name netwatch_sdk_ebpf.o -ls
          fi

      - name: Locate the test binary
        id: testbin
        run: |
          bin=$(cargo test --features ebpf --test ebpf_integration --no-run --message-format=json 2>/dev/null \
                | jq -rs '.[] | select(.profile.test == true and .target.name == "ebpf_integration") | .executable' \
                | tail -n1)
          test -n "$bin" || { echo "could not locate ebpf_integration test binary"; exit 1; }
          echo "bin=$bin" >> "$GITHUB_OUTPUT"
          echo "locating $bin"

      - name: Run integration test as root
        # sudo preserves the existing PATH so ldconfig, etc. still work.
        # --nocapture so the skip messages or assertion output show up.
        # `timeout 60` is belt-and-suspenders — if the test binary hangs
        # (e.g. reader-thread Drop deadlock) we want to know within a
        # minute, not after the whole job's 10-minute budget elapses.
        timeout-minutes: 3
        run: sudo timeout 60 ${{ steps.testbin.outputs.bin }} --nocapture