f0847001ee
- 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
288 lines
12 KiB
Bash
288 lines
12 KiB
Bash
#!/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
|
||
}
|