#!/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" # Контракт кодов возврата (issue #56): 0 — ок, 1 — проверка не прошла, # 3 — заблокировано окружением, 4 — ошибка стенда. Всё остальное, что прилетает # от утилит (например 255 от `ip netns exec`), нормализуем в 4. trap 'rc=$?; case "$rc" in 0|1|3|4) exit "$rc" ;; *) warn "topology.sh: непредвиденный код ${rc} — трактую как ошибку стенда (4)"; exit 4 ;; esac' ERR 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-концы и процессы внутри. # `ip netns delete` в контейнере иногда возвращает успех, но namespace # остаётся виден в `ip netns list` (issue #56) — поэтому повторяем удаление. local ns attempt for ns in "$AWG_NS_CLIENT" "$AWG_NS_GW" "$AWG_NS_UPLINK"; do for attempt in 1 2 3 4 5; do if ! ns_exists "$ns"; then break; fi ip netns delete "$ns" 2>/dev/null || true sleep 0.2 done if ns_exists "$ns"; then warn "netns ${ns} не удалился за 5 попыток" 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 NS_EXEC_RESOLVED="$(select_ns_exec_mode)" export AWG_NS_EXEC_MODE="$NS_EXEC_RESOLVED" log "вход в netns: ${NS_EXEC_RESOLVED}" 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" ns_exec "$ns" ip 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" ns_exec "$AWG_NS_CLIENT" ip addr add "${AWG_CLIENT_IP}/24" dev "$AWG_VETH_CLIENT" ns_exec "$AWG_NS_CLIENT" ip link set "$AWG_VETH_CLIENT" up ns_exec "$AWG_NS_GW" ip addr add "${AWG_GW_LAN_IP}/24" dev "$AWG_VETH_GW_LAN" ns_exec "$AWG_NS_GW" ip 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" ns_exec "$AWG_NS_GW" ip addr add "${AWG_GW_WAN_IP}/24" dev "$AWG_VETH_GW_WAN" ns_exec "$AWG_NS_GW" ip link set "$AWG_VETH_GW_WAN" up ns_exec "$AWG_NS_UPLINK" ip addr add "${AWG_UPLINK_IP}/24" dev "$AWG_VETH_UPLINK" ns_exec "$AWG_NS_UPLINK" ip link set "$AWG_VETH_UPLINK" up # --- forwarding и rp_filter в шлюзе --------------------------------------- ns_exec "$AWG_NS_GW" sysctl -q -w net.ipv4.ip_forward=1 # loose rp_filter: маркированная политика даёт асимметрию reverse-пути ns_exec "$AWG_NS_GW" sysctl -q -w net.ipv4.conf.all.rp_filter=2 ns_exec "$AWG_NS_GW" sysctl -q -w "net.ipv4.conf.${AWG_VETH_GW_LAN}.rp_filter=2" ns_exec "$AWG_NS_GW" sysctl -q -w "net.ipv4.conf.${AWG_VETH_GW_WAN}.rp_filter=2" # --- маршруты ------------------------------------------------------------- ns_exec "$AWG_NS_CLIENT" ip route add default via "$AWG_GW_LAN_IP" dev "$AWG_VETH_CLIENT" ns_exec "$AWG_NS_GW" ip route add default via "$AWG_UPLINK_IP" dev "$AWG_VETH_GW_WAN" metric 100 ns_exec "$AWG_NS_UPLINK" ip route add "$AWG_NET_CLIENT" via "$AWG_GW_WAN_IP" dev "$AWG_VETH_UPLINK" # «публичный» адрес-приманка для детектора утечки (RFC 5737 TEST-NET-2) ns_exec "$AWG_NS_UPLINK" ip addr add "${AWG_LEAK_IP}/32" dev lo # --- policy routing: fwmark 51820 -> table 51820 -> аплинк ---------------- ns_exec "$AWG_NS_GW" ip route add default via "$AWG_UPLINK_IP" dev "$AWG_VETH_GW_WAN" table "$AWG_ROUTE_TABLE" ns_exec "$AWG_NS_GW" ip 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" ns_exec "$ns" ip -br addr show || rc=1 done printf '\n[%s] ip rule:\n' "$AWG_NS_GW" ns_exec "$AWG_NS_GW" ip rule show printf '\n[%s] table %s:\n' "$AWG_NS_GW" "$AWG_ROUTE_TABLE" ns_exec "$AWG_NS_GW" ip route show table "$AWG_ROUTE_TABLE" printf '\n[%s] netfilter:\n' "$AWG_NS_GW" if [ "$(fw_kind)" = nft ]; then ns_exec "$AWG_NS_GW" nft list table inet "$NFT_TABLE" 2>/dev/null || rc=1 else ns_exec "$AWG_NS_GW" iptables -t mangle -S 2>/dev/null || rc=1 ns_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