netns: вход в namespace через nsenter при отказе ip netns exec (#56)

- 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
This commit is contained in:
2026-10-28 21:30:01 +03:00
parent f5e8272c3a
commit f0847001ee
2 changed files with 126 additions and 24 deletions
+98 -12
View File
@@ -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 <ns>` (iproute2 6.x) перемонтируют /sys внутри
# namespace и в контейнере падают с "mount of /sys failed: Operation not
# permitted", хотя `ip netns add` работает (issue #56). nsenter входит в тот же
# /var/run/netns/<ns> без перемонтирования — используется как 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 <netns> <команда...> — выполнить команду внутри 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> — напечатать команду входа в 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 - <<EOF
ns_exec "$AWG_NS_GW" nft delete table inet "$NFT_TABLE" 2>/dev/null || true
ns_exec "$AWG_NS_GW" nft -f - <<EOF
table inet $NFT_TABLE {
chain mark_out {
type route hook output priority mangle; policy accept;
@@ -150,7 +236,7 @@ table inet $NFT_TABLE {
EOF
;;
iptables)
local ipt="ip netns exec $AWG_NS_GW iptables"
local ipt="ns_exec $AWG_NS_GW iptables"
$ipt -t mangle -N "$IPT_CHAIN_MARK" 2>/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)"