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 '\