From f0847001ee70d9536c57d142f10a18c41cc6df03 Mon Sep 17 00:00:00 2001 From: embeddevio Date: Wed, 28 Oct 2026 21:30:01 +0300 Subject: [PATCH] =?UTF-8?q?netns:=20=D0=B2=D1=85=D0=BE=D0=B4=20=D0=B2=20na?= =?UTF-8?q?mespace=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20nsenter=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7=D0=B5=20ip=20netn?= =?UTF-8?q?s=20exec=20(#56)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib.sh: ns_exec()/ns_prefix() с автоопределением режима (AWG_NS_EXEC_MODE), stand_fail() для кода 4; cleanup в run.sh больше не оставляет мок и sink. - run.sh/topology.sh: коды возврата нормализованы к 0/1/3/4 (255 от iproute2 -> 4). Refs #21, Refs #15, Refs #56 --- tests/netns/lib.sh | 110 ++++++++++++++++++++++++++++++++++++++++----- tests/netns/run.sh | 40 ++++++++++++----- 2 files changed, 126 insertions(+), 24 deletions(-) diff --git a/tests/netns/lib.sh b/tests/netns/lib.sh index de2130f..8063752 100644 --- a/tests/netns/lib.sh +++ b/tests/netns/lib.sh @@ -67,6 +67,92 @@ fail() { printf '%s[fail]%s %s\n' "$_c_red" "$_c_off" "$*" >&2; exit 1; } # см. deploy/dev-stand.md. 4 — внутренняя ошибка стенда. blocked() { printf '%s[blocked]%s %s\n' "$_c_yel" "$_c_off" "$*" >&2; exit 3; } +# Ошибка самого стенда (не тестов и не окружения) — код 4. +stand_fail() { echo "[fail] $*" >&2; exit 4; } + +# --- вход в netns ----------------------------------------------------------- +# `ip netns exec` и `ip -n ` (iproute2 6.x) перемонтируют /sys внутри +# namespace и в контейнере падают с "mount of /sys failed: Operation not +# permitted", хотя `ip netns add` работает (issue #56). nsenter входит в тот же +# /var/run/netns/ без перемонтирования — используется как fallback. +# Режим: AWG_NS_EXEC_MODE=auto|ip|nsenter (auto — определить один раз пробой). +: "${AWG_NS_EXEC_MODE:=auto}" +NS_EXEC_RESOLVED="" +NS_EXEC_IP_CMD=(ip netns exec) + +_ns_exec_probe() { + # $1 — режим; 0 — вход в свежесозданный netns работает + local mode="$1" probe="${AWG_NETNS_PREFIX}-probe" rc=0 + ip netns add "$probe" 2>/dev/null || return 1 + case "$mode" in + ip) + timeout 5 "${NS_EXEC_IP_CMD[@]}" "$probe" true >/dev/null 2>&1 || rc=1 + ;; + nsenter) + timeout 5 nsenter --net="/var/run/netns/${probe}" true >/dev/null 2>&1 || rc=1 + ;; + *) + rc=1 + ;; + esac + ip netns delete "$probe" 2>/dev/null || true + return "$rc" +} + +select_ns_exec_mode() { + # Печатает выбранный режим; код 3 — окружение не даёт войти в netns. + case "$AWG_NS_EXEC_MODE" in + ip|nsenter) + echo "$AWG_NS_EXEC_MODE" + return 0 + ;; + esac + if _ns_exec_probe ip; then + echo ip + return 0 + fi + if command -v nsenter >/dev/null 2>&1 && _ns_exec_probe nsenter; then + warn "'ip netns exec' не работает (mount of /sys: Operation not permitted, issue #56) — использую nsenter --net" + echo nsenter + return 0 + fi + blocked "ни 'ip netns exec', ни 'nsenter --net' не работают: нет CAP_SYS_ADMIN/NET_ADMIN (см. issue #56)" +} + +# ns_exec <команда...> — выполнить команду внутри namespace. +ns_exec() { + local ns="$1" + shift + if [ -z "$NS_EXEC_RESOLVED" ]; then + NS_EXEC_RESOLVED="$(select_ns_exec_mode)" || exit 3 + fi + case "$NS_EXEC_RESOLVED" in + nsenter) nsenter --net="/var/run/netns/${ns}" "$@" ;; + *) "${NS_EXEC_IP_CMD[@]}" "$ns" "$@" ;; + esac +} + +# ns_prefix — напечатать команду входа в netns ПО СЛОВУ НА СТРОКУ. +# Нужна для фонового запуска: `mapfile -t P < <(ns_prefix NS)` + +# `( exec "${P[@]}" python3 ... ) &` даёт в $! pid самого процесса, тогда cleanup +# убивает его, а не обёртку-функцию (иначе мок и детектор утечки остаются жить +# после прогона и держат stdout). +ns_prefix() { + local ns="$1" + if [ -z "$NS_EXEC_RESOLVED" ]; then + NS_EXEC_RESOLVED="$(select_ns_exec_mode)" || exit 3 + fi + if [ "$NS_EXEC_RESOLVED" = nsenter ]; then + echo nsenter + echo --net="/var/run/netns/${ns}" + else + echo ip + echo netns + echo exec + echo "$ns" + fi +} + # --- предусловия ------------------------------------------------------------ require_root() { [ "$(id -u)" -eq 0 ] || blocked "нужен root (или CAP_NET_ADMIN): запустите под sudo" @@ -98,18 +184,18 @@ fw_kind() { local want="${AWG_FW_BACKEND:-auto}" case "$want" in nft) - ip netns exec "$AWG_NS_GW" sh -c 'command -v nft' >/dev/null 2>&1 && { echo nft; return; } + ns_exec "$AWG_NS_GW" sh -c 'command -v nft' >/dev/null 2>&1 && { echo nft; return; } echo none; return ;; iptables) - ip netns exec "$AWG_NS_GW" sh -c 'command -v iptables' >/dev/null 2>&1 && { echo iptables; return; } + ns_exec "$AWG_NS_GW" sh -c 'command -v iptables' >/dev/null 2>&1 && { echo iptables; return; } echo none; return ;; esac # command -v — builtin shell, поэтому проверяем через sh -c внутри netns - if ip netns exec "$AWG_NS_GW" sh -c 'command -v nft' >/dev/null 2>&1; then + if ns_exec "$AWG_NS_GW" sh -c 'command -v nft' >/dev/null 2>&1; then echo nft - elif ip netns exec "$AWG_NS_GW" sh -c 'command -v iptables' >/dev/null 2>&1; then + elif ns_exec "$AWG_NS_GW" sh -c 'command -v iptables' >/dev/null 2>&1; then echo iptables else echo none @@ -119,8 +205,8 @@ fw_kind() { fw_available() { local kind; kind="$(fw_kind)" case "$kind" in - nft) ip netns exec "$AWG_NS_GW" nft list ruleset >/dev/null 2>&1 ;; - iptables) ip netns exec "$AWG_NS_GW" iptables -t mangle -L -n >/dev/null 2>&1 ;; + nft) ns_exec "$AWG_NS_GW" nft list ruleset >/dev/null 2>&1 ;; + iptables) ns_exec "$AWG_NS_GW" iptables -t mangle -L -n >/dev/null 2>&1 ;; *) return 1 ;; esac } @@ -130,8 +216,8 @@ fw_setup() { local kind; kind="$(fw_kind)" case "$kind" in nft) - ip netns exec "$AWG_NS_GW" nft delete table inet "$NFT_TABLE" 2>/dev/null || true - ip netns exec "$AWG_NS_GW" nft -f - </dev/null || true + ns_exec "$AWG_NS_GW" nft -f - </dev/null || true $ipt -t mangle -F "$IPT_CHAIN_MARK" $ipt -t mangle -A "$IPT_CHAIN_MARK" -p udp --dport "$AWG_MOCK_PORT" \ @@ -182,17 +268,17 @@ fw_set_killswitch() { local state="$1" kind; kind="$(fw_kind)" case "$kind:$state" in nft:on) - ip netns exec "$AWG_NS_GW" nft delete table inet "$NFT_TABLE" 2>/dev/null || true + ns_exec "$AWG_NS_GW" nft delete table inet "$NFT_TABLE" 2>/dev/null || true fw_setup ;; nft:off) - ip netns exec "$AWG_NS_GW" nft flush chain inet "$NFT_TABLE" forward_ks + ns_exec "$AWG_NS_GW" nft flush chain inet "$NFT_TABLE" forward_ks ;; iptables:on) fw_setup ;; iptables:off) - ip netns exec "$AWG_NS_GW" iptables -t filter -F "$IPT_CHAIN_KS" + ns_exec "$AWG_NS_GW" iptables -t filter -F "$IPT_CHAIN_KS" ;; *) fail "fw_set_killswitch: неизвестное состояние '$state' (ожидается on|off)" diff --git a/tests/netns/run.sh b/tests/netns/run.sh index ee7aa25..c6797bf 100644 --- a/tests/netns/run.sh +++ b/tests/netns/run.sh @@ -27,6 +27,12 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=tests/netns/lib.sh source "$HERE/lib.sh" +# Контракт кодов возврата: 0 — успех, 1 — тесты упали, 3 — заблокировано +# окружением, 4 — ошибка стенда. Неожиданные коды утилит (например 255 от +# `ip netns exec` в контейнере — issue #56) нормализуем в 4, чтобы CI #17 +# различал «нет прав» и «сломался стенд». +trap 'rc=$?; case "$rc" in 0|1|3|4) exit "$rc" ;; *) warn "run.sh: непредвиденный код ${rc} — трактую как ошибку стенда (4)"; exit 4 ;; esac' ERR + KEEP_TOPO=0 NO_TESTS=0 REPORT="" @@ -52,6 +58,9 @@ require_root require_cmd ip require_cmd python3 require_netns_capability +NS_EXEC_RESOLVED="$(select_ns_exec_mode)" +export AWG_NS_EXEC_MODE="$NS_EXEC_RESOLVED" +log "вход в netns: ${NS_EXEC_RESOLVED}" if [ -n "$WORKDIR_OPT" ]; then export AWG_NETNS_WORKDIR="$WORKDIR_OPT" @@ -81,10 +90,16 @@ cleanup() { [ "$CLEANED" -eq 1 ] && return 0 CLEANED=1 for pid in "$MOCK_PID" "$SINK_PID"; do - if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then - kill -TERM "$pid" 2>/dev/null || true - wait "$pid" 2>/dev/null || true - fi + [ -n "$pid" ] || continue + kill -0 "$pid" 2>/dev/null || continue + kill -TERM "$pid" 2>/dev/null || true + # до 2с на штатное завершение, затем SIGKILL — иначе прогон «висит» на wait + for _ in $(seq 1 20); do + kill -0 "$pid" 2>/dev/null || break + sleep 0.1 + done + kill -KILL "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true done if [ "$KEEP_TOPO" -eq 1 ]; then warn "топология сохранена (--keep-topology): ${AWG_NS_CLIENT} ${AWG_NS_GW} ${AWG_NS_UPLINK}" @@ -100,24 +115,25 @@ log "рабочий каталог: ${AWG_NETNS_WORKDIR}" bash "$HERE/topology.sh" up log "запускаю мок WireGuard-endpoint (udp/${AWG_MOCK_PORT}) в ${AWG_NS_UPLINK}" -ip netns exec "$AWG_NS_UPLINK" python3 "$HERE/mock_wg_endpoint.py" \ - --bind "0.0.0.0:${AWG_MOCK_PORT}" --log "$AWG_MOCK_LOG" --ready "$AWG_MOCK_READY" \ - --mode-file "$AWG_MOCK_MODE_FILE" --mode respond & +mapfile -t NS_PREFIX < <(ns_prefix "$AWG_NS_UPLINK") +( exec "${NS_PREFIX[@]}" python3 "$HERE/mock_wg_endpoint.py" \ + --bind "0.0.0.0:${AWG_MOCK_PORT}" --log "$AWG_MOCK_LOG" --ready "$AWG_MOCK_READY" \ + --mode-file "$AWG_MOCK_MODE_FILE" --mode respond ) & MOCK_PID=$! log "запускаю детектор утечки (${AWG_LEAK_IP}:${AWG_LEAK_PORT}) в ${AWG_NS_UPLINK}" -ip netns exec "$AWG_NS_UPLINK" python3 "$HERE/leak_sink.py" \ - --ip "$AWG_LEAK_IP" --port "$AWG_LEAK_PORT" --log "$AWG_LEAK_LOG" --ready "$AWG_LEAK_READY" & +( exec "${NS_PREFIX[@]}" python3 "$HERE/leak_sink.py" \ + --ip "$AWG_LEAK_IP" --port "$AWG_LEAK_PORT" --log "$AWG_LEAK_LOG" --ready "$AWG_LEAK_READY" ) & SINK_PID=$! # ждём готовности обоих процессов (файлы-флаги пишутся после bind) for _ in $(seq 1 100); do if [ -f "$AWG_MOCK_READY" ] && [ -f "$AWG_LEAK_READY" ]; then break; fi - if ! kill -0 "$MOCK_PID" 2>/dev/null; then fail "мок WireGuard-endpoint упал при старте"; fi + if ! kill -0 "$MOCK_PID" 2>/dev/null; then stand_fail "мок WireGuard-endpoint упал при старте"; fi sleep 0.1 done -[ -f "$AWG_MOCK_READY" ] || fail "мок WireGuard-endpoint не поднялся за 10с" -[ -f "$AWG_LEAK_READY" ] || fail "детектор утечки не поднялся за 10с" +[ -f "$AWG_MOCK_READY" ] || stand_fail "мок WireGuard-endpoint не поднялся за 10с" +[ -f "$AWG_LEAK_READY" ] || stand_fail "детектор утечки не поднялся за 10с" ok "мок-endpoint и детектор утечки готовы" # --- тесты -------------------------------------------------------------------