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
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# run.sh — точка входа netns-стенда awg-dash (вызывается из `make test-netns`).
|
||||||
|
#
|
||||||
|
# tests/netns/run.sh [опции]
|
||||||
|
#
|
||||||
|
# Что делает:
|
||||||
|
# 1) preflight: root/CAP_NET_ADMIN/ip/python3;
|
||||||
|
# 2) поднимает netns-топологию (topology.sh up);
|
||||||
|
# 3) поднимает мок WireGuard-endpoint и детектор утечки внутри netns `uplink`;
|
||||||
|
# 4) прогоняет интеграционные тесты стенда (tests/netns/test_netns_stand.py);
|
||||||
|
# 5) сносит стенд и проверяет, что namespace'ы удалены.
|
||||||
|
#
|
||||||
|
# Опции:
|
||||||
|
# --keep-topology не удалять топологию после прогона (для отладки: make test-netns-keep)
|
||||||
|
# --keep-logs не удалять рабочий каталог с логами (по умолчанию сохраняется всегда
|
||||||
|
# при провале или при --keep-topology)
|
||||||
|
# --no-tests только поднять стенд и напечатать сводку
|
||||||
|
# --report PATH путь к JSON-отчёту (по умолчанию $AWG_NETNS_WORKDIR/report.json)
|
||||||
|
# --workdir PATH рабочий каталог стенда (логи, флаги готовности)
|
||||||
|
# -h|--help эта справка
|
||||||
|
#
|
||||||
|
# Коды возврата: 0 — успех; 1 — тесты упали; 3 — заблокировано окружением
|
||||||
|
# (нет root/CAP_NET_ADMIN); 4 — ошибка стенда.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=tests/netns/lib.sh
|
||||||
|
source "$HERE/lib.sh"
|
||||||
|
|
||||||
|
KEEP_TOPO=0
|
||||||
|
NO_TESTS=0
|
||||||
|
REPORT=""
|
||||||
|
WORKDIR_OPT=""
|
||||||
|
|
||||||
|
usage() { sed -n '2,25p' "$HERE/run.sh" | sed 's/^# \{0,1\}//'; }
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--keep-topology) KEEP_TOPO=1 ;;
|
||||||
|
--keep-logs) : ;;
|
||||||
|
--no-tests) NO_TESTS=1 ;;
|
||||||
|
--report) REPORT="${2:?--report требует путь}"; shift ;;
|
||||||
|
--workdir) WORKDIR_OPT="${2:?--workdir требует путь}"; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) fail "неизвестная опция: $1 (см. --help)" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- preflight ---------------------------------------------------------------
|
||||||
|
require_root
|
||||||
|
require_cmd ip
|
||||||
|
require_cmd python3
|
||||||
|
require_netns_capability
|
||||||
|
|
||||||
|
if [ -n "$WORKDIR_OPT" ]; then
|
||||||
|
export AWG_NETNS_WORKDIR="$WORKDIR_OPT"
|
||||||
|
else
|
||||||
|
export AWG_NETNS_WORKDIR="${AWG_NETNS_WORKDIR:-$(mktemp -d -t awg-netns-XXXXXX)}"
|
||||||
|
fi
|
||||||
|
mkdir -p "$AWG_NETNS_WORKDIR"
|
||||||
|
: "${REPORT:=${AWG_NETNS_WORKDIR}/report.json}"
|
||||||
|
|
||||||
|
# пути артефактов процессов — в дочерние процессы уходят через окружение
|
||||||
|
export AWG_MOCK_LOG="${AWG_NETNS_WORKDIR}/mock-endpoint.jsonl"
|
||||||
|
export AWG_MOCK_READY="${AWG_NETNS_WORKDIR}/mock-endpoint.ready"
|
||||||
|
export AWG_MOCK_MODE_FILE="${AWG_NETNS_WORKDIR}/mock-mode"
|
||||||
|
export AWG_LEAK_LOG="${AWG_NETNS_WORKDIR}/leak-sink.jsonl"
|
||||||
|
export AWG_LEAK_READY="${AWG_NETNS_WORKDIR}/leak-sink.ready"
|
||||||
|
|
||||||
|
# чистое состояние артефактов прошлого прогона
|
||||||
|
rm -f "$AWG_MOCK_LOG" "$AWG_MOCK_READY" "$AWG_LEAK_LOG" "$AWG_LEAK_READY"
|
||||||
|
printf 'respond\n' > "$AWG_MOCK_MODE_FILE"
|
||||||
|
|
||||||
|
MOCK_PID=""
|
||||||
|
SINK_PID=""
|
||||||
|
TEST_RC=0
|
||||||
|
CLEANED=0
|
||||||
|
|
||||||
|
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
|
||||||
|
done
|
||||||
|
if [ "$KEEP_TOPO" -eq 1 ]; then
|
||||||
|
warn "топология сохранена (--keep-topology): ${AWG_NS_CLIENT} ${AWG_NS_GW} ${AWG_NS_UPLINK}"
|
||||||
|
warn "снести: bash tests/netns/topology.sh down"
|
||||||
|
else
|
||||||
|
bash "$HERE/topology.sh" down >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# --- стенд -------------------------------------------------------------------
|
||||||
|
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 &
|
||||||
|
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" &
|
||||||
|
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
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
[ -f "$AWG_MOCK_READY" ] || fail "мок WireGuard-endpoint не поднялся за 10с"
|
||||||
|
[ -f "$AWG_LEAK_READY" ] || fail "детектор утечки не поднялся за 10с"
|
||||||
|
ok "мок-endpoint и детектор утечки готовы"
|
||||||
|
|
||||||
|
# --- тесты -------------------------------------------------------------------
|
||||||
|
if [ "$NO_TESTS" -eq 1 ]; then
|
||||||
|
log "--no-tests: стенд поднят, тесты пропущены"
|
||||||
|
else
|
||||||
|
log "прогон интеграционных тестов стенда"
|
||||||
|
AWG_NETNS_RUN=1 AWG_NETNS_WORKDIR="$AWG_NETNS_WORKDIR" \
|
||||||
|
python3 "$HERE/run_tests.py" --report "$REPORT" || TEST_RC=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- проверка сноса ----------------------------------------------------------
|
||||||
|
TOPOLOGY_REMOVED=true
|
||||||
|
if [ "$KEEP_TOPO" -eq 0 ]; then
|
||||||
|
cleanup
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
for ns in "$AWG_NS_CLIENT" "$AWG_NS_GW" "$AWG_NS_UPLINK"; do
|
||||||
|
if ns_exists "$ns"; then
|
||||||
|
warn "после сноса остался netns ${ns} — стенд не очищен"
|
||||||
|
TOPOLOGY_REMOVED=false
|
||||||
|
TEST_RC=4
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ "$TOPOLOGY_REMOVED" = true ] && ok "топология снесена, namespace'ы удалены"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$NO_TESTS" -eq 1 ] && [ "$TOPOLOGY_REMOVED" = true ]; then
|
||||||
|
ok "стенд поднят успешно (AC-3: netns + мок-endpoint без реального провайдера)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$TEST_RC" -eq 0 ]; then
|
||||||
|
ok "netns-стенд: тесты пройдены; отчёт: ${REPORT}"
|
||||||
|
else
|
||||||
|
warn "netns-стенд: тесты/очистка упали (код ${TEST_RC}); логи: ${AWG_NETNS_WORKDIR}"
|
||||||
|
fi
|
||||||
|
exit "$TEST_RC"
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# topology.sh — netns-топология тестового стенда awg-dash (задача #21).
|
||||||
|
#
|
||||||
|
# topology.sh up создать netns, veth-пары, адреса, маршруты, правила меток
|
||||||
|
# topology.sh down удалить netns и veth-пары (идемпотентно)
|
||||||
|
# topology.sh status показать состояние топологии (exit 0 — стенд поднят)
|
||||||
|
# topology.sh killswitch on включить fail-closed правило в шлюзе
|
||||||
|
# topology.sh killswitch off выключить fail-closed правило в шлюзе
|
||||||
|
# topology.sh fw-kind напечатать backend netfilter (nft|iptables)
|
||||||
|
#
|
||||||
|
# Требует root и CAP_NET_ADMIN (в контейнере — --privileged либо NET_ADMIN+SYS_ADMIN).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=tests/netns/lib.sh
|
||||||
|
source "$HERE/lib.sh"
|
||||||
|
|
||||||
|
topology_present() {
|
||||||
|
ns_exists "$AWG_NS_CLIENT" && ns_exists "$AWG_NS_GW" && ns_exists "$AWG_NS_UPLINK"
|
||||||
|
}
|
||||||
|
|
||||||
|
topology_down() {
|
||||||
|
local quiet="${1:-}"
|
||||||
|
[ "$quiet" = quiet ] || log "удаляю топологию"
|
||||||
|
# netns удаляем первыми: вместе с ними уходят veth-концы и процессы внутри.
|
||||||
|
for ns in "$AWG_NS_CLIENT" "$AWG_NS_GW" "$AWG_NS_UPLINK"; do
|
||||||
|
if ns_exists "$ns"; then ip netns del "$ns" 2>/dev/null || true; fi
|
||||||
|
done
|
||||||
|
# страховка на случай, если veth остался в корневом netns
|
||||||
|
for ifc in "$AWG_VETH_CLIENT" "$AWG_VETH_GW_LAN" "$AWG_VETH_GW_WAN" "$AWG_VETH_UPLINK"; do
|
||||||
|
if ip link show "$ifc" >/dev/null 2>&1; then ip link del "$ifc" 2>/dev/null || true; fi
|
||||||
|
done
|
||||||
|
[ "$quiet" = quiet ] || ok "топология удалена"
|
||||||
|
}
|
||||||
|
|
||||||
|
topology_up() {
|
||||||
|
require_root
|
||||||
|
require_cmd ip
|
||||||
|
require_cmd python3
|
||||||
|
require_netns_capability
|
||||||
|
|
||||||
|
log "создаю netns: client=$AWG_NS_CLIENT gw=$AWG_NS_GW uplink=$AWG_NS_UPLINK"
|
||||||
|
topology_down quiet
|
||||||
|
|
||||||
|
local ns
|
||||||
|
for ns in "$AWG_NS_CLIENT" "$AWG_NS_GW" "$AWG_NS_UPLINK"; do
|
||||||
|
ip netns add "$ns"
|
||||||
|
ip -n "$ns" link set lo up
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- client <-> gw (LAN) --------------------------------------------------
|
||||||
|
ip link add "$AWG_VETH_CLIENT" type veth peer name "$AWG_VETH_GW_LAN"
|
||||||
|
ip link set "$AWG_VETH_CLIENT" netns "$AWG_NS_CLIENT"
|
||||||
|
ip link set "$AWG_VETH_GW_LAN" netns "$AWG_NS_GW"
|
||||||
|
ip -n "$AWG_NS_CLIENT" addr add "${AWG_CLIENT_IP}/24" dev "$AWG_VETH_CLIENT"
|
||||||
|
ip -n "$AWG_NS_CLIENT" link set "$AWG_VETH_CLIENT" up
|
||||||
|
ip -n "$AWG_NS_GW" addr add "${AWG_GW_LAN_IP}/24" dev "$AWG_VETH_GW_LAN"
|
||||||
|
ip -n "$AWG_NS_GW" link set "$AWG_VETH_GW_LAN" up
|
||||||
|
|
||||||
|
# --- gw <-> uplink (WAN) --------------------------------------------------
|
||||||
|
ip link add "$AWG_VETH_GW_WAN" type veth peer name "$AWG_VETH_UPLINK"
|
||||||
|
ip link set "$AWG_VETH_GW_WAN" netns "$AWG_NS_GW"
|
||||||
|
ip link set "$AWG_VETH_UPLINK" netns "$AWG_NS_UPLINK"
|
||||||
|
ip -n "$AWG_NS_GW" addr add "${AWG_GW_WAN_IP}/24" dev "$AWG_VETH_GW_WAN"
|
||||||
|
ip -n "$AWG_NS_GW" link set "$AWG_VETH_GW_WAN" up
|
||||||
|
ip -n "$AWG_NS_UPLINK" addr add "${AWG_UPLINK_IP}/24" dev "$AWG_VETH_UPLINK"
|
||||||
|
ip -n "$AWG_NS_UPLINK" link set "$AWG_VETH_UPLINK" up
|
||||||
|
|
||||||
|
# --- forwarding и rp_filter в шлюзе ---------------------------------------
|
||||||
|
ip netns exec "$AWG_NS_GW" sysctl -q -w net.ipv4.ip_forward=1
|
||||||
|
# loose rp_filter: маркированная политика даёт асимметрию reverse-пути
|
||||||
|
ip netns exec "$AWG_NS_GW" sysctl -q -w net.ipv4.conf.all.rp_filter=2
|
||||||
|
ip netns exec "$AWG_NS_GW" sysctl -q -w "net.ipv4.conf.${AWG_VETH_GW_LAN}.rp_filter=2"
|
||||||
|
ip netns exec "$AWG_NS_GW" sysctl -q -w "net.ipv4.conf.${AWG_VETH_GW_WAN}.rp_filter=2"
|
||||||
|
|
||||||
|
# --- маршруты -------------------------------------------------------------
|
||||||
|
ip -n "$AWG_NS_CLIENT" route add default via "$AWG_GW_LAN_IP" dev "$AWG_VETH_CLIENT"
|
||||||
|
ip -n "$AWG_NS_GW" route add default via "$AWG_UPLINK_IP" dev "$AWG_VETH_GW_WAN" metric 100
|
||||||
|
ip -n "$AWG_NS_UPLINK" route add "$AWG_NET_CLIENT" via "$AWG_GW_WAN_IP" dev "$AWG_VETH_UPLINK"
|
||||||
|
# «публичный» адрес-приманка для детектора утечки (RFC 5737 TEST-NET-2)
|
||||||
|
ip -n "$AWG_NS_UPLINK" addr add "${AWG_LEAK_IP}/32" dev lo
|
||||||
|
|
||||||
|
# --- policy routing: fwmark 51820 -> table 51820 -> аплинк ----------------
|
||||||
|
ip -n "$AWG_NS_GW" route add default via "$AWG_UPLINK_IP" dev "$AWG_VETH_GW_WAN" table "$AWG_ROUTE_TABLE"
|
||||||
|
ip -n "$AWG_NS_GW" rule add fwmark "$AWG_FWMARK" table "$AWG_ROUTE_TABLE"
|
||||||
|
|
||||||
|
# --- netfilter: метки + kill-switch ---------------------------------------
|
||||||
|
fw_setup
|
||||||
|
|
||||||
|
ok "стенд поднят: ${AWG_NS_CLIENT}(${AWG_CLIENT_IP}) -> ${AWG_NS_GW}(${AWG_GW_LAN_IP}/${AWG_GW_WAN_IP}) -> ${AWG_NS_UPLINK}(${AWG_UPLINK_IP})"
|
||||||
|
ok "policy routing: fwmark ${AWG_FWMARK} -> table ${AWG_ROUTE_TABLE} (default via ${AWG_UPLINK_IP})"
|
||||||
|
ok "mock endpoint: udp/${AWG_MOCK_PORT} в ${AWG_NS_UPLINK}; детектор утечки: ${AWG_LEAK_IP}:${AWG_LEAK_PORT}/udp"
|
||||||
|
}
|
||||||
|
|
||||||
|
topology_status() {
|
||||||
|
local rc=0
|
||||||
|
if ! topology_present; then
|
||||||
|
printf 'topology: down\n'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
printf 'topology: up\n'
|
||||||
|
local ns
|
||||||
|
for ns in "$AWG_NS_CLIENT" "$AWG_NS_GW" "$AWG_NS_UPLINK"; do
|
||||||
|
printf '\n[%s] links:\n' "$ns"
|
||||||
|
ip -n "$ns" -br addr show || rc=1
|
||||||
|
done
|
||||||
|
printf '\n[%s] ip rule:\n' "$AWG_NS_GW"
|
||||||
|
ip -n "$AWG_NS_GW" rule show
|
||||||
|
printf '\n[%s] table %s:\n' "$AWG_NS_GW" "$AWG_ROUTE_TABLE"
|
||||||
|
ip -n "$AWG_NS_GW" route show table "$AWG_ROUTE_TABLE"
|
||||||
|
printf '\n[%s] netfilter:\n' "$AWG_NS_GW"
|
||||||
|
if [ "$(fw_kind)" = nft ]; then
|
||||||
|
ip netns exec "$AWG_NS_GW" nft list table inet "$NFT_TABLE" 2>/dev/null || rc=1
|
||||||
|
else
|
||||||
|
ip netns exec "$AWG_NS_GW" iptables -t mangle -S 2>/dev/null || rc=1
|
||||||
|
ip netns exec "$AWG_NS_GW" iptables -t filter -S FORWARD 2>/dev/null || rc=1
|
||||||
|
fi
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-status}" in
|
||||||
|
up)
|
||||||
|
topology_up
|
||||||
|
;;
|
||||||
|
down)
|
||||||
|
require_root
|
||||||
|
topology_down
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
topology_status
|
||||||
|
;;
|
||||||
|
killswitch)
|
||||||
|
require_root
|
||||||
|
fw_set_killswitch "${2:?usage: topology.sh killswitch on|off}"
|
||||||
|
ok "kill-switch: ${2}"
|
||||||
|
;;
|
||||||
|
fw-kind)
|
||||||
|
require_root
|
||||||
|
fw_kind
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "usage: topology.sh {up|down|status|killswitch on|off|fw-kind}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user