From 79cbcab397d6c38841c988458cb663692b54f8d9 Mon Sep 17 00:00:00 2001 From: embeddevio Date: Wed, 21 Oct 2026 23:47:48 +0300 Subject: [PATCH] =?UTF-8?q?test(#21):=20netns-=D1=81=D1=82=D0=B5=D0=BD?= =?UTF-8?q?=D0=B4=20=E2=80=94=20=D1=82=D0=BE=D0=BF=D0=BE=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B8=D1=8F=20client/gw/uplink,=20lib=20=D0=B8=20=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D0=B0=20=D0=B2=D1=85=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Основы харнесса для тестов без реального 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 --- tests/netns/lib.sh | 201 ++++++++++++++++++++++++++++++++++++++++ tests/netns/run.sh | 157 +++++++++++++++++++++++++++++++ tests/netns/topology.sh | 144 ++++++++++++++++++++++++++++ 3 files changed, 502 insertions(+) create mode 100644 tests/netns/lib.sh create mode 100644 tests/netns/run.sh create mode 100644 tests/netns/topology.sh diff --git a/tests/netns/lib.sh b/tests/netns/lib.sh new file mode 100644 index 0000000..de2130f --- /dev/null +++ b/tests/netns/lib.sh @@ -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 - </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 +} diff --git a/tests/netns/run.sh b/tests/netns/run.sh new file mode 100644 index 0000000..ee7aa25 --- /dev/null +++ b/tests/netns/run.sh @@ -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" diff --git a/tests/netns/topology.sh b/tests/netns/topology.sh new file mode 100644 index 0000000..53a4198 --- /dev/null +++ b/tests/netns/topology.sh @@ -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