test(#21): netns-стенд — топология client/gw/uplink, lib и точка входа
Основы харнесса для тестов без реального VPN-провайдера: - lib.sh — конфигурация стенда (netns, адреса, fwmark/таблицы) и правила netfilter с поддержкой nft и iptables (в alpine-образе приложения nft нет); - topology.sh — up/down/status/killswitch: veth-пары client<->gw<->uplink, ip_forward, ip rule fwmark 51820 -> table 51820, метки OUTPUT/FORWARD, DROP kill-switch для прямого выхода в «публичную» сеть; - run.sh — preflight, запуск мока и детектора утечки в netns uplink, прогон тестов, снос стенда с проверкой отсутствия остатков. Refs #21 Refs #15
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
#!/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; }
|
||||
|
||||
# --- предусловия ------------------------------------------------------------
|
||||
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)
|
||||
ip netns 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; }
|
||||
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
|
||||
echo nft
|
||||
elif ip netns 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) 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 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Правило метки для трафика шлюза (OUTPUT) и для транзитного трафика LAN (FORWARD).
|
||||
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
|
||||
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="ip netns 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)
|
||||
ip netns 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
|
||||
;;
|
||||
iptables:on)
|
||||
fw_setup
|
||||
;;
|
||||
iptables:off)
|
||||
ip netns exec "$AWG_NS_GW" iptables -t filter -F "$IPT_CHAIN_KS"
|
||||
;;
|
||||
*)
|
||||
fail "fw_set_killswitch: неизвестное состояние '$state' (ожидается on|off)"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
Reference in New Issue
Block a user