Sprint 0 / #21: dev/staging стенд — docker-compose, тома, netns-харнесс #53
@@ -31,6 +31,9 @@ services:
|
||||
AWG_NETNS_PREFIX: ${AWG_NETNS_PREFIX:-awgt21}
|
||||
# nft в образе приложения нет — там iptables (см. tests/netns/README.md)
|
||||
AWG_FW_BACKEND: ${AWG_FW_BACKEND:-auto}
|
||||
# В контейнере `ip netns exec` падает (mount of /sys, issue #56) — харнесс
|
||||
# сам переключается на nsenter --net; переменная позволяет форсировать режим.
|
||||
AWG_NS_EXEC_MODE: ${AWG_NS_EXEC_MODE:-auto}
|
||||
tmpfs:
|
||||
- /tmp:mode=1777
|
||||
logging:
|
||||
|
||||
+16
-2
@@ -35,6 +35,7 @@ make test-netns # поднять стенд, прогнать т
|
||||
make test-netns-keep # оставить топологию и логи для отладки
|
||||
make test-netns NETNS_ARGS="--no-tests" # только поднять стенд
|
||||
make test-netns-ci # то же в привилегированном контейнере (путь для CI)
|
||||
# в контейнере вход в netns идёт через nsenter (issue #56)
|
||||
|
||||
bash tests/netns/topology.sh status # состояние топологии
|
||||
bash tests/netns/topology.sh down # снести топологию вручную
|
||||
@@ -43,8 +44,14 @@ bash tests/netns/topology.sh killswitch on # включить fail-closed пр
|
||||
|
||||
Требования: Linux, **root** (или CAP_NET_ADMIN + CAP_SYS_ADMIN), `ip`, `python3`;
|
||||
backend netfilter — `nft` или `iptables` (в alpine-образе приложения есть только
|
||||
`iptables`, на Debian/Ubuntu — оба). Код возврата `3` означает «заблокировано
|
||||
окружением»: нет прав на создание netns.
|
||||
`iptables`, на Debian/Ubuntu — оба).
|
||||
|
||||
Коды возврата: `0` — тесты прошли, `1` — тесты упали, `3` — «заблокировано окружением»
|
||||
(нет прав на создание netns), `4` — ошибка самого стенда (сюда же нормализуются
|
||||
неожиданные коды утилит, например `255` от `ip netns exec` в контейнере).
|
||||
Вход в namespace выполняется через `ns_exec()`: `ip netns exec` в контейнере падает
|
||||
(`mount of /sys failed`, issue #56), поэтому харнесс сам переключается на
|
||||
`nsenter --net=/var/run/netns/<ns>`; режим форсируется `AWG_NS_EXEC_MODE`.
|
||||
|
||||
## Состав
|
||||
|
||||
@@ -85,11 +92,18 @@ backend netfilter — `nft` или `iptables` (в alpine-образе прило
|
||||
| `AWG_FWMARK` / `AWG_ROUTE_TABLE` | `51820` / `51820` | метка и таблица policy routing |
|
||||
| `AWG_FW_BACKEND` | `auto` | `nft`, `iptables` или автоопределение |
|
||||
| `AWG_NETNS_WORKDIR` | `mktemp -d` | каталог логов и отчёта стенда |
|
||||
| `AWG_NS_EXEC_MODE` | `auto` | вход в netns: `ip`, `nsenter` или `auto` (проба) — issue #56 |
|
||||
|
||||
## Pitfalls
|
||||
|
||||
* **`ip netns add` в контейнере** требует `--privileged` (CAP_SYS_ADMIN) — иначе
|
||||
`run.sh` завершится кодом 3 с сообщением «заблокировано окружением».
|
||||
* **`ip netns exec` и `ip -n <ns>` в контейнере не работают** (iproute2 6.x
|
||||
перемонтирует `/sys`: `mount of /sys failed: Operation not permitted`, rc=255),
|
||||
хотя `ip netns add` и `nsenter --net=...` работают. Поэтому в скриптах только
|
||||
`ns_exec`/`ns_prefix` (`lib.sh`) и `ns_exec_mode()` (`netns_common.py`), а все
|
||||
`ip -n <ns>` заменены на `ns_exec <ns> ip ...` (issue #56). Форсировать режим:
|
||||
`AWG_NS_EXEC_MODE=nsenter|ip`.
|
||||
* **rp_filter** в шлюзе выставлен в `2` (loose): маркированная политика даёт
|
||||
асимметрию reverse-пути, strict-режим молча дропает ответы.
|
||||
* **Мок не умеет криптографию**: он повторяет раскладку сообщений WireGuard
|
||||
|
||||
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any, Iterable
|
||||
@@ -61,6 +62,10 @@ class StandError(RuntimeError):
|
||||
"""Ошибка стенда (не теста): топология/утилиты не отвечают как ожидается."""
|
||||
|
||||
|
||||
# Кэш выбранного режима входа в netns (ip|nsenter), см. ns_exec_mode().
|
||||
_NS_EXEC_MODE: str | None = None
|
||||
|
||||
|
||||
def sh(args: Iterable[str], check: bool = True, timeout: float = 20.0) -> subprocess.CompletedProcess:
|
||||
"""Запуск команды в корневом netns."""
|
||||
cp = subprocess.run(
|
||||
@@ -75,9 +80,56 @@ def sh(args: Iterable[str], check: bool = True, timeout: float = 20.0) -> subpro
|
||||
return cp
|
||||
|
||||
|
||||
def ns_exec_direct(
|
||||
mode: str,
|
||||
ns: str,
|
||||
args: Iterable[str],
|
||||
check: bool = True,
|
||||
timeout: float = 20.0,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""Одна попытка входа в netns выбранным способом (ip|nsenter)."""
|
||||
cmd = (["nsenter", f"--net=/var/run/netns/{ns}"] if mode == "nsenter" else ["ip", "netns", "exec", ns]) + list(args)
|
||||
return sh(cmd, check=check, timeout=timeout)
|
||||
|
||||
|
||||
def _probe_ns_exec(mode: str) -> bool:
|
||||
probe = os.environ.get("AWG_NETNS_PREFIX", "awgt21") + "-probe"
|
||||
if sh(["ip", "netns", "add", probe], check=False).returncode != 0:
|
||||
return False
|
||||
try:
|
||||
return ns_exec_direct(mode, probe, ["sh", "-c", "true"], check=False, timeout=10.0).returncode == 0
|
||||
finally:
|
||||
sh(["ip", "netns", "delete", probe], check=False)
|
||||
|
||||
|
||||
def ns_exec_mode() -> str:
|
||||
"""Режим входа в netns: ip | nsenter (issue #56).
|
||||
|
||||
`ip netns exec` (iproute2 6.x) перемонтирует /sys и в контейнере падает с
|
||||
"mount of /sys failed: Operation not permitted"; nsenter входит в
|
||||
/var/run/netns/<ns> без перемонтирования. Режим кэшируется на процесс,
|
||||
AWG_NS_EXEC_MODE=ip|nsenter|auto переопределяет выбор.
|
||||
"""
|
||||
global _NS_EXEC_MODE
|
||||
if _NS_EXEC_MODE is None:
|
||||
want = os.environ.get("AWG_NS_EXEC_MODE", "auto")
|
||||
if want in ("ip", "nsenter"):
|
||||
_NS_EXEC_MODE = want
|
||||
elif _probe_ns_exec("ip"):
|
||||
_NS_EXEC_MODE = "ip"
|
||||
elif shutil.which("nsenter") and _probe_ns_exec("nsenter"):
|
||||
_NS_EXEC_MODE = "nsenter"
|
||||
else:
|
||||
raise StandError(
|
||||
"ни 'ip netns exec', ни 'nsenter --net' не работают в этом окружении "
|
||||
"(нет CAP_SYS_ADMIN/NET_ADMIN) — issue #56"
|
||||
)
|
||||
return _NS_EXEC_MODE
|
||||
|
||||
|
||||
def ns_sh(ns: str, args: Iterable[str], check: bool = True, timeout: float = 20.0) -> subprocess.CompletedProcess:
|
||||
"""Запуск команды внутри указанного netns."""
|
||||
return sh(["ip", "netns", "exec", ns, *args], check=check, timeout=timeout)
|
||||
"""Запуск команды внутри указанного netns (ip или nsenter — см. ns_exec_mode)."""
|
||||
return ns_exec_direct(ns_exec_mode(), ns, args, check=check, timeout=timeout)
|
||||
|
||||
|
||||
def ns_exists(ns: str) -> bool:
|
||||
@@ -86,15 +138,15 @@ def ns_exists(ns: str) -> bool:
|
||||
|
||||
|
||||
def addrs(ns: str) -> str:
|
||||
return sh(["ip", "-n", ns, "-br", "addr", "show"]).stdout
|
||||
return ns_sh(ns, ["ip", "-br", "addr", "show"]).stdout
|
||||
|
||||
|
||||
def links(ns: str) -> str:
|
||||
return sh(["ip", "-n", ns, "-br", "link", "show"]).stdout
|
||||
return ns_sh(ns, ["ip", "-br", "link", "show"]).stdout
|
||||
|
||||
|
||||
def link_is_up(ns: str, ifname: str) -> bool:
|
||||
cp = sh(["ip", "-n", ns, "-br", "link", "show", ifname], check=False)
|
||||
cp = ns_sh(ns, ["ip", "-br", "link", "show", ifname], check=False)
|
||||
return cp.returncode == 0 and "UP" in cp.stdout
|
||||
|
||||
|
||||
|
||||
+33
-25
@@ -15,6 +15,11 @@ 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"
|
||||
}
|
||||
@@ -38,6 +43,9 @@ topology_up() {
|
||||
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
|
||||
@@ -45,44 +53,44 @@ topology_up() {
|
||||
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
|
||||
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"
|
||||
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
|
||||
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"
|
||||
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
|
||||
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 в шлюзе ---------------------------------------
|
||||
ip netns exec "$AWG_NS_GW" sysctl -q -w net.ipv4.ip_forward=1
|
||||
ns_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"
|
||||
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"
|
||||
|
||||
# --- маршруты -------------------------------------------------------------
|
||||
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"
|
||||
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)
|
||||
ip -n "$AWG_NS_UPLINK" addr add "${AWG_LEAK_IP}/32" dev lo
|
||||
ns_exec "$AWG_NS_UPLINK" ip 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"
|
||||
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
|
||||
@@ -102,18 +110,18 @@ topology_status() {
|
||||
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
|
||||
ns_exec "$ns" ip -br addr show || rc=1
|
||||
done
|
||||
printf '\n[%s] ip rule:\n' "$AWG_NS_GW"
|
||||
ip -n "$AWG_NS_GW" rule show
|
||||
ns_exec "$AWG_NS_GW" ip 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"
|
||||
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
|
||||
ip netns exec "$AWG_NS_GW" nft list table inet "$NFT_TABLE" 2>/dev/null || rc=1
|
||||
ns_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
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user