#!/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 ` (iproute2 6.x) перемонтируют /sys внутри # namespace и в контейнере падают с "mount of /sys failed: Operation not # permitted", хотя `ip netns add` работает (issue #56). nsenter входит в тот же # /var/run/netns/ без перемонтирования — используется как 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 <команда...> — выполнить команду внутри 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 ПО СЛОВУ НА СТРОКУ. # Нужна для фонового запуска: `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 - </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 }