Files
awg-dash/tests/netns/lib.sh
T
embeddevio 79cbcab397 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
2026-10-21 23:47:48 +03:00

202 lines
8.5 KiB
Bash
Raw 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; }
# --- предусловия ------------------------------------------------------------
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
}