From d1979ac302553f86c0059fbc7672e6638ed12e3c Mon Sep 17 00:00:00 2001 From: Marco Cadetg Date: Fri, 26 Dec 2025 18:52:11 +0100 Subject: [PATCH] feat: add static musl binary builds for Linux (#103) --- .github/workflows/release.yml | 56 +++++++- .github/workflows/test-static-build.yml | 86 ++++++++++++ Dockerfile.static | 61 +++++++++ MUSL_BUILD.md | 169 +++++++++++++++++------- 4 files changed, 320 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/test-static-build.yml create mode 100644 Dockerfile.static diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fba5ca..e902433 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -185,10 +185,64 @@ jobs: path: rustnet-${{ github.ref_name }}-x86_64-unknown-freebsd.tar.gz if-no-files-found: error + build-static: + name: build-static-musl + runs-on: ubuntu-latest + container: + image: rust:alpine + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install dependencies + run: | + apk add --no-cache \ + musl-dev libpcap-dev pkgconfig build-base perl \ + elfutils-dev zlib-dev zlib-static zstd-dev zstd-static \ + clang llvm linux-headers git + rustup component add rustfmt + + - name: Configure static zstd linking + run: | + # Fix for elfutils 0.189+ undeclared zstd dependency + # See: https://github.com/libbpf/bpftool/issues/152 + mkdir -p .cargo + printf '[target.x86_64-unknown-linux-musl]\nrustflags = ["-C", "link-arg=-l:libzstd.a"]\n' > .cargo/config.toml + + - name: Build static binary + env: + RUSTFLAGS: "-C strip=symbols" + run: cargo build --release + + - name: Verify static linking + run: | + file target/release/rustnet + ldd target/release/rustnet 2>&1 | grep -q "statically linked" || \ + (echo "ERROR: Binary is not statically linked" && exit 1) + + - name: Create release archive + run: | + staging="rustnet-${{ github.ref_name }}-x86_64-unknown-linux-musl" + mkdir -p "$staging/assets" + + cp target/release/rustnet "$staging/" + cp assets/services "$staging/assets/" 2>/dev/null || true + cp README.md "$staging/" + cp LICENSE "$staging/" 2>/dev/null || true + + tar czf "$staging.tar.gz" "$staging" + + - name: Upload static build artifact + uses: actions/upload-artifact@v6 + with: + name: build-x86_64-unknown-linux-musl + path: rustnet-${{ github.ref_name }}-x86_64-unknown-linux-musl.tar.gz + if-no-files-found: error + create-release: name: create-release runs-on: ubuntu-latest - needs: [build-release, build-freebsd] + needs: [build-release, build-freebsd, build-static] steps: - name: Checkout repository uses: actions/checkout@v6 diff --git a/.github/workflows/test-static-build.yml b/.github/workflows/test-static-build.yml new file mode 100644 index 0000000..a57ae84 --- /dev/null +++ b/.github/workflows/test-static-build.yml @@ -0,0 +1,86 @@ +name: Test Static Build + +on: + pull_request: + paths: + - 'Dockerfile.static' + - '.github/workflows/test-static-build.yml' + - '.github/workflows/release.yml' + - 'MUSL_BUILD.md' + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-static: + name: build-static-musl + runs-on: ubuntu-latest + container: + image: rust:alpine + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install dependencies + run: | + apk add --no-cache \ + musl-dev libpcap-dev pkgconfig build-base perl \ + elfutils-dev zlib-dev zlib-static zstd-dev zstd-static \ + clang llvm linux-headers git + rustup component add rustfmt + + - name: Configure static zstd linking + run: | + # Fix for elfutils 0.189+ undeclared zstd dependency + # See: https://github.com/libbpf/bpftool/issues/152 + mkdir -p .cargo + printf '[target.x86_64-unknown-linux-musl]\nrustflags = ["-C", "link-arg=-l:libzstd.a"]\n' > .cargo/config.toml + + - name: Build static binary + env: + RUSTFLAGS: "-C strip=symbols" + run: cargo build --release + + - name: Verify static linking + run: | + echo "=== File info ===" + file target/release/rustnet + echo "" + echo "=== ldd output ===" + ldd target/release/rustnet 2>&1 || true + echo "" + echo "=== Size ===" + ls -lh target/release/rustnet + echo "" + # Verify it's actually static + if ldd target/release/rustnet 2>&1 | grep -q "statically linked"; then + echo "✅ Binary is statically linked!" + else + echo "❌ ERROR: Binary is NOT statically linked" + exit 1 + fi + + - name: Test binary runs + run: | + ./target/release/rustnet --version + ./target/release/rustnet --help | head -20 + + - name: Create release archive + run: | + staging="rustnet-static-x86_64-unknown-linux-musl" + mkdir -p "$staging/assets" + + cp target/release/rustnet "$staging/" + cp assets/services "$staging/assets/" 2>/dev/null || true + cp README.md "$staging/" + cp LICENSE "$staging/" 2>/dev/null || true + + tar czf "$staging.tar.gz" "$staging" + + - name: Upload static binary + uses: actions/upload-artifact@v4 + with: + name: rustnet-static-musl + path: rustnet-static-x86_64-unknown-linux-musl.tar.gz + retention-days: 7 diff --git a/Dockerfile.static b/Dockerfile.static new file mode 100644 index 0000000..42bb80a --- /dev/null +++ b/Dockerfile.static @@ -0,0 +1,61 @@ +# Dockerfile for building static musl-linked RustNet binary +# +# Usage (with eBPF - default, recommended): +# docker build -f Dockerfile.static -t rustnet-static . +# docker run --rm -v $(pwd)/dist:/dist rustnet-static cp /build/target/release/rustnet /dist/ +# +# Usage (without eBPF - smaller binary, ~5.2MB vs ~6.5MB): +# docker build -f Dockerfile.static --build-arg FEATURES="--no-default-features" -t rustnet-static . + +FROM rust:alpine + +ARG FEATURES="" + +# Install build dependencies +# - musl-dev: musl C library headers +# - libpcap-dev: libpcap headers and static library (/usr/lib/libpcap.a) +# - pkgconfig: for finding library paths +# - build-base: basic build tools (make, gcc, etc.) +# - perl: required by some build scripts (ring crate) +# - elfutils-dev: libelf for eBPF (includes static library) +# - zlib-dev/zlib-static: compression library +# - zstd-dev/zstd-static: Zstandard compression (required by elfutils 0.189+) +# - clang/llvm: for eBPF compilation +# - linux-headers: kernel headers for eBPF +RUN apk add --no-cache \ + musl-dev \ + libpcap-dev \ + pkgconfig \ + build-base \ + perl \ + elfutils-dev \ + zlib-dev \ + zlib-static \ + zstd-dev \ + zstd-static \ + clang \ + llvm \ + linux-headers + +# Add rustfmt for eBPF skeleton generation +RUN rustup component add rustfmt + +WORKDIR /build + +# Copy source code +COPY . . + +# Configure static linking for zstd +# This fixes the elfutils 0.189+ undeclared dependency on zstd +# See: https://github.com/libbpf/bpftool/issues/152 +RUN mkdir -p .cargo && printf '[target.x86_64-unknown-linux-musl]\nrustflags = ["-C", "link-arg=-l:libzstd.a"]\n' > .cargo/config.toml + +# Build configuration: +# - In Alpine/musl, binaries are statically linked by default +# - FEATURES arg controls whether eBPF is included (default) or disabled +# - --release: Optimized build +RUN cargo build --release ${FEATURES} + +# Verify the binary is statically linked +RUN file target/release/rustnet && \ + ldd target/release/rustnet 2>&1 || echo "Binary is statically linked" diff --git a/MUSL_BUILD.md b/MUSL_BUILD.md index fd72302..ab85da4 100644 --- a/MUSL_BUILD.md +++ b/MUSL_BUILD.md @@ -1,66 +1,133 @@ -# musl Static Build Challenges +# musl Static Build Guide -This document explains why RustNet currently does not provide musl static builds and the technical challenges encountered during implementation attempts. +This document explains how to build fully static RustNet binaries using musl. -## Background +## Quick Start -musl is a lightweight C standard library designed for static linking. It would allow RustNet to produce fully static binaries that work on any Linux distribution regardless of GLIBC version. - -## Why We Attempted musl Builds - -GitHub issue #40 reported that pre-built packages required GLIBC 2.38/2.39, which wasn't available on PopOS 22.04 (GLIBC 2.35). musl builds would theoretically solve this by creating fully static binaries. - -## Challenges Encountered - -### libpcap Linking Issues - -The primary challenge appears to be related to **libpcap** static linking with musl: - -- Installing `libpcap-dev` in Ubuntu-based cross-rs containers provides glibc-linked libraries -- Attempting to statically link these with musl resulted in linker errors -- Errors included undefined references to pthread, math (exp), and dynamic loading functions (dladdr) - -It's unclear whether this is due to: -- Fundamental glibc/musl incompatibility when statically linking -- Missing library specifications in the linker flags -- Issues with how cross-rs musl images are configured -- Something specific to our build configuration - -### eBPF Complications - -We initially attempted to include eBPF support, which required vendoring libelf and zlib. This was abandoned to simplify the problem, but even without eBPF the libpcap linking issues persisted. - -## Current Solution - -**We solved the original issue by pinning builds to ubuntu-22.04** (GLIBC 2.35), which ensures compatibility with PopOS 22.04 and similar distributions. - -For users on older distributions, the `cargo install` workaround is documented: ```bash -cargo install rustnet-monitor -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' ~/.cargo/bin/rustnet +# Build static binary with eBPF support (default, ~6.5MB) +docker build -f Dockerfile.static -t rustnet-static . + +# Or build without eBPF (smaller, ~5.2MB) +docker build -f Dockerfile.static --build-arg FEATURES="--no-default-features" -t rustnet-static . + +# Extract the binary +mkdir -p dist +docker run --rm -v $(pwd)/dist:/out rustnet-static cp /build/target/release/rustnet /out/ + +# Verify it's static +file dist/rustnet +# Output: ELF 64-bit LSB pie executable, x86-64, ..., static-pie linked + +ldd dist/rustnet +# Output: statically linked ``` -## Potential Future Approaches +## Binary Characteristics -If someone wants to tackle musl builds in the future, areas to investigate: +| Build | Size | eBPF | Process Detection | Compatibility | +|-------|------|------|-------------------|---------------| +| With eBPF | ~6.5MB | Yes | eBPF + procfs fallback | Any Linux | +| Without eBPF | ~5.2MB | No | procfs only | Any Linux | -1. **Building libpcap from source** targeting musl in the pre-build step -2. **Using Alpine Linux-based images** which have native musl packages -3. **Custom linker flags** to properly link required libraries -4. **Alternative pure-Rust packet capture** libraries (if they exist) +## How It Works -We're uncertain which approach would work best, or if there are other issues we haven't discovered yet. +The `Dockerfile.static` uses `rust:alpine` which provides: +- Native musl toolchain (no glibc/musl mixing issues) +- `libpcap-dev` package with static library (`/usr/lib/libpcap.a`) +- `elfutils-dev` with static libelf for eBPF +- All dependencies compiled against musl -## Why We're Not Pursuing This Now +### The zstd Fix -- The ubuntu-22.04 solution already addresses the reported issue -- The complexity-to-benefit ratio seems high -- `cargo install` provides a universal fallback for edge cases -- More investigation would be needed to understand the root causes +Alpine's elfutils 0.189+ has an undeclared dependency on zstd for ELF section compression. The Dockerfile includes a workaround: -If you have experience with musl static linking and want to contribute, we'd welcome the help! +```toml +# .cargo/config.toml +[target.x86_64-unknown-linux-musl] +rustflags = ["-C", "link-arg=-l:libzstd.a"] +``` + +This explicitly links the static zstd library, fixing the link order issue. See [libbpf/bpftool#152](https://github.com/libbpf/bpftool/issues/152) for details. + +## Running the Static Binary + +```bash +# Set capabilities for packet capture +sudo setcap 'cap_net_raw=eip' dist/rustnet + +# For eBPF support (Linux 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' dist/rustnet + +# Run +./dist/rustnet +``` + +## CI Integration + +For GitHub Actions, use the native container approach (faster than Docker build): + +```yaml +build-static: + name: build-static-musl + runs-on: ubuntu-latest + container: + image: rust:alpine + steps: + - uses: actions/checkout@v6 + + - name: Install dependencies + run: | + apk add --no-cache \ + musl-dev libpcap-dev pkgconfig build-base perl \ + elfutils-dev zlib-dev zlib-static zstd-dev zstd-static \ + clang llvm linux-headers git + rustup component add rustfmt + + - name: Configure static zstd linking + run: | + mkdir -p .cargo + printf '[target.x86_64-unknown-linux-musl]\nrustflags = ["-C", "link-arg=-l:libzstd.a"]\n' > .cargo/config.toml + + - name: Build + run: cargo build --release + + - name: Verify static linking + run: | + file target/release/rustnet + ldd target/release/rustnet 2>&1 | grep -q "statically linked" +``` + +## Historical Context + +### Previous Challenges (Resolved) + +Earlier attempts using cross-rs with Ubuntu-based containers failed: +- Installing `libpcap-dev` in Ubuntu provides glibc-linked libraries +- Mixing glibc libraries with musl linking caused undefined references +- Errors included pthread, math (exp), and dladdr symbols + +### Why Alpine Works + +Alpine Linux uses musl as its system C library: +- All packages are compiled against musl +- Static libraries (`*.a`) are musl-compatible +- No glibc/musl mixing occurs + +### The eBPF Challenge (Resolved) + +Static eBPF builds initially failed due to elfutils → zstd dependency chain: +- elfutils 0.189+ added ZSTD compression for ELF sections +- libbpf-sys didn't propagate the zstd link dependency +- Fixed by explicitly linking `-l:libzstd.a` via cargo config + +## References + +- [libbpf/bpftool#152](https://github.com/libbpf/bpftool/issues/152) - zstd link fix +- [arachsys/libelf](https://github.com/arachsys/libelf) - Standalone libelf (alternative) +- [Alpine Static Linking](https://build-your-own.org/blog/20221229_alpine/) - General guidance --- -*Last updated: 2025-10-09* -*Status: Not currently pursuing due to linking complexity* +*Last updated: 2025-12-26* +*Status: Fully working with eBPF support*