From 55b6f43a0b26d2ff9fcdaf14c1e585705d737f31 Mon Sep 17 00:00:00 2001 From: skullcmd Date: Tue, 28 Apr 2026 02:41:48 +0000 Subject: [PATCH] =?UTF-8?q?build(afxdp):=20apt-get=20deps=20+=20systemd=20?= =?UTF-8?q?CAP=5FBPF=20+=20install-time=20probe=20=E2=80=94=20Phase=202=20?= =?UTF-8?q?PR=20C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to AnyVM-Tech/anyscan-engine-c PR #3 (Makefile USE_AF_XDP=1 conditional). With both PRs landed, a worker bundle built with the fork's USE_AF_XDP=1 toggle has the kernel capabilities and runtime metadata to run --io-engine=af_xdp; PR D will add the runtime opt-in env var that the adapter passes through to the scanner subprocess. What this adds (per plan §3.5, §4.1, §4.3, §4.4): 1. install-external-deps.sh — install_afxdp_build_deps() Best-effort apt-get install for libxdp-dev libbpf-dev libelf-dev so the dev-machine bootstrap path can build the scanner with USE_AF_XDP=1. Skips cleanly when: - apt-get is not on PATH (non-Debian host), - the script runs non-root and sudo is unavailable / would prompt, - ANYSCAN_INSTALL_AFXDP_DEPS=false (operator opt-out). When it skips, it prints the manual command. The default `make` build path does not need these packages, so failure here is non-fatal. 2. anyscan-worker.service / anyscan-worker-only.service — CAP_BPF Adds CAP_BPF to AmbientCapabilities and CapabilityBoundingSet on both worker units. The scanner only uses the bpf() syscall when invoked with --io-engine=af_xdp (XSK socket open + libxdp default redirect program attach); the AF_PACKET default path does not need CAP_BPF, so this grant is purely a runtime gate that lets workers opt in via ANYSCAN_SCANNER_IO_ENGINE without a unit-file refresh. CAP_BPF is the narrow capability introduced in 5.8; on older kernels it collapses into CAP_SYS_ADMIN and the bit we set here is ignored — the install-time probe (next bullet) keeps af_xdp disabled on those hosts so the unit-file grant has no effect there either. 3. install-worker-bundle.sh — probe_afxdp_runtime_available + apply At install time, probe (a) `uname -r` >= 5.10 and (b) `ldconfig -p` shows libxdp.so. Write ANYSCAN_AF_XDP_AVAILABLE=true|false to /etc/agentd/runtime.env unconditionally so the value is explicit and a partial upgrade cannot leave a stale "true" behind after a kernel downgrade. The flag is consumed by Phase 2 PR D (adapter-level opt-in) — this PR only writes it. ANYSCAN_AF_XDP_AVAILABLE=true is necessary but not sufficient: the bundled scanner also has to be built with USE_AF_XDP=1 (worker-bundle build pipeline change, separate work). Test status: - bash -n on both edited scripts: OK. - Probe function unit-tested against this host (kernel 6.12, libxdp 1.5.4 present): returns "true". Forced-fake k=5.4: returns "false". Forced-fake k=6.1 with libxdp present: returns "true". - End-to-end smoke test of apply_afxdp_availability against a tmp runtime.env: writes ANYSCAN_AF_XDP_AVAILABLE=true (this host). - systemd-analyze verify on both unit files: clean (the only reported errors are missing /opt/{anyscan,agentd}/bin in this dev sandbox, which is expected). - cargo build --workspace: clean, two pre-existing dead_code warnings unchanged. - cargo test --workspace --no-fail-fast: 437 passed, 0 failed, 4 ignored (unchanged from main). Out of scope (deferred to Phase 2 PR D): - ANYSCAN_SCANNER_IO_ENGINE env var in runtime.worker.env.template. - vulnscanner-zmap-adapter.py reading the env and passing --io-engine through to the scanner subprocess. - install-worker-bundle.sh writing ANYSCAN_SCANNER_IO_ENGINE=af_packet on fresh installs. Plan: plans/2026-04-27-portscan-afxdp-plan-v1.md §3.5, §4.1, §4.3, §4.4. --- anyscan-worker-only.service | 16 +++++++++-- anyscan-worker.service | 16 +++++++++-- install-external-deps.sh | 49 ++++++++++++++++++++++++++++++++ install-worker-bundle.sh | 56 +++++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 4 deletions(-) diff --git a/anyscan-worker-only.service b/anyscan-worker-only.service index 1761309..acc37a3 100644 --- a/anyscan-worker-only.service +++ b/anyscan-worker-only.service @@ -28,8 +28,20 @@ ExecStart=/opt/agentd/bin/agentd daemon Restart=always RestartSec=5 NoNewPrivileges=true -AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW -CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW +# CAP_BPF is required for the AF_XDP I/O path the scanner gains in +# Phase 2 of plans/2026-04-27-portscan-afxdp-plan-v1.md (§4.4): opening an +# XSK socket and attaching libxdp's default redirect program both go +# through the bpf() syscall. The scanner only opens an XSK when invoked +# with --io-engine=af_xdp; AF_PACKET (the default) does not need CAP_BPF, +# so granting it here is purely a runtime gate that lets workers opt in +# via ANYSCAN_SCANNER_IO_ENGINE without a unit file refresh. CAP_BPF is +# scoped narrowly: it does not imply CAP_SYS_ADMIN or kernel-module load +# rights. Older (<5.8) kernels collapse it into CAP_SYS_ADMIN and the +# capability bits we set here are simply ignored — the install-time +# probe in install-worker-bundle.sh refuses to enable af_xdp on those +# hosts so the unit-file grant has no effect there either. +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_BPF +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_BPF PrivateTmp=true PrivateDevices=true ProtectSystem=strict diff --git a/anyscan-worker.service b/anyscan-worker.service index a766b50..b412896 100644 --- a/anyscan-worker.service +++ b/anyscan-worker.service @@ -26,8 +26,20 @@ ExecStart=/opt/anyscan/bin/anyscan-worker daemon Restart=always RestartSec=5 NoNewPrivileges=true -AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW -CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW +# CAP_BPF is required for the AF_XDP I/O path the scanner gains in +# Phase 2 of plans/2026-04-27-portscan-afxdp-plan-v1.md (§4.4): opening an +# XSK socket and attaching libxdp's default redirect program both go +# through the bpf() syscall. The scanner only opens an XSK when invoked +# with --io-engine=af_xdp; AF_PACKET (the default) does not need CAP_BPF, +# so granting it here is purely a runtime gate that lets workers opt in +# via ANYSCAN_SCANNER_IO_ENGINE without a unit file refresh. CAP_BPF is +# scoped narrowly: it does not imply CAP_SYS_ADMIN or kernel-module load +# rights. Older (<5.8) kernels collapse it into CAP_SYS_ADMIN and the +# capability bits we set here are simply ignored — the install-time +# probe in install-worker-bundle.sh refuses to enable af_xdp on those +# hosts so the unit-file grant has no effect there either. +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_BPF +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_BPF PrivateTmp=true PrivateDevices=true ProtectSystem=strict diff --git a/install-external-deps.sh b/install-external-deps.sh index e8eb6f3..602da7c 100755 --- a/install-external-deps.sh +++ b/install-external-deps.sh @@ -26,6 +26,53 @@ print_banner() { printf '═══════════════════════════════════════════════════════════\n' } +# Install build-time dependencies for the AF_XDP I/O path the scanner gains +# in Phase 2 of plans/2026-04-27-portscan-afxdp-plan-v1.md (§4.1). The fork +# Makefile only pulls these in when invoked with `make USE_AF_XDP=1`; with +# the default `make` they are unused, so we make the install best-effort: +# run only when apt-get is available AND we have permission to install +# packages, skip otherwise with a one-line note. The scanner build still +# succeeds without these packages — `make USE_AF_XDP=1` would fail +# loudly later, which is the right escalation. +# +# Set ANYSCAN_INSTALL_AFXDP_DEPS=false to suppress this block (e.g. on +# AMIs where the operator pre-pinned a different libxdp version). +install_afxdp_build_deps() { + if [ "${ANYSCAN_INSTALL_AFXDP_DEPS:-true}" != "true" ]; then + return 0 + fi + if ! command -v apt-get >/dev/null 2>&1; then + printf '[*] Skipping AF_XDP build deps: apt-get not on PATH (non-Debian host).\n' + return 0 + fi + local apt_cmd=() + if [ "$(id -u 2>/dev/null || echo 1)" = "0" ]; then + apt_cmd=(apt-get) + elif command -v sudo >/dev/null 2>&1; then + apt_cmd=(sudo -n apt-get) + else + printf '[*] Skipping AF_XDP build deps: not root and sudo is not available.\n' + printf ' Install manually if you plan to build the scanner with USE_AF_XDP=1:\n' + printf ' sudo apt-get install -y libxdp-dev libbpf-dev libelf-dev\n' + return 0 + fi + # Probe sudo non-interactively; if it would prompt, bail rather than + # block the script in CI. + if [ "${apt_cmd[0]}" = "sudo" ] && ! sudo -n true >/dev/null 2>&1; then + printf '[*] Skipping AF_XDP build deps: sudo would prompt for a password.\n' + printf ' Install manually if you plan to build the scanner with USE_AF_XDP=1:\n' + printf ' sudo apt-get install -y libxdp-dev libbpf-dev libelf-dev\n' + return 0 + fi + printf '[*] Installing AF_XDP build deps (libxdp-dev libbpf-dev libelf-dev)...\n' + if ! "${apt_cmd[@]}" install -y --no-install-recommends \ + libxdp-dev libbpf-dev libelf-dev >/dev/null; then + printf '[!] apt-get install of AF_XDP build deps failed; the scanner will still build with default `make`.\n' >&2 + printf ' Re-run with USE_AF_XDP=1 only after libxdp-dev / libbpf-dev / libelf-dev are present.\n' >&2 + return 0 + fi +} + upsert_env_value() { local key="$1" local value="$2" @@ -56,6 +103,8 @@ if ! command -v git >/dev/null 2>&1; then exit 1 fi +install_afxdp_build_deps + if [ -d "$VULNSCANNER_REPO_DIR/.git" ]; then printf '[*] Updating external repository in %s...\n' "$VULNSCANNER_REPO_DIR" git -C "$VULNSCANNER_REPO_DIR" fetch --tags --prune diff --git a/install-worker-bundle.sh b/install-worker-bundle.sh index d36cc8a..0348699 100755 --- a/install-worker-bundle.sh +++ b/install-worker-bundle.sh @@ -355,6 +355,61 @@ apply_host_resource_defaults() { fi } +probe_afxdp_runtime_available() { + # Phase 2 PR C of plans/2026-04-27-portscan-afxdp-plan-v1.md §4.3. + # The scanner can be invoked with --io-engine=af_xdp only when (a) the + # kernel is recent enough that XDP_USE_NEED_WAKEUP behaves correctly + # (≥5.10, plan §4.3) and (b) libxdp.so is loadable on the host (the + # binary is dynamically linked against it when built with + # USE_AF_XDP=1; if the runtime libs are absent the scanner crashes + # at startup with a dlopen error). Both checks are cheap, so do them + # at install time and write ANYSCAN_AF_XDP_AVAILABLE so downstream + # code (the adapter, future tooling) does not have to redo the + # probe per-scan. The variable defaults to "false" — operators can + # override to "true" by hand if they know the bundle's bin/scanner + # was built with USE_AF_XDP=1 and they have the libs from a path not + # visible to ldconfig (e.g. LD_LIBRARY_PATH). + local kernel_release kernel_major kernel_minor + kernel_release="$(uname -r 2>/dev/null || true)" + if [ -z "$kernel_release" ]; then + printf 'false' + return 0 + fi + kernel_major="${kernel_release%%.*}" + local rest="${kernel_release#*.}" + kernel_minor="${rest%%.*}" + case "$kernel_major" in ''|*[!0-9]*) printf 'false'; return 0 ;; esac + case "$kernel_minor" in ''|*[!0-9]*) kernel_minor=0 ;; esac + if [ "$kernel_major" -lt 5 ] || { [ "$kernel_major" -eq 5 ] && [ "$kernel_minor" -lt 10 ]; }; then + printf 'false' + return 0 + fi + if ! command_exists ldconfig; then + printf 'false' + return 0 + fi + if ! ldconfig -p 2>/dev/null | grep -q '\