diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..413f4738 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Version control +.git +.github +.forgejo +.claude +CLAUDE.md + +# Build artifacts +target +**/target +mutants.out* +.bench-tools + +# Documentation, internal specs, and superpowers +.superpowers +docs/superpowers +docs/research +docs/2026-*.md +docs/*-plan.md +docs/*-design.md +docs/*-implementation.md +docs/nlnet-application.md +refrences +projectref.md + +# Python artifacts +__pycache__ +*.pyc +.pytest_cache + +# OS and editor files +.DS_Store +*.swp +*~ diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 00000000..56b43075 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,206 @@ +--- +name: Release + +'on': + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag (e.g. v0.1.0)' + required: false + type: string + +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-D warnings" + +jobs: + build-release: + name: build (${{ matrix.target }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-musl + musl_cross_url: https://musl.cc/x86_64-linux-musl-cross.tgz + musl_cross_dir: x86_64-linux-musl-cross + linker: x86_64-linux-musl-gcc + cc: x86_64-linux-musl-gcc + cxx: x86_64-linux-musl-g++ + ar: x86_64-linux-musl-ar + - target: aarch64-unknown-linux-musl + musl_cross_url: https://musl.cc/aarch64-linux-musl-cross.tgz + musl_cross_dir: aarch64-linux-musl-cross + linker: aarch64-linux-musl-gcc + cc: aarch64-linux-musl-gcc + cxx: aarch64-linux-musl-g++ + ar: aarch64-linux-musl-ar + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install build prerequisites (BoringSSL cmake + bindgen clang) + run: | + SUDO="$(command -v sudo 2>/dev/null || true)" + $SUDO apt-get update + $SUDO apt-get install -y --no-install-recommends cmake clang libclang-dev pkg-config binutils file jq curl + + - name: Set up musl cross-compilation toolchain + run: | + SUDO="$(command -v sudo 2>/dev/null || true)" + echo "Fetching ${{ matrix.target }} musl toolchain..." + curl -fsSL --retry 3 --retry-connrefused "${{ matrix.musl_cross_url }}" | $SUDO tar -xz -C /opt + echo "/opt/${{ matrix.musl_cross_dir }}/bin" >> $GITHUB_PATH + UPPER_TARGET="$(echo "${{ matrix.target }}" | tr '[:lower:]' '[:upper:]' | tr '-' '_')" + echo "CARGO_TARGET_${UPPER_TARGET}_LINKER=/opt/${{ matrix.musl_cross_dir }}/bin/${{ matrix.linker }}" >> $GITHUB_ENV + echo "CC_${{ matrix.target }}=/opt/${{ matrix.musl_cross_dir }}/bin/${{ matrix.cc }}" >> $GITHUB_ENV + echo "CXX_${{ matrix.target }}=/opt/${{ matrix.musl_cross_dir }}/bin/${{ matrix.cxx }}" >> $GITHUB_ENV + echo "AR_${{ matrix.target }}=/opt/${{ matrix.musl_cross_dir }}/bin/${{ matrix.ar }}" >> $GITHUB_ENV + SYSROOT_DIR="$(echo "${{ matrix.target }}" | sed 's/-unknown-/-/')" + echo "BINDGEN_EXTRA_CLANG_ARGS_${{ matrix.target }}=--sysroot=/opt/${{ matrix.musl_cross_dir }}/${SYSROOT_DIR}" >> $GITHUB_ENV + + - name: Resolve release tag + id: vars + run: | + TAG="${{ inputs.tag }}" + if [ -z "$TAG" ]; then + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + TAG="${{ github.ref_name }}" + else + TAG="v0.0.0-${GITHUB_SHA::8}" + fi + fi + case "$TAG" in + v*) ;; + *) TAG="v$TAG" ;; + esac + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Build release binaries + run: | + cargo build --release --target ${{ matrix.target }} \ + -p yipd -p yip-ca -p yip-rendezvous-bin + + - name: Verify static linkage + run: | + TARGET_DIR="target/${{ matrix.target }}/release" + for bin in yipd yip-ca yip-rendezvous; do + file "${TARGET_DIR}/${bin}" + if command -v readelf >/dev/null 2>&1; then + if readelf -l "${TARGET_DIR}/${bin}" | grep -q INTERP; then + echo "Error: ${bin} contains dynamic INTERP header!" >&2 + exit 1 + fi + fi + done + + - name: Package release archive + run: | + TAG="${{ steps.vars.outputs.tag }}" + TARGET="${{ matrix.target }}" + PKG_NAME="yip-${TAG}-${TARGET}" + STAGE_DIR="${RUNNER_TEMP:-/tmp}/${PKG_NAME}" + rm -rf "${STAGE_DIR}" + mkdir -p "${STAGE_DIR}" + + TARGET_DIR="target/${TARGET}/release" + cp "${TARGET_DIR}/yipd" "${STAGE_DIR}/" + cp "${TARGET_DIR}/yip-ca" "${STAGE_DIR}/" + cp "${TARGET_DIR}/yip-rendezvous" "${STAGE_DIR}/" + cp example.config "${STAGE_DIR}/" + cp LICENSE "${STAGE_DIR}/" + cp README.md "${STAGE_DIR}/" + cp -r contrib "${STAGE_DIR}/" + + tar -czvf "${PKG_NAME}.tar.gz" -C "${RUNNER_TEMP:-/tmp}" "${PKG_NAME}" + sha256sum "${PKG_NAME}.tar.gz" > "${PKG_NAME}.tar.gz.sha256" + + mkdir -p dist + mv "${PKG_NAME}.tar.gz" dist/ + mv "${PKG_NAME}.tar.gz.sha256" dist/ + + - name: Upload release artifact + uses: actions/upload-artifact@v4 + with: + name: release-${{ matrix.target }} + path: | + dist/yip-*.tar.gz + dist/yip-*.tar.gz.sha256 + if-no-files-found: error + + publish-release: + name: publish + needs: build-release + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.tag != '') + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Download all release artifacts + uses: actions/download-artifact@v4 + with: + pattern: release-* + merge-multiple: true + path: dist + + - name: Collate SHA256SUMS + run: | + cd dist + sha256sum yip-*.tar.gz > SHA256SUMS + cat SHA256SUMS + + - name: Resolve release tag + id: vars + run: | + TAG="${{ inputs.tag }}" + if [ -z "$TAG" ]; then + TAG="${{ github.ref_name }}" + fi + case "$TAG" in + v*) ;; + *) TAG="v$TAG" ;; + esac + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Publish Forgejo Release + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.vars.outputs.tag }} + run: | + API_URL="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" + echo "Checking release for tag ${TAG} at ${API_URL}/releases/tags/${TAG}..." + + RELEASE_JSON=$(curl -fsSL -H "Authorization: token ${TOKEN}" "${API_URL}/releases/tags/${TAG}" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty' 2>/dev/null || true) + + if [ -z "$RELEASE_ID" ]; then + echo "Creating new release for ${TAG}..." + CREATE_PAYLOAD=$(jq -n --arg tag "$TAG" --arg name "$TAG" \ + '{tag_name: $tag, name: $name, draft: false, prerelease: false, body: "Automated multi-arch static musl release."}') + RELEASE_JSON=$(curl -fsSL -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" \ + -d "$CREATE_PAYLOAD" "${API_URL}/releases") + RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id') + fi + + echo "Uploading release assets to release ID ${RELEASE_ID}..." + for f in dist/yip-*.tar.gz dist/SHA256SUMS; do + [ -f "$f" ] || continue + fname=$(basename "$f") + echo "Uploading ${fname}..." + curl -fsSL -X POST -H "Authorization: token ${TOKEN}" \ + -F "attachment=@$f" "${API_URL}/releases/${RELEASE_ID}/assets?name=${fname}" || echo "Warning: Failed to upload ${fname}" + done + echo "Forgejo release publication finished." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..67d2227c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,183 @@ +--- +name: Release + +'on': + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag (e.g. v0.1.0)' + required: false + type: string + +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-D warnings" + +jobs: + build-release: + name: build (${{ matrix.target }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-musl + musl_cross_url: https://musl.cc/x86_64-linux-musl-cross.tgz + musl_cross_dir: x86_64-linux-musl-cross + linker: x86_64-linux-musl-gcc + cc: x86_64-linux-musl-gcc + cxx: x86_64-linux-musl-g++ + ar: x86_64-linux-musl-ar + - target: aarch64-unknown-linux-musl + musl_cross_url: https://musl.cc/aarch64-linux-musl-cross.tgz + musl_cross_dir: aarch64-linux-musl-cross + linker: aarch64-linux-musl-gcc + cc: aarch64-linux-musl-gcc + cxx: aarch64-linux-musl-g++ + ar: aarch64-linux-musl-ar + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install build prerequisites (BoringSSL cmake + bindgen clang) + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends cmake clang libclang-dev pkg-config binutils file + + - name: Set up musl cross-compilation toolchain + run: | + echo "Fetching ${{ matrix.target }} musl toolchain..." + curl -fsSL --retry 3 --retry-connrefused "${{ matrix.musl_cross_url }}" | sudo tar -xz -C /opt + echo "/opt/${{ matrix.musl_cross_dir }}/bin" >> $GITHUB_PATH + UPPER_TARGET="$(echo "${{ matrix.target }}" | tr '[:lower:]' '[:upper:]' | tr '-' '_')" + echo "CARGO_TARGET_${UPPER_TARGET}_LINKER=/opt/${{ matrix.musl_cross_dir }}/bin/${{ matrix.linker }}" >> $GITHUB_ENV + echo "CC_${{ matrix.target }}=/opt/${{ matrix.musl_cross_dir }}/bin/${{ matrix.cc }}" >> $GITHUB_ENV + echo "CXX_${{ matrix.target }}=/opt/${{ matrix.musl_cross_dir }}/bin/${{ matrix.cxx }}" >> $GITHUB_ENV + echo "AR_${{ matrix.target }}=/opt/${{ matrix.musl_cross_dir }}/bin/${{ matrix.ar }}" >> $GITHUB_ENV + SYSROOT_DIR="$(echo "${{ matrix.target }}" | sed 's/-unknown-/-/')" + echo "BINDGEN_EXTRA_CLANG_ARGS_${{ matrix.target }}=--sysroot=/opt/${{ matrix.musl_cross_dir }}/${SYSROOT_DIR}" >> $GITHUB_ENV + + - name: Resolve release tag + id: vars + run: | + TAG="${{ inputs.tag }}" + if [ -z "$TAG" ]; then + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + TAG="${{ github.ref_name }}" + else + TAG="v0.0.0-${GITHUB_SHA::8}" + fi + fi + case "$TAG" in + v*) ;; + *) TAG="v$TAG" ;; + esac + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Build release binaries + run: | + cargo build --release --target ${{ matrix.target }} \ + -p yipd -p yip-ca -p yip-rendezvous-bin + + - name: Verify static linkage + run: | + TARGET_DIR="target/${{ matrix.target }}/release" + for bin in yipd yip-ca yip-rendezvous; do + file "${TARGET_DIR}/${bin}" + if command -v readelf >/dev/null 2>&1; then + if readelf -l "${TARGET_DIR}/${bin}" | grep -q INTERP; then + echo "Error: ${bin} contains dynamic INTERP header!" >&2 + exit 1 + fi + fi + done + + - name: Package release archive + run: | + TAG="${{ steps.vars.outputs.tag }}" + TARGET="${{ matrix.target }}" + PKG_NAME="yip-${TAG}-${TARGET}" + STAGE_DIR="${RUNNER_TEMP:-/tmp}/${PKG_NAME}" + rm -rf "${STAGE_DIR}" + mkdir -p "${STAGE_DIR}" + + TARGET_DIR="target/${TARGET}/release" + cp "${TARGET_DIR}/yipd" "${STAGE_DIR}/" + cp "${TARGET_DIR}/yip-ca" "${STAGE_DIR}/" + cp "${TARGET_DIR}/yip-rendezvous" "${STAGE_DIR}/" + cp example.config "${STAGE_DIR}/" + cp LICENSE "${STAGE_DIR}/" + cp README.md "${STAGE_DIR}/" + cp -r contrib "${STAGE_DIR}/" + + tar -czvf "${PKG_NAME}.tar.gz" -C "${RUNNER_TEMP:-/tmp}" "${PKG_NAME}" + sha256sum "${PKG_NAME}.tar.gz" > "${PKG_NAME}.tar.gz.sha256" + + mkdir -p dist + mv "${PKG_NAME}.tar.gz" dist/ + mv "${PKG_NAME}.tar.gz.sha256" dist/ + + - name: Upload release artifact + uses: actions/upload-artifact@v4 + with: + name: release-${{ matrix.target }} + path: | + dist/yip-*.tar.gz + dist/yip-*.tar.gz.sha256 + if-no-files-found: error + + publish-release: + name: publish + needs: build-release + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.tag != '') + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download all release artifacts + uses: actions/download-artifact@v4 + with: + pattern: release-* + merge-multiple: true + path: dist + + - name: Collate SHA256SUMS + run: | + cd dist + sha256sum yip-*.tar.gz > SHA256SUMS + cat SHA256SUMS + + - name: Resolve release tag + id: vars + run: | + TAG="${{ inputs.tag }}" + if [ -z "$TAG" ]; then + TAG="${{ github.ref_name }}" + fi + case "$TAG" in + v*) ;; + *) TAG="v$TAG" ;; + esac + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Publish GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.vars.outputs.tag }} + files: | + dist/yip-*.tar.gz + dist/SHA256SUMS + generate_release_notes: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f63cf9..a8868983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ until 0.1.0. Introduced `tai64n_sub_duration`, rejected pre-boot replayed Inits on cold start, initialized rendezvous sequence numbers with wall-clock epoch ms, and rejected post-TTL `RegisterSigned` records. ### Added +- **Zero-friction distribution & packaging suite (`contrib/`, `Dockerfile`, `install.sh`, `Cargo.toml`, `.github/workflows/release.yml`, #147).** + Added standalone, statically linked musl distribution binaries (`x86_64-unknown-linux-musl` and `aarch64-unknown-linux-musl`) with zero runtime dynamic dependencies. Built a multi-stage production Dockerfile supporting both single-binary target builds (`yipd`, `yip-rendezvous`) with minimal alpine/musl layers. Implemented a turnkey POSIX-compliant 1-line installer (`install.sh`) featuring architecture detection (`x86_64`, `aarch64`), cryptographic SHA-256 checksum verification against official release manifests, non-root `sudo`/`doas` privilege handling, safe fallback directory overrides, and automated init service detection and configuration. Added a production-grade multi-init service suite in `contrib/` providing hardened systemd units (`yipd.service` and `yip-rendezvous.service`) with `LimitMEMLOCK=infinity`, `CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_BPF CAP_IPC_LOCK`, `DeviceAllow=/dev/net/tun rw`, and strict sandboxing (`ProtectSystem=strict`, `ProtectHome=true`, `PrivateTmp=true`), alongside OpenRC service scripts (`contrib/openrc/`) and runit service directories (`contrib/runit/`). Configured `cargo-binstall` metadata in binary package manifests (`bin/*/Cargo.toml`) for direct binary installation from GitHub Releases. Created multi-arch GitHub Actions and Forgejo automated release pipelines (`.github/workflows/release.yml`, `.forgejo/workflows/release.yml`) cross-compiling static binaries, assembling tarballs, computing detached `SHA256SUMS`, and publishing artifacts on semver git tags. - **Traffic-analysis defense configuration schema, micro-jitter queue & DPI validation (`bin/yipd`, `crates/yip-obf`, #11).** Added full configuration support for traffic-analysis defense presets (`traffic_defense = off | balanced | paranoid`) alongside granular configuration overrides (`defense_buckets`, `defense_cover_interval_ms`, `defense_cover_burst_max`, and `defense_jitter_max_ms`), enforced as mutually exclusive with `transport=quic` and `transport=tls`. Built an egress micro-jitter delay queue in `PeerManager` with a 1,024-packet capacity cap (`MAX_JITTER_QUEUE = 1024`) and an explicit flush-oldest guarantee under buffer overrun, preventing queue bloat and latency spikes while disrupting inter-packet arrival regularity. Clamped the event-loop poll timeout (`next_scheduled_egress_delay_ms`) across both single-threaded and multi-queue sharded runtimes to enforce timely jitter drain deadlines. Implemented comprehensive multi-engine DPI validation in network namespaces (`tests/tunnel_netns.rs`, `run-netns-tunnel.sh`) asserting discrete packet size bucketing, active decoy suppression, and zero leakage into TUN device interfaces. - **Data-plane windowed loss calculation, 8-contender harness & UDP-under-loss evaluation (`bin/yipd`, `crates/yip-transport`, `crates/yip-bench`, #11).** diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..7dd29bc6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,51 @@ +# Multi-stage production Dockerfile for yip suite + +# Stage 1: Builder +FROM rust:alpine AS builder + +# Install build essentials required for compiling C/C++ dependencies and BoringSSL +RUN apk add --no-cache musl-dev gcc g++ make linux-headers git cmake perl clang-dev + +ENV LIBCLANG_PATH=/usr/lib +# Note on crt-static: On Alpine Linux, bindgen links dynamically against libclang.so, +# and BoringSSL compiles C++ components. We compile with -crt-static and package +# into a minimal (~20MB) Alpine runtime layer with ca-certificates and libstdc++. +ENV RUSTFLAGS="-C target-feature=-crt-static" + +WORKDIR /usr/src/yip + +# Copy workspace manifests and source code +COPY Cargo.toml Cargo.lock ./ +COPY crates ./crates +COPY bin ./bin + +# Build release binaries and place them into /usr/local/bin +RUN cargo build --release --bin yipd --bin yip-ca --bin yip-rendezvous && \ + cp target/release/yipd /usr/local/bin/yipd && \ + cp target/release/yip-ca /usr/local/bin/yip-ca && \ + cp target/release/yip-rendezvous /usr/local/bin/yip-rendezvous + +# Stage 2: yipd +FROM alpine:latest AS yipd + +RUN apk add --no-cache ca-certificates libstdc++ + +COPY --from=builder /usr/local/bin/yipd /usr/local/bin/yipd +COPY --from=builder /usr/local/bin/yip-ca /usr/local/bin/yip-ca + +RUN mkdir -p /etc/yip /run/yipd + +VOLUME ["/etc/yip", "/run/yipd"] + +ENTRYPOINT ["/usr/local/bin/yipd"] +CMD ["/etc/yip/yipd.conf"] + +# Stage 3: yip-rendezvous +FROM alpine:latest AS yip-rendezvous + +RUN apk add --no-cache ca-certificates libstdc++ + +COPY --from=builder /usr/local/bin/yip-rendezvous /usr/local/bin/yip-rendezvous +COPY --from=builder /usr/local/bin/yip-ca /usr/local/bin/yip-ca + +ENTRYPOINT ["/usr/local/bin/yip-rendezvous"] diff --git a/README.md b/README.md index 6666dc7c..6160f84e 100644 --- a/README.md +++ b/README.md @@ -117,13 +117,13 @@ All benchmark charts are generated deterministically from verified testbed data. -*Two 1-vCPU Debian 13 VPSes, ~55.054 ms apart, 2026-10-22. yip trades raw clean-path throughput for loss resilience — see the FEC row.* +*Two 1-vCPU Debian 13 VPSes, ~60.940 ms apart, 2026-10-22. yip trades raw clean-path throughput for loss resilience — see the FEC row.* | | TCP 1-stream | UDP loss (200M) | effective loss @10% injected | |---|--:|--:|--:| -| **yip** | 182.2 Mbit/s | 0.00% | 1% | -| **WireGuard** | 354.3 Mbit/s | 0.00% | 7% | -| **OpenVPN** | 24.1 Mbit/s | 0.96% | 10% | +| **yip** | 227.0 Mbit/s | 0.00% | 0% | +| **WireGuard** | 412.4 Mbit/s | 0.00% | 9% | +| **OpenVPN** | 26.0 Mbit/s | 0.00% | 3% | @@ -230,7 +230,60 @@ To guarantee that test suites verify rigorous behavioral semantics rather than s ## Quick Start -### 1. Build from Source +### 1. 1-Line Quick Start (Recommended) + +Install the latest pre-compiled static musl binary (`x86_64` or `aarch64`) with automated SHA-256 verification and init service registration: + +```bash +curl -fsSL https://raw.githubusercontent.com/femboyisp/yip/main/install.sh | sh +``` + +The POSIX installer places `yipd`, `yip-ca`, and `yip-rendezvous` into `/usr/local/bin`, creates `/etc/yip`, and configures your init manager (`systemd`, `OpenRC`, or `runit`). + +#### Alternative Installation Methods + +
+cargo-binstall + +Fetch pre-compiled static binaries directly via cargo without compiling from source: + +```bash +cargo binstall yipd +``` +
+ +
+Docker Container + +Run the official lightweight musl container image: + +```bash +docker run -d --restart=always --privileged --net=host -v /etc/yip:/etc/yip -v /run/yipd:/run/yipd ghcr.io/femboyisp/yipd:latest +``` +
+ +
+Pre-built Static Binaries (GitHub Releases) + +Download standalone statically linked musl binaries with zero runtime dependencies directly from [GitHub Releases](https://github.com/femboyisp/yip/releases): +- `yip-v0.1.0-x86_64-unknown-linux-musl.tar.gz` +- `yip-v0.1.0-aarch64-unknown-linux-musl.tar.gz` + +Every release includes detached `SHA256SUMS` for integrity verification. +
+ +
+Production Init Services (contrib/) + +Hardened init service definitions with privilege separation are available in [`contrib/`](contrib/): +- **systemd**: [`contrib/systemd/yipd.service`](contrib/systemd/yipd.service) and [`contrib/systemd/yip-rendezvous.service`](contrib/systemd/yip-rendezvous.service) with `LimitMEMLOCK=infinity`, `CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_BPF CAP_IPC_LOCK`, `DeviceAllow=/dev/net/tun rw`, and `ProtectSystem=strict`. +- **OpenRC**: [`contrib/openrc/yipd`](contrib/openrc/yipd) and [`contrib/openrc/yip-rendezvous`](contrib/openrc/yip-rendezvous) for Alpine Linux and Gentoo. +- **runit**: [`contrib/runit/yipd/`](contrib/runit/yipd/) and [`contrib/runit/yip-rendezvous/`](contrib/runit/yip-rendezvous/) for Void Linux and minimal container environments. +
+ +--- + +### 2. Build from Source Requires a stable Rust toolchain (`1.85+`), `cmake`, and a C toolchain (for BoringSSL linking): @@ -240,15 +293,18 @@ cd yip cargo build --release --workspace ``` -### 2. Generate Keypair & Start Node +--- + +### 3. Generate Keypair & Start Node ```sh # Generate identity keypair -cargo run --release -p yipd -- --genkey > node.key +yipd --genkey > node.key +# (Or if building from source: cargo run --release -p yipd -- --genkey > node.key) # Copy example config and run cp example.config my-node.config -sudo target/release/yipd my-node.config +sudo yipd my-node.config ``` For two-node tunnels, mesh setups, NAT configuration, and DAITA traffic analysis defense (`traffic_defense = balanced`), consult the [User Guide](docs/user-guide.md) and [Configuration Reference](docs/configuration.md). diff --git a/ROADMAP.md b/ROADMAP.md index c28cfd24..bd769c96 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -54,11 +54,11 @@ maintainer. intermediate heap allocations on data-plane framing. - **Data-plane windowed loss, 8-contender harness & bench polish (#11)** — exact per-class windowed loss fractions ($L_{\text{class}} = M_{\text{class}} / S_{\text{class}}$ in $(H_{\text{prev}}..H_{\text{curr}}]$), 4096-entry synchronized loss detection window matching sent-log capacity, bounded spurious NACK drop invariants, 8-contender comparison matrix and netns runners (yip, WireGuard, AmneziaWG, OpenVPN, n2n, ZeroTier, Tailscale, NetBird), dual TCP & UDP-under-loss evaluation, and configurable 1000-ping variance tightening. - **Traffic-analysis defense (DAITA & AmneziaWG recipes, #11 / Sub-project #4)** — DAITA discrete packet size bucketing (`round_to_bucket`), stochastic decoy bursts with zero-allocation wire generation (`obf_scratch`), bounded egress micro-jitter queue (up to 1,024 packets with flush-oldest overrun protection), and event-loop poll timeout clamping (`next_scheduled_egress_delay_ms`). +- **Zero-friction distribution & packaging (#147)** — standalone, statically linked musl binaries (`x86_64-unknown-linux-musl` and `aarch64-unknown-linux-musl`) with zero dynamic runtime dependencies, multi-arch GitHub Actions and Forgejo automated release pipelines generating tarballs with detached `SHA256SUMS`, production multi-stage Dockerfile (`ghcr.io/femboyisp/yipd` and `yip-rendezvous`), turnkey POSIX `install.sh` with architecture detection, checksum verification, and init auto-detection, hardened multi-init service suite in `contrib/` (systemd with `LimitMEMLOCK=infinity`, capability bounding, and strict sandboxing; OpenRC; and runit), and `cargo-binstall` package metadata integration. ## Now - **Multi-platform native drivers** — macOS (NetworkExtension / utun) and Windows (Wintun). -- **Zero-friction distribution & packaging (#147)** — pre-built static musl binaries, hardened systemd service unit, and container/cargo-binstall support. ## Next diff --git a/assets/bench-fec-recovery.svg b/assets/bench-fec-recovery.svg index afd6e6bf..a82ccf9a 100644 --- a/assets/bench-fec-recovery.svg +++ b/assets/bench-fec-recovery.svg @@ -45,35 +45,35 @@ 10% Injected Network Loss (netem) Effective Ping Packet Loss % - - + + - - + + - - + + - - - - + + + + - - - - - + + + + + - + - - - 7% effective loss - - 1% loss (90% recovered) + + + 9% effective loss + + 0% loss (100% recovered) Cauchy RS GF(256): 100% recovery before kernel TCP timeout diff --git a/assets/bench-throughput-loss.svg b/assets/bench-throughput-loss.svg index 2a07f148..bc8caedd 100644 --- a/assets/bench-throughput-loss.svg +++ b/assets/bench-throughput-loss.svg @@ -19,8 +19,8 @@ OpenVPN - - 98× to 133× higher throughput under loss + + 57× to 95× higher throughput under loss 0.5 @@ -53,42 +53,42 @@ 10% Netem Injected Packet Loss TCP Goodput (Mbit/s, log scale) - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - 146.0M - - 133× advantage (146.0 vs 1.1 Mbit/s) - - 48.8 Mbit/s + + + + + + + 123.4M + + 95× advantage (123.4 vs 1.3 Mbit/s) + + 28.7 Mbit/s 0.5 Mbit/s - - + + - - 98× higher (48.8 vs 0.5) + + 57× higher (28.7 vs 0.5) NOTE Systematic Reed–Solomon FEC trades minor clean-path overhead for near-complete immunity against loss. diff --git a/bin/yip-ca/Cargo.toml b/bin/yip-ca/Cargo.toml index ea0105e0..2b09e97e 100644 --- a/bin/yip-ca/Cargo.toml +++ b/bin/yip-ca/Cargo.toml @@ -5,6 +5,11 @@ edition.workspace = true license.workspace = true repository.workspace = true +[package.metadata.cargo-binstall] +pkg-url = "{ repo }/releases/download/v{ version }/yip-v{ version }-{ target }.tar.gz" +bin-dir = "yip-v{ version }-{ target }/{ bin }" +pkg-fmt = "tgz" + [dependencies] yip-membership = { path = "../../crates/yip-membership" } ed25519-dalek = { version = "3", features = ["rand_core"] } diff --git a/bin/yip-rendezvous/Cargo.toml b/bin/yip-rendezvous/Cargo.toml index 673aff7a..539ceecc 100644 --- a/bin/yip-rendezvous/Cargo.toml +++ b/bin/yip-rendezvous/Cargo.toml @@ -5,6 +5,11 @@ edition.workspace = true license.workspace = true repository.workspace = true +[package.metadata.cargo-binstall] +pkg-url = "{ repo }/releases/download/v{ version }/yip-v{ version }-{ target }.tar.gz" +bin-dir = "yip-v{ version }-{ target }/{ bin }" +pkg-fmt = "tgz" + [[bin]] name = "yip-rendezvous" path = "src/main.rs" diff --git a/bin/yip-rendezvous/src/main.rs b/bin/yip-rendezvous/src/main.rs index f9b22fbd..30cef8a8 100644 --- a/bin/yip-rendezvous/src/main.rs +++ b/bin/yip-rendezvous/src/main.rs @@ -229,7 +229,7 @@ fn vpn_port_warning(port: u16) -> Option { fn usage_exit() -> ! { eprintln!( - "usage: yip-rendezvous [--obf-psk ] \ + "usage: yip-rendezvous [--listen-udp] [--obf-psk ] \ [--listen-tcp [--tls-cert --tls-key ] [--decoy ] \ [--reality-dest --reality-private-key \ [--reality-short-id ]... [--reality-server-name ]... \ @@ -283,10 +283,34 @@ async fn main() -> std::io::Result<()> { while let Some(arg) = args.next() { match arg.as_str() { + "--help" | "-h" => { + println!( + "usage: yip-rendezvous [--listen-udp] [--obf-psk ] \ + [--listen-tcp [--tls-cert --tls-key ] [--decoy ] \ + [--reality-dest --reality-private-key \ + [--reality-short-id ]... [--reality-server-name ]... \ + [--reality-cert-refresh-secs ] [--reality-cert-max-stale-secs ] \ + [--reality-replay-max-bucket ] [--reality-max-inflight ]]] \ + [--roots --network-id ]\n\ + (--reality-dest supersedes --decoy when both are set; --tls-cert/--tls-key are \ + optional when --reality-dest is set — REALITY forges its own per-SNI cert; \ + --roots/--network-id are both-or-neither and switch the server into mesh mode, \ + requiring every registration to carry a verified signed record)\n\ + e.g. 0.0.0.0:51821" + ); + return Ok(()); + } "--version" | "-V" => { println!("yip-rendezvous {}", env!("CARGO_PKG_VERSION")); return Ok(()); } + "--listen-udp" => { + let Some(v) = args.next() else { + eprintln!("--listen-udp requires an address argument"); + std::process::exit(2); + }; + listen = Some(v); + } "--obf-psk" => { let Some(hex) = args.next() else { eprintln!("--obf-psk requires a 64-char hex argument"); diff --git a/bin/yip-rendezvous/tests/smoke.rs b/bin/yip-rendezvous/tests/smoke.rs index af2d72c8..2ac36edd 100644 --- a/bin/yip-rendezvous/tests/smoke.rs +++ b/bin/yip-rendezvous/tests/smoke.rs @@ -276,6 +276,19 @@ fn register_lookup_relay_over_udp_with_obf_psk() { let _ = server.wait(); } +#[test] +fn server_accepts_listen_udp_flag() { + let listen = format!("127.0.0.1:{}", free_udp_port()); + let mut server = Command::new(env!("CARGO_BIN_EXE_yip-rendezvous")) + .arg("--listen-udp") + .arg(&listen) + .spawn() + .expect("spawn server with --listen-udp"); + wait_until_listening(&listen, None); + let _ = server.kill(); + let _ = server.wait(); +} + /// Local hex decode mirroring the binary's own (unexported) `hex_to_32`, just /// enough for this test to derive the same key it passes on the command line. fn hex_decode(hex: &str) -> [u8; 32] { diff --git a/bin/yipd/Cargo.toml b/bin/yipd/Cargo.toml index acb0950c..ac9186e9 100644 --- a/bin/yipd/Cargo.toml +++ b/bin/yipd/Cargo.toml @@ -5,6 +5,11 @@ edition.workspace = true license.workspace = true repository.workspace = true +[package.metadata.cargo-binstall] +pkg-url = "{ repo }/releases/download/v{ version }/yip-v{ version }-{ target }.tar.gz" +bin-dir = "yip-v{ version }-{ target }/{ bin }" +pkg-fmt = "tgz" + [dependencies] yip-io = { path = "../../crates/yip-io" } yip-wire = { path = "../../crates/yip-wire" } diff --git a/bin/yipd/src/control.rs b/bin/yipd/src/control.rs index 5f33bebf..c8637dfe 100644 --- a/bin/yipd/src/control.rs +++ b/bin/yipd/src/control.rs @@ -340,11 +340,11 @@ pub fn dispatch_cmd( } } -pub const DEFAULT_CONTROL_SOCKET: &str = "/run/yipd.sock"; +pub const DEFAULT_CONTROL_SOCKET: &str = "/run/yipd/yip.sock"; pub const FALLBACK_CONTROL_SOCKET: &str = "/tmp/yipd.sock"; /// Resolve the control socket path from an optional override, falling back to -/// `/run/yipd.sock` if present, or `/tmp/yipd.sock`. +/// `/run/yipd/yip.sock` if present, or `/tmp/yipd.sock`. pub fn resolve_control_socket(override_path: Option<&Path>) -> PathBuf { if let Some(p) = override_path { return p.to_path_buf(); @@ -1014,7 +1014,7 @@ mod tests { let custom = Path::new("/tmp/custom_yipd.sock"); assert_eq!(resolve_control_socket(Some(custom)), custom); let default = resolve_control_socket(None); - assert!(default.to_str().unwrap().contains("yipd.sock")); + assert!(default.to_str().unwrap().contains("yip.sock")); } #[test] diff --git a/contrib/openrc/yip-rendezvous b/contrib/openrc/yip-rendezvous new file mode 100755 index 00000000..3f11f841 --- /dev/null +++ b/contrib/openrc/yip-rendezvous @@ -0,0 +1,19 @@ +#!/sbin/openrc-run +# OpenRC service script for yip-rendezvous + +name="yip-rendezvous" +description="yip rendezvous & relay server" + +command="/usr/local/bin/yip-rendezvous" +command_args="${YIP_RENDEZVOUS_ARGS:---listen-udp 0.0.0.0:51820}" +supervisor="supervise-daemon" +capabilities="^cap_net_bind_service" + +depend() { + need net + use dns +} + +start_pre() { + checkpath -d -m 0755 /run/yip-rendezvous +} diff --git a/contrib/openrc/yipd b/contrib/openrc/yipd new file mode 100755 index 00000000..b4b0f0c6 --- /dev/null +++ b/contrib/openrc/yipd @@ -0,0 +1,26 @@ +#!/sbin/openrc-run +# OpenRC service script for yipd + +name="yipd" +description="yip high-performance obfuscated mesh VPN daemon" + +command="/usr/local/bin/yipd" +command_args="${YIPD_CONF:-/etc/yip/yipd.conf}" +supervisor="supervise-daemon" +capabilities="^cap_net_admin,^cap_net_raw,^cap_bpf,^cap_ipc_lock" + +rc_ulimit="-l unlimited" + +depend() { + need net + after firewall + use dns +} + +start_pre() { + checkpath -d -m 0755 /run/yipd + if [ ! -f "${command_args}" ]; then + eerror "Config file ${command_args} not found!" + return 1 + fi +} diff --git a/contrib/runit/yip-rendezvous/log/run b/contrib/runit/yip-rendezvous/log/run new file mode 100755 index 00000000..fc56c2c6 --- /dev/null +++ b/contrib/runit/yip-rendezvous/log/run @@ -0,0 +1,8 @@ +#!/bin/sh +exec 2>&1 + +LOGDIR="/var/log/yip-rendezvous" +mkdir -p "$LOGDIR" +chmod 0750 "$LOGDIR" + +exec svlogd -tt "$LOGDIR" diff --git a/contrib/runit/yip-rendezvous/run b/contrib/runit/yip-rendezvous/run new file mode 100755 index 00000000..f0087d3a --- /dev/null +++ b/contrib/runit/yip-rendezvous/run @@ -0,0 +1,9 @@ +#!/bin/sh +exec 2>&1 + +ulimit -l unlimited 2>/dev/null || true + +mkdir -p /run/yip-rendezvous +chmod 0755 /run/yip-rendezvous + +exec /usr/local/bin/yip-rendezvous --listen-udp 0.0.0.0:51820 diff --git a/contrib/runit/yipd/log/run b/contrib/runit/yipd/log/run new file mode 100755 index 00000000..bdea3d1f --- /dev/null +++ b/contrib/runit/yipd/log/run @@ -0,0 +1,8 @@ +#!/bin/sh +exec 2>&1 + +LOGDIR="/var/log/yipd" +mkdir -p "$LOGDIR" +chmod 0750 "$LOGDIR" + +exec svlogd -tt "$LOGDIR" diff --git a/contrib/runit/yipd/run b/contrib/runit/yipd/run new file mode 100755 index 00000000..5d0e6ba2 --- /dev/null +++ b/contrib/runit/yipd/run @@ -0,0 +1,11 @@ +#!/bin/sh +exec 2>&1 + +ulimit -l unlimited 2>/dev/null || true + +mkdir -p /run/yipd +chmod 0755 /run/yipd + +CONF="${YIPD_CONF:-/etc/yip/yipd.conf}" + +exec /usr/local/bin/yipd "$CONF" diff --git a/contrib/systemd/yip-rendezvous.service b/contrib/systemd/yip-rendezvous.service new file mode 100644 index 00000000..2e00e98b --- /dev/null +++ b/contrib/systemd/yip-rendezvous.service @@ -0,0 +1,27 @@ +[Unit] +Description=yip rendezvous & relay server +After=network.target network-online.target +Wants=network-online.target +Documentation=https://github.com/femboyisp/yip + +[Service] +Type=simple +ExecStart=/usr/local/bin/yip-rendezvous --listen-udp 0.0.0.0:51820 +Restart=always +RestartSec=3s + +# Privileges and capabilities +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE + +# Sandboxing and filesystem protection +ProtectSystem=strict +ProtectHome=true +NoNewPrivileges=true +PrivateTmp=true + +# Runtime directory +RuntimeDirectory=yip-rendezvous + +[Install] +WantedBy=multi-user.target diff --git a/contrib/systemd/yipd.service b/contrib/systemd/yipd.service new file mode 100644 index 00000000..9335c070 --- /dev/null +++ b/contrib/systemd/yipd.service @@ -0,0 +1,39 @@ +[Unit] +Description=yip high-performance obfuscated mesh VPN daemon +After=network.target network-online.target +Wants=network-online.target +Documentation=https://github.com/femboyisp/yip + +[Service] +Type=simple +ExecStart=/usr/local/bin/yipd /etc/yip/yipd.conf +Restart=always +RestartSec=3s + +# Privileges and capabilities +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_BPF CAP_IPC_LOCK +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_BPF CAP_IPC_LOCK +LimitMEMLOCK=infinity + +# Device permissions +DeviceAllow=/dev/net/tun rw + +# Sandboxing and filesystem protection +ProtectSystem=strict +ProtectHome=true +NoNewPrivileges=true +PrivateTmp=true +ProtectControlGroups=true +ProtectKernelModules=true +ProtectKernelLogs=true +RestrictRealtime=true +LockPersonality=true +MemoryDenyWriteExecute=true +RestrictNamespaces=false + +# Runtime and configuration directories +RuntimeDirectory=yipd +ConfigurationDirectory=yip + +[Install] +WantedBy=multi-user.target diff --git a/crates/yip-bench/RESULTS.md b/crates/yip-bench/RESULTS.md index 84e26919..8c03c472 100644 --- a/crates/yip-bench/RESULTS.md +++ b/crates/yip-bench/RESULTS.md @@ -740,8 +740,8 @@ Regime F delivers: ### Conditions - **Hosts:** `EC2 us-east-1 (t3.small)` ↔ `EC2 us-west-2 (t3.small)` (two 1-vCPU / ~1 GB Debian 13 VPSes, kernel 6.1.0-54-cloud-amd64) -- **Baseline path:** ~55.054 ms RTT, single Gbit links -- **Date / commit:** 2026-10-22T02:47:23Z / `7acce84` +- **Baseline path:** ~60.940 ms RTT, single Gbit links +- **Date / commit:** 2026-10-22T05:21:23Z / `405450c` - **Host tuning (applied equally, all tunnels):** `net.core.rmem_max=net.core.wmem_max=16 MiB` (so yipd's userspace 4 MiB socket buffers take effect; in-kernel WireGuard is unaffected) - **All tunnels:** UDP outer transport, MTU 1380; yip `transport=raw` (epoll driver — kernel 6.12 has no io_uring multishot recv, #25); WireGuard kernel module; OpenVPN static-key AES-256-GCM @@ -751,10 +751,10 @@ Regime F delivers: | tunnel | TCP 1-stream | TCP 4-stream | UDP goodput | UDP loss | idle RTT | RTT under load | CPU (A, 1 vCPU) | |---|--:|--:|--:|--:|--:|--:|--:| -| baseline (no tunnel) | 428.3 Mbit/s | 1010.7 Mbit/s | 200.0 Mbit/s | 0.00% | 54.948 ms | 54.920 ms | 4.0% | -| yip | 182.2 Mbit/s | 323.0 Mbit/s | 200.0 Mbit/s | 0.00% | 57.619 ms | 61.891 ms | 15.8% | -| WireGuard | 354.3 Mbit/s | 870.7 Mbit/s | 200.0 Mbit/s | 0.00% | 55.266 ms | 55.414 ms | 14.0% | -| OpenVPN | 24.1 Mbit/s | 27.5 Mbit/s | 200.0 Mbit/s | 0.96% | 55.032 ms | 55.349 ms | 4.2% | +| baseline (no tunnel) | 398.6 Mbit/s | 908.6 Mbit/s | 200.0 Mbit/s | 0.00% | 60.886 ms | 60.939 ms | 4.0% | +| yip | 227.0 Mbit/s | 355.7 Mbit/s | 200.0 Mbit/s | 0.00% | 58.956 ms | 60.299 ms | 12.2% | +| WireGuard | 412.4 Mbit/s | 1640.9 Mbit/s | 200.0 Mbit/s | 0.00% | 56.662 ms | 56.787 ms | 13.3% | +| OpenVPN | 26.0 Mbit/s | 33.6 Mbit/s | 200.0 Mbit/s | 0.00% | 54.933 ms | 55.574 ms | 4.7% | ### Loss resilience (netem injected on the A→B outer path) @@ -763,20 +763,20 @@ Effective end-to-end loss (ping) — lower is better; yip's FEC recovers what th | injected loss | baseline (no tunnel) | yip | WireGuard | OpenVPN | |---|--:|--:|--:|--:| | 0% | 0% | 0% | 0% | 0% | -| 1% | 0% | 0% | 2% | 1% | -| 3% | 4% | 1% | 1% | 3% | -| 5% | 5% | 0% | 6% | 9% | -| 10% | 16% | 1% | 7% | 10% | +| 1% | 3% | 0% | 1% | 1% | +| 3% | 3% | 0% | 0% | 2% | +| 5% | 5% | 0% | 2% | 4% | +| 10% | 13% | 0% | 9% | 3% | TCP goodput under the same injected loss (Mbit/s): | injected loss | baseline (no tunnel) | yip | WireGuard | OpenVPN | |---|--:|--:|--:|--:| -| 0% | 426.0 | 201.6 | 409.4 | 18.3 | -| 1% | 4.4 | 165.9 | 2.4 | 3.0 | -| 3% | 3.7 | 146.0 | 1.1 | 1.0 | -| 5% | 1.2 | 89.5 | 0.8 | 0.7 | -| 10% | 0.6 | 48.8 | 0.5 | 0.4 | +| 0% | 397.7 | 75.1 | 412.1 | 21.7 | +| 1% | 3.9 | 244.7 | 2.2 | 2.3 | +| 3% | 2.0 | 123.4 | 1.3 | 1.1 | +| 5% | 1.3 | 104.1 | 0.7 | 0.9 | +| 10% | 0.8 | 28.7 | 0.5 | 0.5 | ### Reproduce ```sh diff --git a/crates/yip-bench/wan/results/20261022T052109Z/wan.csv b/crates/yip-bench/wan/results/20261022T052109Z/wan.csv new file mode 100644 index 00000000..664264ab --- /dev/null +++ b/crates/yip-bench/wan/results/20261022T052109Z/wan.csv @@ -0,0 +1,73 @@ +ts,host_pair,commit,tunnel,profile,param,metric,value,conditions +2026-10-22T05:21:23Z,54.152.154.212->34.220.241.114,405450c,baseline,tcp_throughput,streams=1,mbit_s,398.6,clean +2026-10-22T05:21:36Z,54.152.154.212->34.220.241.114,405450c,baseline,tcp_throughput,streams=4,mbit_s,908.6,clean +2026-10-22T05:21:47Z,54.152.154.212->34.220.241.114,405450c,baseline,udp_throughput,offered=200M,mbit_s,200.0,clean +2026-10-22T05:21:47Z,54.152.154.212->34.220.241.114,405450c,baseline,udp_jitter,offered=200M,ms,0.004,clean +2026-10-22T05:21:47Z,54.152.154.212->34.220.241.114,405450c,baseline,udp_loss,offered=200M,pct,0.00,clean +2026-10-22T05:22:03Z,54.152.154.212->34.220.241.114,405450c,baseline,latency,idle,rtt_ms,60.886,clean +2026-10-22T05:22:03Z,54.152.154.212->34.220.241.114,405450c,baseline,latency,loaded,rtt_ms,60.939,saturated +2026-10-22T05:22:09Z,54.152.154.212->34.220.241.114,405450c,baseline,cpu_used,tcp_saturate,pct,4.0,sender_host_A_1vcpu +2026-10-22T05:22:28Z,54.152.154.212->34.220.241.114,405450c,baseline,loss_effective,injected=0pct,pct,0,netem_0pct +2026-10-22T05:22:28Z,54.152.154.212->34.220.241.114,405450c,baseline,loss_goodput,injected=0pct,mbit_s,397.7,netem_0pct +2026-10-22T05:22:46Z,54.152.154.212->34.220.241.114,405450c,baseline,loss_effective,injected=1pct,pct,3,netem_1pct +2026-10-22T05:22:46Z,54.152.154.212->34.220.241.114,405450c,baseline,loss_goodput,injected=1pct,mbit_s,3.9,netem_1pct +2026-10-22T05:23:05Z,54.152.154.212->34.220.241.114,405450c,baseline,loss_effective,injected=3pct,pct,3,netem_3pct +2026-10-22T05:23:05Z,54.152.154.212->34.220.241.114,405450c,baseline,loss_goodput,injected=3pct,mbit_s,2.0,netem_3pct +2026-10-22T05:23:24Z,54.152.154.212->34.220.241.114,405450c,baseline,loss_effective,injected=5pct,pct,5,netem_5pct +2026-10-22T05:23:24Z,54.152.154.212->34.220.241.114,405450c,baseline,loss_goodput,injected=5pct,mbit_s,1.3,netem_5pct +2026-10-22T05:23:43Z,54.152.154.212->34.220.241.114,405450c,baseline,loss_effective,injected=10pct,pct,13,netem_10pct +2026-10-22T05:23:43Z,54.152.154.212->34.220.241.114,405450c,baseline,loss_goodput,injected=10pct,mbit_s,0.8,netem_10pct +2026-10-22T05:23:56Z,54.152.154.212->34.220.241.114,405450c,yip,tcp_throughput,streams=1,mbit_s,227.0,clean +2026-10-22T05:24:08Z,54.152.154.212->34.220.241.114,405450c,yip,tcp_throughput,streams=4,mbit_s,355.7,clean +2026-10-22T05:24:19Z,54.152.154.212->34.220.241.114,405450c,yip,udp_throughput,offered=200M,mbit_s,200.0,clean +2026-10-22T05:24:19Z,54.152.154.212->34.220.241.114,405450c,yip,udp_jitter,offered=200M,ms,0.007,clean +2026-10-22T05:24:19Z,54.152.154.212->34.220.241.114,405450c,yip,udp_loss,offered=200M,pct,0.00,clean +2026-10-22T05:24:35Z,54.152.154.212->34.220.241.114,405450c,yip,latency,idle,rtt_ms,58.956,clean +2026-10-22T05:24:35Z,54.152.154.212->34.220.241.114,405450c,yip,latency,loaded,rtt_ms,60.299,saturated +2026-10-22T05:24:42Z,54.152.154.212->34.220.241.114,405450c,yip,cpu_used,tcp_saturate,pct,12.2,sender_host_A_1vcpu +2026-10-22T05:25:00Z,54.152.154.212->34.220.241.114,405450c,yip,loss_effective,injected=0pct,pct,0,netem_0pct +2026-10-22T05:25:00Z,54.152.154.212->34.220.241.114,405450c,yip,loss_goodput,injected=0pct,mbit_s,75.1,netem_0pct +2026-10-22T05:25:19Z,54.152.154.212->34.220.241.114,405450c,yip,loss_effective,injected=1pct,pct,0,netem_1pct +2026-10-22T05:25:19Z,54.152.154.212->34.220.241.114,405450c,yip,loss_goodput,injected=1pct,mbit_s,244.7,netem_1pct +2026-10-22T05:25:38Z,54.152.154.212->34.220.241.114,405450c,yip,loss_effective,injected=3pct,pct,0,netem_3pct +2026-10-22T05:25:38Z,54.152.154.212->34.220.241.114,405450c,yip,loss_goodput,injected=3pct,mbit_s,123.4,netem_3pct +2026-10-22T05:25:56Z,54.152.154.212->34.220.241.114,405450c,yip,loss_effective,injected=5pct,pct,0,netem_5pct +2026-10-22T05:25:56Z,54.152.154.212->34.220.241.114,405450c,yip,loss_goodput,injected=5pct,mbit_s,104.1,netem_5pct +2026-10-22T05:26:15Z,54.152.154.212->34.220.241.114,405450c,yip,loss_effective,injected=10pct,pct,0,netem_10pct +2026-10-22T05:26:15Z,54.152.154.212->34.220.241.114,405450c,yip,loss_goodput,injected=10pct,mbit_s,28.7,netem_10pct +2026-10-22T05:26:28Z,54.152.154.212->34.220.241.114,405450c,wireguard,tcp_throughput,streams=1,mbit_s,412.4,clean +2026-10-22T05:26:41Z,54.152.154.212->34.220.241.114,405450c,wireguard,tcp_throughput,streams=4,mbit_s,1640.9,clean +2026-10-22T05:26:51Z,54.152.154.212->34.220.241.114,405450c,wireguard,udp_throughput,offered=200M,mbit_s,200.0,clean +2026-10-22T05:26:51Z,54.152.154.212->34.220.241.114,405450c,wireguard,udp_jitter,offered=200M,ms,0.019,clean +2026-10-22T05:26:51Z,54.152.154.212->34.220.241.114,405450c,wireguard,udp_loss,offered=200M,pct,0.00,clean +2026-10-22T05:27:07Z,54.152.154.212->34.220.241.114,405450c,wireguard,latency,idle,rtt_ms,56.662,clean +2026-10-22T05:27:07Z,54.152.154.212->34.220.241.114,405450c,wireguard,latency,loaded,rtt_ms,56.787,saturated +2026-10-22T05:27:14Z,54.152.154.212->34.220.241.114,405450c,wireguard,cpu_used,tcp_saturate,pct,13.3,sender_host_A_1vcpu +2026-10-22T05:27:32Z,54.152.154.212->34.220.241.114,405450c,wireguard,loss_effective,injected=0pct,pct,0,netem_0pct +2026-10-22T05:27:32Z,54.152.154.212->34.220.241.114,405450c,wireguard,loss_goodput,injected=0pct,mbit_s,412.1,netem_0pct +2026-10-22T05:27:51Z,54.152.154.212->34.220.241.114,405450c,wireguard,loss_effective,injected=1pct,pct,1,netem_1pct +2026-10-22T05:27:51Z,54.152.154.212->34.220.241.114,405450c,wireguard,loss_goodput,injected=1pct,mbit_s,2.2,netem_1pct +2026-10-22T05:28:10Z,54.152.154.212->34.220.241.114,405450c,wireguard,loss_effective,injected=3pct,pct,0,netem_3pct +2026-10-22T05:28:10Z,54.152.154.212->34.220.241.114,405450c,wireguard,loss_goodput,injected=3pct,mbit_s,1.3,netem_3pct +2026-10-22T05:28:29Z,54.152.154.212->34.220.241.114,405450c,wireguard,loss_effective,injected=5pct,pct,2,netem_5pct +2026-10-22T05:28:29Z,54.152.154.212->34.220.241.114,405450c,wireguard,loss_goodput,injected=5pct,mbit_s,0.7,netem_5pct +2026-10-22T05:28:48Z,54.152.154.212->34.220.241.114,405450c,wireguard,loss_effective,injected=10pct,pct,9,netem_10pct +2026-10-22T05:28:48Z,54.152.154.212->34.220.241.114,405450c,wireguard,loss_goodput,injected=10pct,mbit_s,0.5,netem_10pct +2026-10-22T05:29:01Z,54.152.154.212->34.220.241.114,405450c,openvpn,tcp_throughput,streams=1,mbit_s,26.0,clean +2026-10-22T05:29:14Z,54.152.154.212->34.220.241.114,405450c,openvpn,tcp_throughput,streams=4,mbit_s,33.6,clean +2026-10-22T05:29:25Z,54.152.154.212->34.220.241.114,405450c,openvpn,udp_throughput,offered=200M,mbit_s,200.0,clean +2026-10-22T05:29:25Z,54.152.154.212->34.220.241.114,405450c,openvpn,udp_jitter,offered=200M,ms,0.031,clean +2026-10-22T05:29:25Z,54.152.154.212->34.220.241.114,405450c,openvpn,udp_loss,offered=200M,pct,0.00,clean +2026-10-22T05:29:40Z,54.152.154.212->34.220.241.114,405450c,openvpn,latency,idle,rtt_ms,54.933,clean +2026-10-22T05:29:40Z,54.152.154.212->34.220.241.114,405450c,openvpn,latency,loaded,rtt_ms,55.574,saturated +2026-10-22T05:29:47Z,54.152.154.212->34.220.241.114,405450c,openvpn,cpu_used,tcp_saturate,pct,4.7,sender_host_A_1vcpu +2026-10-22T05:30:05Z,54.152.154.212->34.220.241.114,405450c,openvpn,loss_effective,injected=0pct,pct,0,netem_0pct +2026-10-22T05:30:05Z,54.152.154.212->34.220.241.114,405450c,openvpn,loss_goodput,injected=0pct,mbit_s,21.7,netem_0pct +2026-10-22T05:30:24Z,54.152.154.212->34.220.241.114,405450c,openvpn,loss_effective,injected=1pct,pct,1,netem_1pct +2026-10-22T05:30:24Z,54.152.154.212->34.220.241.114,405450c,openvpn,loss_goodput,injected=1pct,mbit_s,2.3,netem_1pct +2026-10-22T05:30:44Z,54.152.154.212->34.220.241.114,405450c,openvpn,loss_effective,injected=3pct,pct,2,netem_3pct +2026-10-22T05:30:44Z,54.152.154.212->34.220.241.114,405450c,openvpn,loss_goodput,injected=3pct,mbit_s,1.1,netem_3pct +2026-10-22T05:31:02Z,54.152.154.212->34.220.241.114,405450c,openvpn,loss_effective,injected=5pct,pct,4,netem_5pct +2026-10-22T05:31:02Z,54.152.154.212->34.220.241.114,405450c,openvpn,loss_goodput,injected=5pct,mbit_s,0.9,netem_5pct +2026-10-22T05:31:21Z,54.152.154.212->34.220.241.114,405450c,openvpn,loss_effective,injected=10pct,pct,3,netem_10pct +2026-10-22T05:31:21Z,54.152.154.212->34.220.241.114,405450c,openvpn,loss_goodput,injected=10pct,mbit_s,0.5,netem_10pct diff --git a/crates/yip-bench/wan/results/history.csv b/crates/yip-bench/wan/results/history.csv index 6cc6fd37..4c397cda 100644 --- a/crates/yip-bench/wan/results/history.csv +++ b/crates/yip-bench/wan/results/history.csv @@ -863,3 +863,75 @@ ts,host_pair,commit,tunnel,profile,param,metric,value,conditions 2026-10-22T02:58:12Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),7acce84,openvpn,loss_goodput,injected=5pct,mbit_s,0.7,netem_5pct 2026-10-22T02:58:35Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),7acce84,openvpn,loss_effective,injected=10pct,pct,10,netem_10pct 2026-10-22T02:58:35Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),7acce84,openvpn,loss_goodput,injected=10pct,mbit_s,0.4,netem_10pct +2026-10-22T05:21:23Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,tcp_throughput,streams=1,mbit_s,398.6,clean +2026-10-22T05:21:36Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,tcp_throughput,streams=4,mbit_s,908.6,clean +2026-10-22T05:21:47Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,udp_throughput,offered=200M,mbit_s,200.0,clean +2026-10-22T05:21:47Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,udp_jitter,offered=200M,ms,0.004,clean +2026-10-22T05:21:47Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,udp_loss,offered=200M,pct,0.00,clean +2026-10-22T05:22:03Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,latency,idle,rtt_ms,60.886,clean +2026-10-22T05:22:03Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,latency,loaded,rtt_ms,60.939,saturated +2026-10-22T05:22:09Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,cpu_used,tcp_saturate,pct,4.0,sender_host_A_1vcpu +2026-10-22T05:22:28Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,loss_effective,injected=0pct,pct,0,netem_0pct +2026-10-22T05:22:28Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,loss_goodput,injected=0pct,mbit_s,397.7,netem_0pct +2026-10-22T05:22:46Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,loss_effective,injected=1pct,pct,3,netem_1pct +2026-10-22T05:22:46Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,loss_goodput,injected=1pct,mbit_s,3.9,netem_1pct +2026-10-22T05:23:05Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,loss_effective,injected=3pct,pct,3,netem_3pct +2026-10-22T05:23:05Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,loss_goodput,injected=3pct,mbit_s,2.0,netem_3pct +2026-10-22T05:23:24Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,loss_effective,injected=5pct,pct,5,netem_5pct +2026-10-22T05:23:24Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,loss_goodput,injected=5pct,mbit_s,1.3,netem_5pct +2026-10-22T05:23:43Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,loss_effective,injected=10pct,pct,13,netem_10pct +2026-10-22T05:23:43Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,baseline,loss_goodput,injected=10pct,mbit_s,0.8,netem_10pct +2026-10-22T05:23:56Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,tcp_throughput,streams=1,mbit_s,227.0,clean +2026-10-22T05:24:08Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,tcp_throughput,streams=4,mbit_s,355.7,clean +2026-10-22T05:24:19Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,udp_throughput,offered=200M,mbit_s,200.0,clean +2026-10-22T05:24:19Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,udp_jitter,offered=200M,ms,0.007,clean +2026-10-22T05:24:19Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,udp_loss,offered=200M,pct,0.00,clean +2026-10-22T05:24:35Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,latency,idle,rtt_ms,58.956,clean +2026-10-22T05:24:35Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,latency,loaded,rtt_ms,60.299,saturated +2026-10-22T05:24:42Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,cpu_used,tcp_saturate,pct,12.2,sender_host_A_1vcpu +2026-10-22T05:25:00Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,loss_effective,injected=0pct,pct,0,netem_0pct +2026-10-22T05:25:00Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,loss_goodput,injected=0pct,mbit_s,75.1,netem_0pct +2026-10-22T05:25:19Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,loss_effective,injected=1pct,pct,0,netem_1pct +2026-10-22T05:25:19Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,loss_goodput,injected=1pct,mbit_s,244.7,netem_1pct +2026-10-22T05:25:38Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,loss_effective,injected=3pct,pct,0,netem_3pct +2026-10-22T05:25:38Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,loss_goodput,injected=3pct,mbit_s,123.4,netem_3pct +2026-10-22T05:25:56Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,loss_effective,injected=5pct,pct,0,netem_5pct +2026-10-22T05:25:56Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,loss_goodput,injected=5pct,mbit_s,104.1,netem_5pct +2026-10-22T05:26:15Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,loss_effective,injected=10pct,pct,0,netem_10pct +2026-10-22T05:26:15Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,yip,loss_goodput,injected=10pct,mbit_s,28.7,netem_10pct +2026-10-22T05:26:28Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,tcp_throughput,streams=1,mbit_s,412.4,clean +2026-10-22T05:26:41Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,tcp_throughput,streams=4,mbit_s,1640.9,clean +2026-10-22T05:26:51Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,udp_throughput,offered=200M,mbit_s,200.0,clean +2026-10-22T05:26:51Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,udp_jitter,offered=200M,ms,0.019,clean +2026-10-22T05:26:51Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,udp_loss,offered=200M,pct,0.00,clean +2026-10-22T05:27:07Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,latency,idle,rtt_ms,56.662,clean +2026-10-22T05:27:07Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,latency,loaded,rtt_ms,56.787,saturated +2026-10-22T05:27:14Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,cpu_used,tcp_saturate,pct,13.3,sender_host_A_1vcpu +2026-10-22T05:27:32Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,loss_effective,injected=0pct,pct,0,netem_0pct +2026-10-22T05:27:32Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,loss_goodput,injected=0pct,mbit_s,412.1,netem_0pct +2026-10-22T05:27:51Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,loss_effective,injected=1pct,pct,1,netem_1pct +2026-10-22T05:27:51Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,loss_goodput,injected=1pct,mbit_s,2.2,netem_1pct +2026-10-22T05:28:10Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,loss_effective,injected=3pct,pct,0,netem_3pct +2026-10-22T05:28:10Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,loss_goodput,injected=3pct,mbit_s,1.3,netem_3pct +2026-10-22T05:28:29Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,loss_effective,injected=5pct,pct,2,netem_5pct +2026-10-22T05:28:29Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,loss_goodput,injected=5pct,mbit_s,0.7,netem_5pct +2026-10-22T05:28:48Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,loss_effective,injected=10pct,pct,9,netem_10pct +2026-10-22T05:28:48Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,wireguard,loss_goodput,injected=10pct,mbit_s,0.5,netem_10pct +2026-10-22T05:29:01Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,tcp_throughput,streams=1,mbit_s,26.0,clean +2026-10-22T05:29:14Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,tcp_throughput,streams=4,mbit_s,33.6,clean +2026-10-22T05:29:25Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,udp_throughput,offered=200M,mbit_s,200.0,clean +2026-10-22T05:29:25Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,udp_jitter,offered=200M,ms,0.031,clean +2026-10-22T05:29:25Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,udp_loss,offered=200M,pct,0.00,clean +2026-10-22T05:29:40Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,latency,idle,rtt_ms,54.933,clean +2026-10-22T05:29:40Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,latency,loaded,rtt_ms,55.574,saturated +2026-10-22T05:29:47Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,cpu_used,tcp_saturate,pct,4.7,sender_host_A_1vcpu +2026-10-22T05:30:05Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,loss_effective,injected=0pct,pct,0,netem_0pct +2026-10-22T05:30:05Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,loss_goodput,injected=0pct,mbit_s,21.7,netem_0pct +2026-10-22T05:30:24Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,loss_effective,injected=1pct,pct,1,netem_1pct +2026-10-22T05:30:24Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,loss_goodput,injected=1pct,mbit_s,2.3,netem_1pct +2026-10-22T05:30:44Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,loss_effective,injected=3pct,pct,2,netem_3pct +2026-10-22T05:30:44Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,loss_goodput,injected=3pct,mbit_s,1.1,netem_3pct +2026-10-22T05:31:02Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,loss_effective,injected=5pct,pct,4,netem_5pct +2026-10-22T05:31:02Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,loss_goodput,injected=5pct,mbit_s,0.9,netem_5pct +2026-10-22T05:31:21Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,loss_effective,injected=10pct,pct,3,netem_10pct +2026-10-22T05:31:21Z,EC2 us-east-1 (t3.small)->EC2 us-west-2 (t3.small),405450c,openvpn,loss_goodput,injected=10pct,mbit_s,0.5,netem_10pct diff --git a/crates/yip-bench/wan/results/readme-snippet.md b/crates/yip-bench/wan/results/readme-snippet.md index 3062f22e..9a88a0a1 100644 --- a/crates/yip-bench/wan/results/readme-snippet.md +++ b/crates/yip-bench/wan/results/readme-snippet.md @@ -1,8 +1,8 @@ -*Two 1-vCPU Debian 13 VPSes, ~55.054 ms apart, 2026-10-22. yip trades raw clean-path throughput for loss resilience — see the FEC row.* +*Two 1-vCPU Debian 13 VPSes, ~60.940 ms apart, 2026-10-22. yip trades raw clean-path throughput for loss resilience — see the FEC row.* | | TCP 1-stream | UDP loss (200M) | effective loss @10% injected | |---|--:|--:|--:| -| **yip** | 182.2 Mbit/s | 0.00% | 1% | -| **WireGuard** | 354.3 Mbit/s | 0.00% | 7% | -| **OpenVPN** | 24.1 Mbit/s | 0.96% | 10% | +| **yip** | 227.0 Mbit/s | 0.00% | 0% | +| **WireGuard** | 412.4 Mbit/s | 0.00% | 9% | +| **OpenVPN** | 26.0 Mbit/s | 0.00% | 3% | diff --git a/crates/yip-device/src/lib.rs b/crates/yip-device/src/lib.rs index 97778258..abba58dd 100644 --- a/crates/yip-device/src/lib.rs +++ b/crates/yip-device/src/lib.rs @@ -2,6 +2,11 @@ //! I/O lands in M4; this milestone fixes the public surface. #![deny(unsafe_op_in_unsafe_fn)] +#[cfg(target_env = "musl")] +type IoctlReq = libc::c_int; +#[cfg(not(target_env = "musl"))] +type IoctlReq = libc::c_ulong; + use std::io; use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; @@ -72,7 +77,7 @@ fn negotiate_offload(fd: std::os::fd::RawFd) -> Option { let full = unsafe { libc::ioctl( fd, - TUNSETOFFLOAD, + TUNSETOFFLOAD as IoctlReq, libc::c_ulong::from(TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6), ) }; @@ -81,7 +86,13 @@ fn negotiate_offload(fd: std::os::fd::RawFd) -> Option { } // SAFETY: same rationale as the previous TUNSETOFFLOAD call. - let csum_only = unsafe { libc::ioctl(fd, TUNSETOFFLOAD, libc::c_ulong::from(TUN_F_CSUM)) }; + let csum_only = unsafe { + libc::ioctl( + fd, + TUNSETOFFLOAD as IoctlReq, + libc::c_ulong::from(TUN_F_CSUM), + ) + }; if csum_only == 0 { return Some(OffloadMode::CsumOnly); } @@ -95,7 +106,7 @@ fn apply_offload(fd: std::os::fd::RawFd, mode: OffloadMode) -> Result<(), Device OffloadMode::CsumOnly => libc::c_ulong::from(TUN_F_CSUM), }; // SAFETY: `fd` is a valid open /dev/net/tun fd. TUNSETOFFLOAD takes its flags as an integer passed by value. - let rc = unsafe { libc::ioctl(fd, TUNSETOFFLOAD, flags) }; + let rc = unsafe { libc::ioctl(fd, TUNSETOFFLOAD as IoctlReq, flags) }; if rc != 0 { return Err(DeviceError::Io(io::Error::last_os_error())); } @@ -146,12 +157,12 @@ fn bring_up(ifname: &[u8; libc::IFNAMSIZ]) -> Result<(), DeviceError> { if sock_fd < 0 { return Err(DeviceError::Io(io::Error::last_os_error())); } - let rc_get = libc::ioctl(sock_fd, libc::SIOCGIFFLAGS, &raw mut req); + let rc_get = libc::ioctl(sock_fd, libc::SIOCGIFFLAGS as IoctlReq, &raw mut req); if rc_get == 0 { // FFI: libc exposes IFF_UP/IFF_RUNNING as c_int, but `ifreq.ifr_flags` // is c_short; the combined value (0x41) fits trivially. req.flags |= (libc::IFF_UP | libc::IFF_RUNNING) as libc::c_short; - let rc_set = libc::ioctl(sock_fd, libc::SIOCSIFFLAGS, &raw mut req); + let rc_set = libc::ioctl(sock_fd, libc::SIOCSIFFLAGS as IoctlReq, &raw mut req); libc::close(sock_fd); if rc_set != 0 { Some(io::Error::last_os_error()) @@ -192,7 +203,7 @@ fn set_device_mtu(ifname: &[u8; libc::IFNAMSIZ], mtu: u32) -> Result<(), DeviceE if sock_fd < 0 { return Err(DeviceError::Io(io::Error::last_os_error())); } - let rc = libc::ioctl(sock_fd, libc::SIOCSIFMTU, &req); + let rc = libc::ioctl(sock_fd, libc::SIOCSIFMTU as IoctlReq, &req); let e = if rc != 0 { Some(io::Error::last_os_error()) } else { @@ -321,7 +332,7 @@ impl TunTap { // TUNSETIFF; the fd is a freshly-opened /dev/net/tun. The kernel reads // `req` and writes back the resolved name into the same buffer, which // we own exclusively here. - let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF, &raw mut req) }; + let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) }; if rc != 0 { // Driver rejected IFF_VNET_HDR (or the type/name flags altogether). // `file` drops here, closing the fd; caller reopens plain. @@ -332,7 +343,13 @@ impl TunTap { let mut hdr_sz: libc::c_int = VNET_HDR_LEN as libc::c_int; // SAFETY: `file` is a valid open /dev/net/tun fd. TUNSETVNETHDRSZ takes an int pointer // specifying the virtio_net_hdr byte size. - let _ = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETVNETHDRSZ, &raw mut hdr_sz) }; + let _ = unsafe { + libc::ioctl( + file.as_raw_fd(), + TUNSETVNETHDRSZ as IoctlReq, + &raw mut hdr_sz, + ) + }; if let Some(_mode) = negotiate_offload(file.as_raw_fd()) { bring_up(&req.name)?; @@ -371,7 +388,7 @@ impl TunTap { // SAFETY: `req` is a correctly-sized, properly-initialized `ifreq` for TUNSETIFF; // the fd is a freshly-opened /dev/net/tun. The kernel reads `req` and writes back // the resolved name into the same buffer, which we own exclusively here. - let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF, &raw mut req) }; + let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) }; if rc != 0 { return Err(DeviceError::Io(io::Error::last_os_error())); } @@ -409,14 +426,20 @@ impl TunTap { }; // SAFETY: req0 is a correctly-sized ifreq for TUNSETIFF; file0 is a freshly-opened /dev/net/tun. - let rc = unsafe { libc::ioctl(file0.as_raw_fd(), TUNSETIFF, &raw mut req0) }; + let rc = unsafe { libc::ioctl(file0.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req0) }; if rc != 0 { return Ok(None); } let mut hdr_sz0: libc::c_int = VNET_HDR_LEN as libc::c_int; // SAFETY: file0 is a valid open /dev/net/tun fd. TUNSETVNETHDRSZ takes an int pointer. - let _ = unsafe { libc::ioctl(file0.as_raw_fd(), TUNSETVNETHDRSZ, &raw mut hdr_sz0) }; + let _ = unsafe { + libc::ioctl( + file0.as_raw_fd(), + TUNSETVNETHDRSZ as IoctlReq, + &raw mut hdr_sz0, + ) + }; let offload_mode = match negotiate_offload(file0.as_raw_fd()) { Some(mode) => mode, @@ -450,14 +473,20 @@ impl TunTap { }; // SAFETY: req is a correctly-sized ifreq for TUNSETIFF; file is a freshly-opened /dev/net/tun. - let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF, &raw mut req) }; + let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) }; if rc != 0 { return Err(DeviceError::Io(io::Error::last_os_error())); } let mut hdr_sz: libc::c_int = VNET_HDR_LEN as libc::c_int; // SAFETY: file is a valid open /dev/net/tun fd. TUNSETVNETHDRSZ takes an int pointer. - let _ = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETVNETHDRSZ, &raw mut hdr_sz) }; + let _ = unsafe { + libc::ioctl( + file.as_raw_fd(), + TUNSETVNETHDRSZ as IoctlReq, + &raw mut hdr_sz, + ) + }; apply_offload(file.as_raw_fd(), offload_mode)?; @@ -495,7 +524,7 @@ impl TunTap { }; // SAFETY: req0 is a correctly-sized ifreq for TUNSETIFF; file0 is a freshly-opened /dev/net/tun. - let rc = unsafe { libc::ioctl(file0.as_raw_fd(), TUNSETIFF, &raw mut req0) }; + let rc = unsafe { libc::ioctl(file0.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req0) }; if rc != 0 { return Err(DeviceError::Io(io::Error::last_os_error())); } @@ -527,7 +556,7 @@ impl TunTap { }; // SAFETY: req is a correctly-sized ifreq for TUNSETIFF; file is a freshly-opened /dev/net/tun. - let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF, &raw mut req) }; + let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) }; if rc != 0 { return Err(DeviceError::Io(io::Error::last_os_error())); } diff --git a/crates/yip-io/src/batch.rs b/crates/yip-io/src/batch.rs index 1bd95ec4..879fb594 100644 --- a/crates/yip-io/src/batch.rs +++ b/crates/yip-io/src/batch.rs @@ -96,18 +96,7 @@ impl BatchUdpSocket { iov_base: std::ptr::null_mut(), iov_len: 0, }; BATCH_SIZE]; - let mut msgs = [libc::mmsghdr { - msg_hdr: libc::msghdr { - msg_name: std::ptr::null_mut(), - msg_namelen: 0, - msg_iov: std::ptr::null_mut(), - msg_iovlen: 0, - msg_control: std::ptr::null_mut(), - msg_controllen: 0, - msg_flags: 0, - }, - msg_len: 0, - }; BATCH_SIZE]; + let mut msgs = [crate::empty_mmsghdr(); BATCH_SIZE]; for i in 0..BATCH_SIZE { iovecs[i].iov_base = buffers[i].as_mut_ptr().cast::(); @@ -130,7 +119,7 @@ impl BatchUdpSocket { self.fd, msgs.as_mut_ptr(), u32::try_from(BATCH_SIZE).expect("BATCH_SIZE fits u32"), - libc::MSG_DONTWAIT, + libc::MSG_DONTWAIT as crate::MsgFlags, std::ptr::null_mut(), ) }; @@ -190,18 +179,7 @@ impl BatchUdpSocket { iov_base: std::ptr::null_mut(), iov_len: 0, }; BATCH_SIZE]; - let mut msgs = [libc::mmsghdr { - msg_hdr: libc::msghdr { - msg_name: std::ptr::null_mut(), - msg_namelen: 0, - msg_iov: std::ptr::null_mut(), - msg_iovlen: 0, - msg_control: std::ptr::null_mut(), - msg_controllen: 0, - msg_flags: 0, - }, - msg_len: 0, - }; BATCH_SIZE]; + let mut msgs = [crate::empty_mmsghdr(); BATCH_SIZE]; for (i, &(payload, dst)) in packets[..count].iter().enumerate() { let (storage, addr_len) = crate::addr::std_to_sockaddr(dst); @@ -225,7 +203,7 @@ impl BatchUdpSocket { self.fd, msgs.as_mut_ptr(), u32::try_from(count).expect("count fits u32"), - libc::MSG_NOSIGNAL, + libc::MSG_NOSIGNAL as crate::MsgFlags, ) }; @@ -304,7 +282,7 @@ pub fn send_gso_superpacket( let cmsg_space = usize::try_from(unsafe { libc::CMSG_SPACE(std::mem::size_of::() as u32) }) .unwrap_or(control.len()); - msg.msg_controllen = cmsg_space; + msg.msg_controllen = cmsg_space as crate::CmsgLen; // SAFETY: msg points to valid control storage; write SOL_UDP / UDP_SEGMENT cmsg. unsafe { @@ -312,8 +290,8 @@ pub fn send_gso_superpacket( if !cmsg.is_null() { (*cmsg).cmsg_level = libc::SOL_UDP; (*cmsg).cmsg_type = libc::UDP_SEGMENT; - (*cmsg).cmsg_len = - usize::try_from(libc::CMSG_LEN(std::mem::size_of::() as u32)).unwrap_or(0); + (*cmsg).cmsg_len = usize::try_from(libc::CMSG_LEN(std::mem::size_of::() as u32)) + .unwrap_or(0) as crate::CmsgLen; let seg_ptr = libc::CMSG_DATA(cmsg).cast::(); *seg_ptr = segment_size; } diff --git a/crates/yip-io/src/lib.rs b/crates/yip-io/src/lib.rs index 4d98e9c5..86f0dfac 100644 --- a/crates/yip-io/src/lib.rs +++ b/crates/yip-io/src/lib.rs @@ -14,9 +14,32 @@ pub mod spsc; pub mod tun_offload; pub mod uring; +#[cfg(target_env = "musl")] +pub(crate) type MsgFlags = libc::c_uint; +#[cfg(not(target_env = "musl"))] +pub(crate) type MsgFlags = libc::c_int; + +#[cfg(target_env = "musl")] +pub(crate) type CmsgLen = libc::socklen_t; +#[cfg(not(target_env = "musl"))] +pub(crate) type CmsgLen = usize; + pub use addr::{sockaddr_to_std, std_to_sockaddr}; pub use bpf::{BpfFilterStatus, XdpRedirectFilter}; +#[inline] +pub(crate) fn empty_msghdr() -> libc::msghdr { + // SAFETY: msghdr contains only pointers, lengths, and integer flags, which + // are valid when all bytes are zeroed. + unsafe { std::mem::zeroed() } +} + +#[inline] +pub(crate) fn empty_mmsghdr() -> libc::mmsghdr { + // SAFETY: mmsghdr contains msghdr and msg_len, which are valid when zeroed. + unsafe { std::mem::zeroed() } +} + /// Maximum number of datagrams in a single batched send/recv call. pub const MAX_DATAGRAM_BATCH: usize = 64; @@ -189,18 +212,7 @@ impl DataPlaneIo for PlainIo { iov_base: std::ptr::null_mut(), iov_len: 0, }; MAX_DATAGRAM_BATCH]; - let mut msgs = [libc::mmsghdr { - msg_hdr: libc::msghdr { - msg_name: std::ptr::null_mut(), - msg_namelen: 0, - msg_iov: std::ptr::null_mut(), - msg_iovlen: 0, - msg_control: std::ptr::null_mut(), - msg_controllen: 0, - msg_flags: 0, - }, - msg_len: 0, - }; MAX_DATAGRAM_BATCH]; + let mut msgs = [empty_mmsghdr(); MAX_DATAGRAM_BATCH]; for (i, dg) in datagrams[..count].iter().enumerate() { // SAFETY: We cast a shared slice to a *mut c_void as required by the @@ -224,7 +236,7 @@ impl DataPlaneIo for PlainIo { self.socket.as_raw_fd(), msgs.as_mut_ptr(), u32::try_from(count).expect("count ≤ MAX_DATAGRAM_BATCH ≤ 64 fits u32"), - libc::MSG_NOSIGNAL, + libc::MSG_NOSIGNAL as MsgFlags, ) }; @@ -253,18 +265,7 @@ impl DataPlaneIo for PlainIo { iov_base: std::ptr::null_mut(), iov_len: 0, }; MAX_DATAGRAM_BATCH]; - let mut msgs = [libc::mmsghdr { - msg_hdr: libc::msghdr { - msg_name: std::ptr::null_mut(), - msg_namelen: 0, - msg_iov: std::ptr::null_mut(), - msg_iovlen: 0, - msg_control: std::ptr::null_mut(), - msg_controllen: 0, - msg_flags: 0, - }, - msg_len: 0, - }; MAX_DATAGRAM_BATCH]; + let mut msgs = [empty_mmsghdr(); MAX_DATAGRAM_BATCH]; for i in 0..count { // SAFETY: bufs[i] is a mutable slice owned by the caller and lives @@ -289,7 +290,7 @@ impl DataPlaneIo for PlainIo { self.socket.as_raw_fd(), msgs.as_mut_ptr(), u32::try_from(count).expect("count ≤ MAX_DATAGRAM_BATCH ≤ 64 fits u32"), - libc::MSG_WAITFORONE, + libc::MSG_WAITFORONE as MsgFlags, std::ptr::null_mut(), ) }; diff --git a/crates/yip-io/src/poll.rs b/crates/yip-io/src/poll.rs index e3e18892..4c8df45b 100644 --- a/crates/yip-io/src/poll.rs +++ b/crates/yip-io/src/poll.rs @@ -392,18 +392,7 @@ fn send_mmsg(udp_fd: RawFd, datagrams: &[EgressDatagram]) -> io::Result { iov_base: std::ptr::null_mut(), iov_len: 0, }; MAX_DATAGRAM_BATCH]; - let mut msgs = [libc::mmsghdr { - msg_hdr: libc::msghdr { - msg_name: std::ptr::null_mut(), - msg_namelen: 0, - msg_iov: std::ptr::null_mut(), - msg_iovlen: 0, - msg_control: std::ptr::null_mut(), - msg_controllen: 0, - msg_flags: 0, - }, - msg_len: 0, - }; MAX_DATAGRAM_BATCH]; + let mut msgs = [crate::empty_mmsghdr(); MAX_DATAGRAM_BATCH]; for (i, dg) in datagrams[..count].iter().enumerate() { let (storage, addr_len) = std_to_sockaddr(dg.dst); @@ -428,7 +417,7 @@ fn send_mmsg(udp_fd: RawFd, datagrams: &[EgressDatagram]) -> io::Result { udp_fd, msgs.as_mut_ptr(), u32::try_from(count).expect("count ≤ 64 fits u32"), - libc::MSG_NOSIGNAL, + libc::MSG_NOSIGNAL as crate::MsgFlags, ) }; if ret < 0 { @@ -476,7 +465,7 @@ fn send_gso_payload( if cmsg_space > control.len() { return Err(io::Error::other("GSO control buffer too small")); } - msg.msg_controllen = cmsg_space; + msg.msg_controllen = cmsg_space as crate::CmsgLen; // SAFETY: `msg` points to valid in-frame iovec/control storage; we write // exactly one SOL_UDP/UDP_SEGMENT cmsg (a u16 segment size) into `control`. @@ -487,8 +476,8 @@ fn send_gso_payload( } (*cmsg).cmsg_level = libc::SOL_UDP; (*cmsg).cmsg_type = libc::UDP_SEGMENT; - (*cmsg).cmsg_len = - usize::try_from(libc::CMSG_LEN(GSO_CONTROL_PAYLOAD_LEN)).expect("cmsg len fits usize"); + (*cmsg).cmsg_len = usize::try_from(libc::CMSG_LEN(GSO_CONTROL_PAYLOAD_LEN)) + .expect("cmsg len fits usize") as crate::CmsgLen; let seg_ptr = libc::CMSG_DATA(cmsg).cast::(); *seg_ptr = segment_size; } @@ -577,18 +566,7 @@ fn recv_mmsg( iov_base: std::ptr::null_mut(), iov_len: 0, }; MAX_DATAGRAM_BATCH]; - let mut msgs = [libc::mmsghdr { - msg_hdr: libc::msghdr { - msg_name: std::ptr::null_mut(), - msg_namelen: 0, - msg_iov: std::ptr::null_mut(), - msg_iovlen: 0, - msg_control: std::ptr::null_mut(), - msg_controllen: 0, - msg_flags: 0, - }, - msg_len: 0, - }; MAX_DATAGRAM_BATCH]; + let mut msgs = [crate::empty_mmsghdr(); MAX_DATAGRAM_BATCH]; for i in 0..count { // SAFETY: each iov_base/msg_name points to a distinct element of @@ -609,7 +587,7 @@ fn recv_mmsg( udp_fd, msgs.as_mut_ptr(), u32::try_from(count).expect("count ≤ 64 fits u32"), - libc::MSG_DONTWAIT, + libc::MSG_DONTWAIT as crate::MsgFlags, std::ptr::null_mut(), ) }; diff --git a/crates/yip-io/src/uring.rs b/crates/yip-io/src/uring.rs index 28f63286..e6dbf6ba 100644 --- a/crates/yip-io/src/uring.rs +++ b/crates/yip-io/src/uring.rs @@ -120,15 +120,7 @@ impl GsoSendContext { iov_base: std::ptr::null_mut(), iov_len: 0, }, - msg: libc::msghdr { - msg_name: std::ptr::null_mut(), - msg_namelen: 0, - msg_iov: std::ptr::null_mut(), - msg_iovlen: 0, - msg_control: std::ptr::null_mut(), - msg_controllen: 0, - msg_flags: 0, - }, + msg: crate::empty_msghdr(), control: [0_u8; GSO_CONTROL_SPACE], // SAFETY: `sockaddr_storage` is plain-old-data (integers/byte // arrays); the all-zero bit pattern is a valid value for it. @@ -177,7 +169,7 @@ impl GsoSendContext { if cmsg_space > self.control.len() { return Err(io::Error::other("GSO control buffer too small")); } - self.msg.msg_controllen = cmsg_space; + self.msg.msg_controllen = cmsg_space as crate::CmsgLen; self.msg.msg_flags = 0; self.set_destination(dst); @@ -191,8 +183,9 @@ impl GsoSendContext { } (*cmsg).cmsg_level = libc::SOL_UDP; (*cmsg).cmsg_type = libc::UDP_SEGMENT; - (*cmsg).cmsg_len = usize::try_from(libc::CMSG_LEN(GSO_CONTROL_PAYLOAD_LEN_U32)) + let cmsg_len: usize = usize::try_from(libc::CMSG_LEN(GSO_CONTROL_PAYLOAD_LEN_U32)) .map_err(|_| io::Error::other("cmsg len does not fit usize"))?; + (*cmsg).cmsg_len = cmsg_len as crate::CmsgLen; let segment_ptr = libc::CMSG_DATA(cmsg).cast::(); *segment_ptr = segment_size; } @@ -298,15 +291,11 @@ impl UringDriver { iov_base: slot.buf.as_mut_ptr().cast::(), iov_len: slot.buf.len(), }; - slot.msg = libc::msghdr { - msg_name: std::ptr::addr_of_mut!(slot.name).cast::(), - msg_namelen: namelen, - msg_iov: std::ptr::addr_of_mut!(slot.iov), - msg_iovlen: 1, - msg_control: std::ptr::null_mut(), - msg_controllen: 0, - msg_flags: 0, - }; + slot.msg = crate::empty_msghdr(); + slot.msg.msg_name = std::ptr::addr_of_mut!(slot.name).cast::(); + slot.msg.msg_namelen = namelen; + slot.msg.msg_iov = std::ptr::addr_of_mut!(slot.iov); + slot.msg.msg_iovlen = 1; } let mut in_flight = Vec::with_capacity(SEND_SLOTS); diff --git a/example.config b/example.config index 3814cdba..3e66b05e 100644 --- a/example.config +++ b/example.config @@ -193,3 +193,14 @@ endpoint=203.0.113.10:443 # # transport=tls # tls_sni=www.apple.com + + +# --------------------------------------------------------------------------- +# 7. CONTROL SOCKET (optional) +# --------------------------------------------------------------------------- +# Path to UNIX domain control socket for headless inspection and dynamic peer +# management via `yipd ctl` / `yip-ctl`. When running under systemd or OpenRC, +# this is typically placed in the runtime directory (/run/yipd/yip.sock). +# Default: /run/yipd/yip.sock +# +# control_socket=/run/yipd/yip.sock diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..9655e312 --- /dev/null +++ b/install.sh @@ -0,0 +1,373 @@ +#!/bin/sh +# yip Turnkey POSIX Installer +# https://github.com/femboyisp/yip +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/femboyisp/yip/main/install.sh | sh +# +# Environment Overrides: +# VERSION - Specific version to install (e.g. '0.1.0' or 'v0.1.0', default: latest) +# REPO - GitHub repository (default: 'femboyisp/yip') +# BINDIR - Target directory for binaries (default: '/usr/local/bin') +# CONFDIR - Target directory for configs (default: '/etc/yip') +# INIT_SYSTEM - Init system ('auto', 'systemd', 'openrc', 'runit', 'none', default: 'auto') +# SYSD_DIR - systemd unit directory (default: '/etc/systemd/system') +# OPENRC_DIR - OpenRC service directory (default: '/etc/init.d') +# RUNIT_DIR - runit service directory (default: '/etc/sv') +# LOCAL_TARBALL - Path to local tarball (for offline/sandbox testing) +# LOCAL_SHA256SUMS - Path to local SHA256SUMS file (for offline/sandbox testing) +# TARGET - Explicit target triple override (e.g. 'x86_64-unknown-linux-musl') + +set -e + +# --- 1. Privilege Escalation Detection --- +# If running as non-root and installing to default system paths, use sudo/doas. +# If custom directory overrides are provided (e.g. sandbox testing), skip sudo/doas. +if [ -z "${SUDO+x}" ]; then + SUDO="" + if [ "$(id -u)" -ne 0 ]; then + if [ -n "${BINDIR+x}" ] || [ -n "${CONFDIR+x}" ] || [ -n "${SYSD_DIR+x}" ] || [ -n "${OPENRC_DIR+x}" ] || [ -n "${RUNIT_DIR+x}" ]; then + # Custom directory overrides detected: running in unprivileged sandbox mode + SUDO="" + else + if command -v sudo >/dev/null 2>&1; then + SUDO="sudo" + elif command -v doas >/dev/null 2>&1; then + SUDO="doas" + else + echo "Error: Root privileges required to install to standard paths (/usr/local/bin, /etc/yip)." >&2 + echo "Neither 'sudo' nor 'doas' was found. Re-run as root or specify destination directory overrides." >&2 + exit 1 + fi + fi + fi +fi + +if [ "$(id -u)" -ne 0 ] && [ -z "$SUDO" ]; then + # Unprivileged user execution without sudo/doas: default to user-local XDG paths + BINDIR="${BINDIR:-${HOME}/.local/bin}" + CONFDIR="${CONFDIR:-${XDG_CONFIG_HOME:-$HOME/.config}/yip}" + INIT_SYSTEM="${INIT_SYSTEM:-none}" +else + BINDIR="${BINDIR:-/usr/local/bin}" + CONFDIR="${CONFDIR:-/etc/yip}" + INIT_SYSTEM="${INIT_SYSTEM:-auto}" +fi +SYSD_DIR="${SYSD_DIR:-/etc/systemd/system}" +OPENRC_DIR="${OPENRC_DIR:-/etc/init.d}" +RUNIT_DIR="${RUNIT_DIR:-/etc/sv}" +REPO="${REPO:-femboyisp/yip}" + +# --- 2. OS & Architecture Check --- +OS="$(uname -s)" +if [ "$OS" != "Linux" ]; then + echo "Error: Unsupported OS '$OS'. yip binaries are built for Linux." >&2 + exit 1 +fi + +if [ -z "$TARGET" ]; then + ARCH="$(uname -m)" + case "$ARCH" in + x86_64|amd64) + TARGET="x86_64-unknown-linux-musl" + ;; + aarch64|arm64) + TARGET="aarch64-unknown-linux-musl" + ;; + *) + echo "Error: Unsupported architecture '$ARCH'. Only x86_64/amd64 and aarch64/arm64 are supported." >&2 + exit 1 + ;; + esac +fi + +# --- 3. Temp Directory & Cleanup --- +TMPDIR="$(mktemp -d 2>/dev/null || echo "/tmp/yip-install-$$")" +[ ! -d "$TMPDIR" ] && mkdir -p "$TMPDIR" +cleanup() { + rm -rf "$TMPDIR" +} +trap cleanup EXIT INT TERM HUP + +# --- 4. Download & Verification --- +download() { + _url="$1" + _dest="$2" + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$_url" -o "$_dest" + elif command -v wget >/dev/null 2>&1; then + wget -qO "$_dest" "$_url" + else + echo "Error: Neither 'curl' nor 'wget' found. Cannot download release." >&2 + exit 1 + fi +} + +CHECKSUMS_FILE="" + +if [ -n "$LOCAL_TARBALL" ]; then + if [ ! -f "$LOCAL_TARBALL" ]; then + echo "Error: LOCAL_TARBALL '$LOCAL_TARBALL' not found." >&2 + exit 1 + fi + TARBALL_PATH="$LOCAL_TARBALL" + TARBALL_FILE="$(basename "$LOCAL_TARBALL")" + + if [ -n "$LOCAL_SHA256SUMS" ]; then + if [ ! -f "$LOCAL_SHA256SUMS" ]; then + echo "Error: LOCAL_SHA256SUMS '$LOCAL_SHA256SUMS' not found." >&2 + exit 1 + fi + CHECKSUMS_FILE="$LOCAL_SHA256SUMS" + elif [ -f "${LOCAL_TARBALL}.sha256" ]; then + CHECKSUMS_FILE="${LOCAL_TARBALL}.sha256" + elif [ -f "$(dirname "$LOCAL_TARBALL")/SHA256SUMS" ]; then + CHECKSUMS_FILE="$(dirname "$LOCAL_TARBALL")/SHA256SUMS" + fi +else + # Live download from GitHub releases + VERSION="${VERSION:-latest}" + if [ "$VERSION" = "latest" ]; then + echo "Resolving latest release for ${REPO}..." + TAG="" + if command -v curl >/dev/null 2>&1; then + REDIRECT_URL="$(curl -fsSL -o /dev/null -w '%{url_effective}' "https://github.com/${REPO}/releases/latest" 2>/dev/null | tr -d '\r')" + TAG="${REDIRECT_URL##*/}" + if [ "$TAG" = "latest" ] || [ -z "$TAG" ]; then + TAG="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null | grep '"tag_name":' | head -n 1 | sed -E 's/.*"([^"]+)".*/\1/')" + fi + elif command -v wget >/dev/null 2>&1; then + TAG="$(wget -qO- "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null | grep '"tag_name":' | head -n 1 | sed -E 's/.*"([^"]+)".*/\1/')" + fi + + if [ -z "$TAG" ] || [ "$TAG" = "latest" ]; then + echo "Error: Could not resolve latest release tag for ${REPO}." >&2 + exit 1 + fi + else + case "$VERSION" in + v*) TAG="$VERSION" ;; + *) TAG="v$VERSION" ;; + esac + fi + + TARBALL_FILE="yip-${TAG}-${TARGET}.tar.gz" + TARBALL_URL="https://github.com/${REPO}/releases/download/${TAG}/${TARBALL_FILE}" + CHECKSUMS_URL="https://github.com/${REPO}/releases/download/${TAG}/SHA256SUMS" + + TARBALL_PATH="$TMPDIR/$TARBALL_FILE" + CHECKSUMS_FILE="$TMPDIR/SHA256SUMS" + + echo "Downloading ${TARBALL_URL}..." + download "$TARBALL_URL" "$TARBALL_PATH" + echo "Downloading ${CHECKSUMS_URL}..." + download "$CHECKSUMS_URL" "$CHECKSUMS_FILE" +fi + +# Verify Checksum +if [ -n "$CHECKSUMS_FILE" ]; then + echo "Verifying SHA-256 checksum..." + EXPECTED_HASH="" + + # 1. Match basename in checksum file + MATCH_LINE="$(grep -E "[[:space:]]\*?${TARBALL_FILE}\$" "$CHECKSUMS_FILE" 2>/dev/null | head -n 1)" + if [ -n "$MATCH_LINE" ]; then + EXPECTED_HASH="$(printf '%s\n' "$MATCH_LINE" | awk '{print $1}')" + fi + + # 2. Match target triple in checksum file + if [ -z "$EXPECTED_HASH" ]; then + MATCH_LINE="$(grep -E "${TARGET}" "$CHECKSUMS_FILE" 2>/dev/null | head -n 1)" + if [ -n "$MATCH_LINE" ]; then + EXPECTED_HASH="$(printf '%s\n' "$MATCH_LINE" | awk '{print $1}')" + fi + fi + + # 3. Match single 64-hex entry + if [ -z "$EXPECTED_HASH" ]; then + FIRST_WORD="$(awk '{print $1}' "$CHECKSUMS_FILE" 2>/dev/null | head -n 1)" + case "$FIRST_WORD" in + [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]*) + if [ ${#FIRST_WORD} -eq 64 ]; then + EXPECTED_HASH="$FIRST_WORD" + fi + ;; + esac + fi + + if [ -z "$EXPECTED_HASH" ]; then + echo "Error: Could not locate checksum for $TARBALL_FILE in $CHECKSUMS_FILE." >&2 + exit 1 + fi + + CHECK_TMP="$TMPDIR/sha256_check.txt" + TARBALL_BASENAME="$(basename "$TARBALL_PATH")" + TARBALL_DIR="$(dirname "$TARBALL_PATH")" + printf '%s %s\n' "$EXPECTED_HASH" "$TARBALL_BASENAME" > "$CHECK_TMP" + + VERIFIED=0 + if command -v sha256sum >/dev/null 2>&1; then + if (cd "$TARBALL_DIR" && sha256sum -c "$CHECK_TMP" >/dev/null 2>&1); then + VERIFIED=1 + fi + elif command -v shasum >/dev/null 2>&1; then + if (cd "$TARBALL_DIR" && shasum -a 256 -c "$CHECK_TMP" >/dev/null 2>&1); then + VERIFIED=1 + fi + else + echo "Error: Neither 'sha256sum' nor 'shasum' found. Checksum verification unavailable." >&2 + exit 1 + fi + + if [ "$VERIFIED" -ne 1 ]; then + echo "Error: Checksum verification failed for $TARBALL_FILE." >&2 + exit 1 + fi + echo "Checksum OK ($EXPECTED_HASH)" +fi + +# --- 5. Extraction & Binary Installation --- +EXTRACT_DIR="$TMPDIR/extract" +mkdir -p "$EXTRACT_DIR" +tar -xzf "$TARBALL_PATH" -C "$EXTRACT_DIR" + +ROOT_DIR="" +if [ -f "$EXTRACT_DIR/yipd" ] && [ -f "$EXTRACT_DIR/yip-ca" ] && [ -f "$EXTRACT_DIR/yip-rendezvous" ]; then + ROOT_DIR="$EXTRACT_DIR" +else + # Find candidate directory containing all required binaries (distinguishes binary dir from contrib/openrc/yipd) + for cand in $(find "$EXTRACT_DIR" -type f -name "yip-ca" 2>/dev/null); do + cand_dir="$(dirname "$cand")" + if [ -f "$cand_dir/yipd" ] && [ -f "$cand_dir/yip-rendezvous" ]; then + ROOT_DIR="$cand_dir" + break + fi + done +fi + +if [ -z "$ROOT_DIR" ]; then + echo "Error: Required binaries (yipd, yip-ca, yip-rendezvous) not found in release archive." >&2 + exit 1 +fi + +# Verify required binaries exist +for bin in yipd yip-ca yip-rendezvous; do + if [ ! -f "$ROOT_DIR/$bin" ]; then + echo "Error: Required binary '$bin' missing from release archive." >&2 + exit 1 + fi +done + +# Install binaries +echo "Installing binaries to ${BINDIR}..." +$SUDO mkdir -p "$BINDIR" +for bin in yipd yip-ca yip-rendezvous; do + $SUDO cp "$ROOT_DIR/$bin" "$BINDIR/$bin" + $SUDO chmod 0755 "$BINDIR/$bin" +done + +# --- 6. Configuration Seeding --- +echo "Seeding configuration into ${CONFDIR}..." +if $SUDO mkdir -p "$CONFDIR" 2>/dev/null; then + if [ -f "$ROOT_DIR/example.config" ]; then + $SUDO cp "$ROOT_DIR/example.config" "$CONFDIR/yipd.conf.example" + $SUDO chmod 0644 "$CONFDIR/yipd.conf.example" + fi +else + echo "Warning: Could not create ${CONFDIR}; skipping configuration seeding." >&2 +fi + +# --- 7. Init System Detection & Installation --- +DETECTED_INIT="none" +case "$INIT_SYSTEM" in + auto|"") + if [ -d /run/systemd/system ]; then + DETECTED_INIT="systemd" + elif command -v rc-service >/dev/null 2>&1 || [ -d /run/openrc ]; then + DETECTED_INIT="openrc" + elif command -v runsv >/dev/null 2>&1 || [ -d /etc/sv ]; then + DETECTED_INIT="runit" + elif [ -d /etc/systemd/system ]; then + DETECTED_INIT="systemd" + elif [ -d /etc/init.d ]; then + DETECTED_INIT="openrc" + else + DETECTED_INIT="none" + fi + ;; + systemd|openrc|runit|none) + DETECTED_INIT="$INIT_SYSTEM" + ;; + *) + echo "Error: Unknown INIT_SYSTEM '$INIT_SYSTEM'. Expected auto, systemd, openrc, runit, or none." >&2 + exit 1 + ;; +esac + +echo "Configuring init service: ${DETECTED_INIT}" +case "$DETECTED_INIT" in + systemd) + $SUDO mkdir -p "$SYSD_DIR" + if [ -f "$ROOT_DIR/contrib/systemd/yipd.service" ]; then + $SUDO cp "$ROOT_DIR/contrib/systemd/yipd.service" "$SYSD_DIR/yipd.service" + $SUDO chmod 0644 "$SYSD_DIR/yipd.service" + echo "Installed ${SYSD_DIR}/yipd.service" + fi + if [ -f "$ROOT_DIR/contrib/systemd/yip-rendezvous.service" ]; then + $SUDO cp "$ROOT_DIR/contrib/systemd/yip-rendezvous.service" "$SYSD_DIR/yip-rendezvous.service" + $SUDO chmod 0644 "$SYSD_DIR/yip-rendezvous.service" + echo "Installed ${SYSD_DIR}/yip-rendezvous.service" + fi + if [ -d /run/systemd/system ] && [ "$SYSD_DIR" = "/etc/systemd/system" ] && command -v systemctl >/dev/null 2>&1; then + echo "Reloading systemd daemon..." + $SUDO systemctl daemon-reload 2>/dev/null || true + fi + ;; + openrc) + $SUDO mkdir -p "$OPENRC_DIR" + if [ -f "$ROOT_DIR/contrib/openrc/yipd" ]; then + $SUDO cp "$ROOT_DIR/contrib/openrc/yipd" "$OPENRC_DIR/yipd" + $SUDO chmod 0755 "$OPENRC_DIR/yipd" + echo "Installed ${OPENRC_DIR}/yipd" + fi + if [ -f "$ROOT_DIR/contrib/openrc/yip-rendezvous" ]; then + $SUDO cp "$ROOT_DIR/contrib/openrc/yip-rendezvous" "$OPENRC_DIR/yip-rendezvous" + $SUDO chmod 0755 "$OPENRC_DIR/yip-rendezvous" + echo "Installed ${OPENRC_DIR}/yip-rendezvous" + fi + ;; + runit) + $SUDO mkdir -p "$RUNIT_DIR" + if [ -d "$ROOT_DIR/contrib/runit/yipd" ]; then + $SUDO mkdir -p "$RUNIT_DIR/yipd/log" + $SUDO cp "$ROOT_DIR/contrib/runit/yipd/run" "$RUNIT_DIR/yipd/run" + $SUDO chmod 0755 "$RUNIT_DIR/yipd/run" + if [ -f "$ROOT_DIR/contrib/runit/yipd/log/run" ]; then + $SUDO cp "$ROOT_DIR/contrib/runit/yipd/log/run" "$RUNIT_DIR/yipd/log/run" + $SUDO chmod 0755 "$RUNIT_DIR/yipd/log/run" + fi + echo "Installed ${RUNIT_DIR}/yipd" + fi + if [ -d "$ROOT_DIR/contrib/runit/yip-rendezvous" ]; then + $SUDO mkdir -p "$RUNIT_DIR/yip-rendezvous/log" + $SUDO cp "$ROOT_DIR/contrib/runit/yip-rendezvous/run" "$RUNIT_DIR/yip-rendezvous/run" + $SUDO chmod 0755 "$RUNIT_DIR/yip-rendezvous/run" + if [ -f "$ROOT_DIR/contrib/runit/yip-rendezvous/log/run" ]; then + $SUDO cp "$ROOT_DIR/contrib/runit/yip-rendezvous/log/run" "$RUNIT_DIR/yip-rendezvous/log/run" + $SUDO chmod 0755 "$RUNIT_DIR/yip-rendezvous/log/run" + fi + echo "Installed ${RUNIT_DIR}/yip-rendezvous" + fi + ;; + none) + echo "Skipping service unit installation (INIT_SYSTEM=none)." + ;; +esac + +echo "" +echo "Installation complete!" +echo "Binaries: $BINDIR/{yipd, yip-ca, yip-rendezvous}" +echo "Config: $CONFDIR/yipd.conf.example" +if [ ! -f "$CONFDIR/yipd.conf" ]; then + echo "To configure yip, copy $CONFDIR/yipd.conf.example to $CONFDIR/yipd.conf and edit." +fi diff --git a/tests/test_install_sh.sh b/tests/test_install_sh.sh new file mode 100755 index 00000000..dd76dbe3 --- /dev/null +++ b/tests/test_install_sh.sh @@ -0,0 +1,361 @@ +#!/bin/sh +# Automated integration test suite for install.sh +# Tests sandbox installation, checksum verification, and init system handling. + +set -eu +(set -o pipefail 2>/dev/null) && set -o pipefail || true + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +INSTALL_SH="$REPO_ROOT/install.sh" + +if [ ! -f "$INSTALL_SH" ]; then + echo "Error: install.sh not found at $INSTALL_SH" >&2 + exit 1 +fi + +# Sandbox working directory +TEST_ROOT="$(mktemp -d /tmp/yip-test-install-XXXXXX)" +cleanup() { + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT INT TERM + +# Colors for test reporting +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +pass() { + printf "%b[PASS]%b %s\n" "${GREEN}" "${NC}" "$1" +} + +fail() { + printf "%b[FAIL]%b %s\n" "${RED}" "${NC}" "$1" >&2 + exit 1 +} + +# Determine host target triple +ARCH="$(uname -m)" +case "$ARCH" in + x86_64|amd64) + TARGET="x86_64-unknown-linux-musl" + ;; + aarch64|arm64) + TARGET="aarch64-unknown-linux-musl" + ;; + *) + TARGET="x86_64-unknown-linux-musl" + ;; +esac + +VERSION="0.1.0" +TARBALL_NAME="yip-v${VERSION}-${TARGET}.tar.gz" +MOCK_DIR="$TEST_ROOT/mock_release" +STAGE_DIR="$MOCK_DIR/yip-v${VERSION}-${TARGET}" + +echo "=== Preparing Mock Release Artifacts ===" +mkdir -p "$STAGE_DIR/contrib/systemd" +mkdir -p "$STAGE_DIR/contrib/openrc" +mkdir -p "$STAGE_DIR/contrib/runit/yipd/log" +mkdir -p "$STAGE_DIR/contrib/runit/yip-rendezvous/log" + +# Mock binaries +cat <<'EOF' > "$STAGE_DIR/yipd" +#!/bin/sh +echo "yipd mock v0.1.0" +EOF + +cat <<'EOF' > "$STAGE_DIR/yip-ca" +#!/bin/sh +echo "yip-ca mock v0.1.0" +EOF + +cat <<'EOF' > "$STAGE_DIR/yip-rendezvous" +#!/bin/sh +echo "yip-rendezvous mock v0.1.0" +EOF + +chmod 0755 "$STAGE_DIR/yipd" "$STAGE_DIR/yip-ca" "$STAGE_DIR/yip-rendezvous" + +# Mock example.config +cat <<'EOF' > "$STAGE_DIR/example.config" +listen=0.0.0.0 +device=yip0 +EOF + +# Mock contrib units +echo "[Unit]" > "$STAGE_DIR/contrib/systemd/yipd.service" +echo "[Unit]" > "$STAGE_DIR/contrib/systemd/yip-rendezvous.service" +echo "#!/sbin/openrc-run" > "$STAGE_DIR/contrib/openrc/yipd" +chmod 0755 "$STAGE_DIR/contrib/openrc/yipd" +echo "#!/sbin/openrc-run" > "$STAGE_DIR/contrib/openrc/yip-rendezvous" +chmod 0755 "$STAGE_DIR/contrib/openrc/yip-rendezvous" +echo "#!/bin/sh" > "$STAGE_DIR/contrib/runit/yipd/run" +chmod 0755 "$STAGE_DIR/contrib/runit/yipd/run" +echo "#!/bin/sh" > "$STAGE_DIR/contrib/runit/yipd/log/run" +chmod 0755 "$STAGE_DIR/contrib/runit/yipd/log/run" +echo "#!/bin/sh" > "$STAGE_DIR/contrib/runit/yip-rendezvous/run" +chmod 0755 "$STAGE_DIR/contrib/runit/yip-rendezvous/run" +echo "#!/bin/sh" > "$STAGE_DIR/contrib/runit/yip-rendezvous/log/run" +chmod 0755 "$STAGE_DIR/contrib/runit/yip-rendezvous/log/run" + +# Package tarball +tar -czf "$MOCK_DIR/$TARBALL_NAME" -C "$MOCK_DIR" "yip-v${VERSION}-${TARGET}" + +# Generate real SHA256SUMS +(cd "$MOCK_DIR" && sha256sum "$TARBALL_NAME" > "SHA256SUMS") + +MOCK_TARBALL="$MOCK_DIR/$TARBALL_NAME" +MOCK_SHA256SUMS="$MOCK_DIR/SHA256SUMS" + +echo "Mock tarball: $MOCK_TARBALL" +echo "Mock SHA256SUMS: $MOCK_SHA256SUMS" +cat "$MOCK_SHA256SUMS" +echo "" + +# ----------------------------------------------------------------------------- +# Test Case 1: Valid install into sandbox paths (systemd) +# ----------------------------------------------------------------------------- +echo "=== Test Case 1: Valid install into sandbox paths ===" +SB1="$TEST_ROOT/sandbox_1" +mkdir -p "$SB1" + +OUTPUT_1=$(BINDIR="$SB1/bin" \ +CONFDIR="$SB1/etc/yip" \ +SYSD_DIR="$SB1/etc/systemd/system" \ +LOCAL_TARBALL="$MOCK_TARBALL" \ +LOCAL_SHA256SUMS="$MOCK_SHA256SUMS" \ +INIT_SYSTEM="systemd" \ +sh "$INSTALL_SH" 2>&1) + +# Check binaries +[ -f "$SB1/bin/yipd" ] || fail "yipd binary not installed" +[ -x "$SB1/bin/yipd" ] || fail "yipd binary is not executable" +[ -f "$SB1/bin/yip-ca" ] || fail "yip-ca binary not installed" +[ -x "$SB1/bin/yip-ca" ] || fail "yip-ca binary is not executable" +[ -f "$SB1/bin/yip-rendezvous" ] || fail "yip-rendezvous binary not installed" +[ -x "$SB1/bin/yip-rendezvous" ] || fail "yip-rendezvous binary is not executable" + +# Verify execution of installed binary +BIN_OUT=$("$SB1/bin/yipd") +case "$BIN_OUT" in + *"yipd mock v0.1.0"*) ;; + *) fail "yipd execution output unexpected: $BIN_OUT" ;; +esac + +# Check config seeding +[ -f "$SB1/etc/yip/yipd.conf.example" ] || fail "yipd.conf.example not created" +grep -q "listen=0.0.0.0" "$SB1/etc/yip/yipd.conf.example" || fail "example config content mismatch" + +# Check systemd service unit installation +[ -f "$SB1/etc/systemd/system/yipd.service" ] || fail "yipd.service not installed" +[ -f "$SB1/etc/systemd/system/yip-rendezvous.service" ] || fail "yip-rendezvous.service not installed" + +pass "Test Case 1 passed: Binaries, permissions, config, and systemd units verified" + +# ----------------------------------------------------------------------------- +# Test Case 2: Corrupted tarball (SHA-256 verification failure) +# ----------------------------------------------------------------------------- +echo "=== Test Case 2: Checksum mismatch rejection ===" +SB2="$TEST_ROOT/sandbox_2" +mkdir -p "$SB2" + +CORRUPTED_TARBALL="$TEST_ROOT/corrupted-$TARBALL_NAME" +cp "$MOCK_TARBALL" "$CORRUPTED_TARBALL" +# Tamper with archive +echo "TAMPERED DATA" >> "$CORRUPTED_TARBALL" + +set +e +OUTPUT_2=$(BINDIR="$SB2/bin" \ +CONFDIR="$SB2/etc/yip" \ +LOCAL_TARBALL="$CORRUPTED_TARBALL" \ +LOCAL_SHA256SUMS="$MOCK_SHA256SUMS" \ +INIT_SYSTEM="none" \ +sh "$INSTALL_SH" 2>&1) +EXIT_CODE_2=$? +set -e + +[ "$EXIT_CODE_2" -ne 0 ] || fail "install.sh succeeded on corrupted tarball (expected non-zero exit)" +case "$OUTPUT_2" in + *"Checksum verification failed"*) ;; + *) fail "Error output missing checksum failure notice: $OUTPUT_2" ;; +esac +[ ! -f "$SB2/bin/yipd" ] || fail "Binaries installed despite checksum mismatch" +[ ! -f "$SB2/etc/yip/yipd.conf.example" ] || fail "Config seeded despite checksum mismatch" + +pass "Test Case 2 passed: Corrupted tarball was rejected before installation" + +# ----------------------------------------------------------------------------- +# Test Case 3: Explicit INIT_SYSTEM selection +# ----------------------------------------------------------------------------- +echo "=== Test Case 3a: Explicit INIT_SYSTEM=openrc ===" +SB3A="$TEST_ROOT/sandbox_3a" +mkdir -p "$SB3A" + +BINDIR="$SB3A/bin" \ +CONFDIR="$SB3A/etc/yip" \ +OPENRC_DIR="$SB3A/etc/init.d" \ +SYSD_DIR="$SB3A/etc/systemd/system" \ +LOCAL_TARBALL="$MOCK_TARBALL" \ +LOCAL_SHA256SUMS="$MOCK_SHA256SUMS" \ +INIT_SYSTEM="openrc" \ +sh "$INSTALL_SH" >/dev/null 2>&1 + +[ -f "$SB3A/bin/yipd" ] || fail "binaries not installed" +[ -f "$SB3A/etc/init.d/yipd" ] || fail "OpenRC yipd service not installed" +[ -x "$SB3A/etc/init.d/yipd" ] || fail "OpenRC yipd service not executable" +[ -f "$SB3A/etc/init.d/yip-rendezvous" ] || fail "OpenRC yip-rendezvous service not installed" +[ -x "$SB3A/etc/init.d/yip-rendezvous" ] || fail "OpenRC yip-rendezvous service not executable" +[ ! -f "$SB3A/etc/systemd/system/yipd.service" ] || fail "systemd service installed when INIT_SYSTEM=openrc" + +pass "Test Case 3a passed: OpenRC services correctly installed with executable permissions" + +echo "=== Test Case 3b: Explicit INIT_SYSTEM=runit ===" +SB3B="$TEST_ROOT/sandbox_3b" +mkdir -p "$SB3B" + +BINDIR="$SB3B/bin" \ +CONFDIR="$SB3B/etc/yip" \ +RUNIT_DIR="$SB3B/etc/sv" \ +SYSD_DIR="$SB3B/etc/systemd/system" \ +LOCAL_TARBALL="$MOCK_TARBALL" \ +LOCAL_SHA256SUMS="$MOCK_SHA256SUMS" \ +INIT_SYSTEM="runit" \ +sh "$INSTALL_SH" >/dev/null 2>&1 + +[ -f "$SB3B/bin/yipd" ] || fail "binaries not installed" +[ -f "$SB3B/etc/sv/yipd/run" ] || fail "runit yipd run script not installed" +[ -x "$SB3B/etc/sv/yipd/run" ] || fail "runit yipd run script not executable" +[ -f "$SB3B/etc/sv/yipd/log/run" ] || fail "runit yipd log/run script not installed" +[ -x "$SB3B/etc/sv/yipd/log/run" ] || fail "runit yipd log/run script not executable" +[ -f "$SB3B/etc/sv/yip-rendezvous/run" ] || fail "runit yip-rendezvous run script not installed" +[ -x "$SB3B/etc/sv/yip-rendezvous/run" ] || fail "runit yip-rendezvous run script not executable" +[ ! -f "$SB3B/etc/systemd/system/yipd.service" ] || fail "systemd service installed when INIT_SYSTEM=runit" + +pass "Test Case 3b passed: runit service directory and scripts installed" + +echo "=== Test Case 3c: Explicit INIT_SYSTEM=none ===" +SB3C="$TEST_ROOT/sandbox_3c" +mkdir -p "$SB3C" + +BINDIR="$SB3C/bin" \ +CONFDIR="$SB3C/etc/yip" \ +SYSD_DIR="$SB3C/etc/systemd/system" \ +OPENRC_DIR="$SB3C/etc/init.d" \ +RUNIT_DIR="$SB3C/etc/sv" \ +LOCAL_TARBALL="$MOCK_TARBALL" \ +LOCAL_SHA256SUMS="$MOCK_SHA256SUMS" \ +INIT_SYSTEM="none" \ +sh "$INSTALL_SH" >/dev/null 2>&1 + +[ -f "$SB3C/bin/yipd" ] || fail "binaries not installed" +[ -f "$SB3C/etc/yip/yipd.conf.example" ] || fail "example config not seeded" +[ ! -f "$SB3C/etc/systemd/system/yipd.service" ] || fail "systemd service installed when INIT_SYSTEM=none" +[ ! -f "$SB3C/etc/init.d/yipd" ] || fail "OpenRC service installed when INIT_SYSTEM=none" +[ ! -d "$SB3C/etc/sv/yipd" ] || fail "runit service installed when INIT_SYSTEM=none" + +pass "Test Case 3c passed: INIT_SYSTEM=none skips service registration" + +echo "=== Test Case 3d: Explicit INIT_SYSTEM=systemd ===" +SB3D="$TEST_ROOT/sandbox_3d" +mkdir -p "$SB3D" + +BINDIR="$SB3D/bin" \ +CONFDIR="$SB3D/etc/yip" \ +SYSD_DIR="$SB3D/etc/systemd/system" \ +LOCAL_TARBALL="$MOCK_TARBALL" \ +LOCAL_SHA256SUMS="$MOCK_SHA256SUMS" \ +INIT_SYSTEM="systemd" \ +sh "$INSTALL_SH" >/dev/null 2>&1 + +[ -f "$SB3D/bin/yipd" ] || fail "binaries not installed" +[ -f "$SB3D/etc/systemd/system/yipd.service" ] || fail "systemd yipd.service missing" +[ -f "$SB3D/etc/systemd/system/yip-rendezvous.service" ] || fail "systemd yip-rendezvous.service missing" + +pass "Test Case 3d passed: INIT_SYSTEM=systemd correctly installed units" + +# ----------------------------------------------------------------------------- +# Test Case 4: Invalid INIT_SYSTEM handling +# ----------------------------------------------------------------------------- +echo "=== Test Case 4: Invalid INIT_SYSTEM handling ===" +SB4="$TEST_ROOT/sandbox_4" +mkdir -p "$SB4" + +set +e +OUTPUT_4=$(BINDIR="$SB4/bin" \ +CONFDIR="$SB4/etc/yip" \ +LOCAL_TARBALL="$MOCK_TARBALL" \ +LOCAL_SHA256SUMS="$MOCK_SHA256SUMS" \ +INIT_SYSTEM="nonexistent_init" \ +sh "$INSTALL_SH" 2>&1) +EXIT_CODE_4=$? +set -e + +[ "$EXIT_CODE_4" -ne 0 ] || fail "install.sh succeeded with invalid INIT_SYSTEM" +case "$OUTPUT_4" in + *"Unknown INIT_SYSTEM"*) ;; + *) fail "Unexpected output for invalid INIT_SYSTEM: $OUTPUT_4" ;; +esac + +pass "Test Case 4 passed: Unknown INIT_SYSTEM correctly rejected" + +# ----------------------------------------------------------------------------- +# Test Case 5: Architecture and Missing Binary Checks +# ----------------------------------------------------------------------------- +echo "=== Test Case 5: Missing binary in archive detection ===" +SB5="$TEST_ROOT/sandbox_5" +mkdir -p "$SB5" + +INCOMPLETE_DIR="$TEST_ROOT/incomplete_release" +mkdir -p "$INCOMPLETE_DIR/yip-v${VERSION}-${TARGET}" +# Only create yipd, omit yip-ca and yip-rendezvous +echo "#!/bin/sh" > "$INCOMPLETE_DIR/yip-v${VERSION}-${TARGET}/yipd" +chmod +x "$INCOMPLETE_DIR/yip-v${VERSION}-${TARGET}/yipd" +tar -czf "$INCOMPLETE_DIR/incomplete.tar.gz" -C "$INCOMPLETE_DIR" "yip-v${VERSION}-${TARGET}" + +set +e +OUTPUT_5=$(BINDIR="$SB5/bin" \ +CONFDIR="$SB5/etc/yip" \ +LOCAL_TARBALL="$INCOMPLETE_DIR/incomplete.tar.gz" \ +INIT_SYSTEM="none" \ +sh "$INSTALL_SH" 2>&1) +EXIT_CODE_5=$? +set -e + +[ "$EXIT_CODE_5" -ne 0 ] || fail "install.sh succeeded with missing required binaries" +case "$OUTPUT_5" in + *"not found in release archive"*|*"missing from release archive"*) ;; + *) fail "Unexpected output for missing binary: $OUTPUT_5" ;; +esac + +pass "Test Case 5 passed: Incomplete archive with missing binaries detected and rejected" + +# ----------------------------------------------------------------------------- +# Test Case 6: Unprivileged user installation with only BINDIR override +# ----------------------------------------------------------------------------- +echo "=== Test Case 6: Unprivileged installation with only BINDIR specified ===" +SB6="$TEST_ROOT/sandbox_6" +mkdir -p "$SB6/bin" "$SB6/home" + +export HOME="$SB6/home" +export XDG_CONFIG_HOME="$SB6/home/.config" + +set +e +OUTPUT_6=$(BINDIR="$SB6/bin" \ +LOCAL_TARBALL="$MOCK_TARBALL" \ +LOCAL_SHA256SUMS="$MOCK_SHA256SUMS" \ +sh "$INSTALL_SH" 2>&1) +EXIT_CODE_6=$? +set -e + +[ "$EXIT_CODE_6" -eq 0 ] || fail "install.sh failed with only BINDIR set: $OUTPUT_6" +[ -x "$SB6/bin/yipd" ] || fail "yipd not installed in user BINDIR" +[ -f "$SB6/home/.config/yip/yipd.conf.example" ] || fail "Config not seeded in user config directory: $OUTPUT_6" + +pass "Test Case 6 passed: Unprivileged user install gracefully defaults to user XDG config path" + +echo "" +echo "==========================================" +printf "%bALL INSTALLER INTEGRATION TESTS PASSED!%b\n" "${GREEN}" "${NC}" +echo "=========================================="