Files
awg-dash/tests/netns/lib.sh
embeddevio f0847001ee 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
2026-10-28 21:30:01 +03:00

288 lines
12 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# lib.sh — общие настройки и утилиты netns-стенда awg-dash (задача #21, эпик #15).
#
# Подключается через `source` из topology.sh / run.sh. Ничего не выполняет при
# подключении, ничего не создаёт — только конфигурация и вспомогательные функции.
#
# Все имена/адреса переопределяются переменными окружения (см. tests/netns/README.md).
# shellcheck shell=bash
# --- имена сетевых namespace'ов ---------------------------------------------
: "${AWG_NETNS_PREFIX:=awgt21}"
: "${AWG_NS_CLIENT:=${AWG_NETNS_PREFIX}-client}" # LAN-клиент за шлюзом
: "${AWG_NS_GW:=${AWG_NETNS_PREFIX}-gw}" # хост со шлюзом awg-dash
: "${AWG_NS_UPLINK:=${AWG_NETNS_PREFIX}-uplink}" # «ISP» + мок WireGuard-endpoint
# --- адресация --------------------------------------------------------------
: "${AWG_NET_CLIENT:=10.213.0.0/24}"
: "${AWG_CLIENT_IP:=10.213.0.2}"
: "${AWG_GW_LAN_IP:=10.213.0.1}"
: "${AWG_NET_WAN:=10.214.0.0/24}"
: "${AWG_GW_WAN_IP:=10.214.0.1}"
: "${AWG_UPLINK_IP:=10.214.0.2}"
# «Провайдер»/мок-endpoint: порт как в wg-quick.
: "${AWG_MOCK_PORT:=51820}"
# Адрес из TEST-NET-2 (RFC 5737) — «публичный» получатель для детектора утечки.
: "${AWG_LEAK_IP:=198.51.100.9}"
: "${AWG_LEAK_PORT:=9999}"
: "${AWG_LEAK_NET:=198.51.100.0/24}"
# --- policy routing ---------------------------------------------------------
# Метка и таблица как в wg-quick: fwmark 51820 -> table 51820.
: "${AWG_FWMARK:=51820}"
: "${AWG_ROUTE_TABLE:=51820}"
# --- имена интерфейсов и константы стенда -----------------------------------
: "${AWG_VETH_CLIENT:=veth-c-gw}"
: "${AWG_VETH_GW_LAN:=veth-gw-lan}"
: "${AWG_VETH_GW_WAN:=veth-gw-wan}"
: "${AWG_VETH_UPLINK:=veth-uplink}"
NFT_TABLE="awgtest"
C_MARK_OUT="awgtest-mark-out"
C_MARK_FWD="awgtest-mark-fwd"
C_DROP="awgtest-killswitch-drop"
IPT_CHAIN_MARK="awgtest-mark"
IPT_CHAIN_FWD="awgtest-mark-fwd"
IPT_CHAIN_KS="awgtest-ks"
# Рабочий каталог стенда (логи мока, sink'а, отчёты). Задаётся run.sh.
: "${AWG_NETNS_WORKDIR:=/tmp/awg-netns}"
# --- вывод ------------------------------------------------------------------
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
_c_red=$'\033[31m'; _c_grn=$'\033[32m'; _c_yel=$'\033[33m'; _c_dim=$'\033[2m'; _c_off=$'\033[0m'
else
_c_red=''; _c_grn=''; _c_yel=''; _c_dim=''; _c_off=''
fi
log() { printf '%s[netns]%s %s\n' "$_c_dim" "$_c_off" "$*"; }
ok() { printf '%s[ ok ]%s %s\n' "$_c_grn" "$_c_off" "$*"; }
warn() { printf '%s[warn]%s %s\n' "$_c_yel" "$_c_off" "$*" >&2; }
fail() { printf '%s[fail]%s %s\n' "$_c_red" "$_c_off" "$*" >&2; exit 1; }
# Код возврата 3 — «заблокировано окружением» (нет root/CAP_NET_ADMIN/ip),
# см. 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"
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || blocked "нет утилиты '$1' в PATH"
}
# Проверяем, что ядро/контейнер вообще позволяют netns (иначе AC невыполним).
require_netns_capability() {
local probe="${AWG_NETNS_PREFIX}-probe"
if ! ip netns add "$probe" 2>/dev/null; then
blocked "не удалось создать netns — нет CAP_NET_ADMIN/SYS_ADMIN (запустите контейнер с --privileged)"
fi
ip netns del "$probe" 2>/dev/null || true
}
ns_exists() { ip netns list | awk '{print $1}' | grep -qx "$1"; }
# --- netfilter: метки policy routing и kill-switch --------------------------
# Стенд работает и на nftables (хост Debian/Ubuntu с nft), и на iptables
# (alpine-образ приложения: iptables без бинаря nft). Backend выбирается по
# наличию бинаря в netns шлюза; семантика правил одинаковая.
fw_kind() {
# AWG_FW_BACKEND=nft|iptables|auto — принудительный выбор backend (в alpine-образе
# приложения есть только iptables, на хосте обычно nft).
local want="${AWG_FW_BACKEND:-auto}"
case "$want" in
nft)
ns_exec "$AWG_NS_GW" sh -c 'command -v nft' >/dev/null 2>&1 && { echo nft; return; }
echo none; return
;;
iptables)
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 ns_exec "$AWG_NS_GW" sh -c 'command -v nft' >/dev/null 2>&1; then
echo nft
elif ns_exec "$AWG_NS_GW" sh -c 'command -v iptables' >/dev/null 2>&1; then
echo iptables
else
echo none
fi
}
fw_available() {
local kind; kind="$(fw_kind)"
case "$kind" in
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
}
# Правило метки для трафика шлюза (OUTPUT) и для транзитного трафика LAN (FORWARD).
fw_setup() {
local kind; kind="$(fw_kind)"
case "$kind" in
nft)
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;
udp dport $AWG_MOCK_PORT counter meta mark set $AWG_FWMARK comment "$C_MARK_OUT"
return
}
chain mark_fwd {
type filter hook forward priority mangle; policy accept;
udp dport $AWG_MOCK_PORT counter meta mark set $AWG_FWMARK comment "$C_MARK_FWD"
}
chain forward_ks {
type filter hook forward priority filter; policy accept;
ip saddr $AWG_NET_CLIENT ip daddr $AWG_LEAK_NET counter drop comment "$C_DROP"
}
}
EOF
;;
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" \
-m comment --comment "$C_MARK_OUT" -j MARK --set-mark "$AWG_FWMARK"
$ipt -t mangle -C OUTPUT -j "$IPT_CHAIN_MARK" 2>/dev/null \
|| $ipt -t mangle -A OUTPUT -j "$IPT_CHAIN_MARK"
$ipt -t mangle -N "$IPT_CHAIN_FWD" 2>/dev/null || true
$ipt -t mangle -F "$IPT_CHAIN_FWD"
$ipt -t mangle -A "$IPT_CHAIN_FWD" -p udp --dport "$AWG_MOCK_PORT" \
-m comment --comment "$C_MARK_FWD" -j MARK --set-mark "$AWG_FWMARK"
$ipt -t mangle -C FORWARD -j "$IPT_CHAIN_FWD" 2>/dev/null \
|| $ipt -t mangle -A FORWARD -j "$IPT_CHAIN_FWD"
$ipt -t filter -N "$IPT_CHAIN_KS" 2>/dev/null || true
$ipt -t filter -F "$IPT_CHAIN_KS"
$ipt -t filter -A "$IPT_CHAIN_KS" -s "$AWG_NET_CLIENT" -d "$AWG_LEAK_NET" \
-m comment --comment "$C_DROP" -j DROP
$ipt -t filter -C FORWARD -j "$IPT_CHAIN_KS" 2>/dev/null \
|| $ipt -t filter -A FORWARD -j "$IPT_CHAIN_KS"
;;
*)
fail "нет ни nft, ни iptables в ${AWG_NS_GW} — стенд не может проверить маркировку"
;;
esac
log "правила меток/kill-switch установлены (backend: $kind)"
}
# killswitch on|off — включить/выключить fail-closed правило FORWARD.
fw_set_killswitch() {
local state="$1" kind; kind="$(fw_kind)"
case "$kind:$state" in
nft:on)
ns_exec "$AWG_NS_GW" nft delete table inet "$NFT_TABLE" 2>/dev/null || true
fw_setup
;;
nft:off)
ns_exec "$AWG_NS_GW" nft flush chain inet "$NFT_TABLE" forward_ks
;;
iptables:on)
fw_setup
;;
iptables:off)
ns_exec "$AWG_NS_GW" iptables -t filter -F "$IPT_CHAIN_KS"
;;
*)
fail "fw_set_killswitch: неизвестное состояние '$state' (ожидается on|off)"
;;
esac
}