Sprint 0 / #21: dev/staging стенд — docker-compose, тома, netns-харнесс #53
+98
-12
@@ -67,6 +67,92 @@ fail() { printf '%s[fail]%s %s\n' "$_c_red" "$_c_off" "$*" >&2; exit 1; }
|
|||||||
# см. deploy/dev-stand.md. 4 — внутренняя ошибка стенда.
|
# см. deploy/dev-stand.md. 4 — внутренняя ошибка стенда.
|
||||||
blocked() { printf '%s[blocked]%s %s\n' "$_c_yel" "$_c_off" "$*" >&2; exit 3; }
|
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 <ns>` (iproute2 6.x) перемонтируют /sys внутри
|
||||||
|
# namespace и в контейнере падают с "mount of /sys failed: Operation not
|
||||||
|
# permitted", хотя `ip netns add` работает (issue #56). nsenter входит в тот же
|
||||||
|
# /var/run/netns/<ns> без перемонтирования — используется как 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 <netns> <команда...> — выполнить команду внутри 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> — напечатать команду входа в 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() {
|
require_root() {
|
||||||
[ "$(id -u)" -eq 0 ] || blocked "нужен root (или CAP_NET_ADMIN): запустите под sudo"
|
[ "$(id -u)" -eq 0 ] || blocked "нужен root (или CAP_NET_ADMIN): запустите под sudo"
|
||||||
@@ -98,18 +184,18 @@ fw_kind() {
|
|||||||
local want="${AWG_FW_BACKEND:-auto}"
|
local want="${AWG_FW_BACKEND:-auto}"
|
||||||
case "$want" in
|
case "$want" in
|
||||||
nft)
|
nft)
|
||||||
ip netns exec "$AWG_NS_GW" sh -c 'command -v nft' >/dev/null 2>&1 && { echo nft; return; }
|
ns_exec "$AWG_NS_GW" sh -c 'command -v nft' >/dev/null 2>&1 && { echo nft; return; }
|
||||||
echo none; return
|
echo none; return
|
||||||
;;
|
;;
|
||||||
iptables)
|
iptables)
|
||||||
ip netns exec "$AWG_NS_GW" sh -c 'command -v iptables' >/dev/null 2>&1 && { echo iptables; return; }
|
ns_exec "$AWG_NS_GW" sh -c 'command -v iptables' >/dev/null 2>&1 && { echo iptables; return; }
|
||||||
echo none; return
|
echo none; return
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
# command -v — builtin shell, поэтому проверяем через sh -c внутри netns
|
# command -v — builtin shell, поэтому проверяем через sh -c внутри netns
|
||||||
if ip netns exec "$AWG_NS_GW" sh -c 'command -v nft' >/dev/null 2>&1; then
|
if ns_exec "$AWG_NS_GW" sh -c 'command -v nft' >/dev/null 2>&1; then
|
||||||
echo nft
|
echo nft
|
||||||
elif ip netns exec "$AWG_NS_GW" sh -c 'command -v iptables' >/dev/null 2>&1; then
|
elif ns_exec "$AWG_NS_GW" sh -c 'command -v iptables' >/dev/null 2>&1; then
|
||||||
echo iptables
|
echo iptables
|
||||||
else
|
else
|
||||||
echo none
|
echo none
|
||||||
@@ -119,8 +205,8 @@ fw_kind() {
|
|||||||
fw_available() {
|
fw_available() {
|
||||||
local kind; kind="$(fw_kind)"
|
local kind; kind="$(fw_kind)"
|
||||||
case "$kind" in
|
case "$kind" in
|
||||||
nft) ip netns exec "$AWG_NS_GW" nft list ruleset >/dev/null 2>&1 ;;
|
nft) ns_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 ;;
|
iptables) ns_exec "$AWG_NS_GW" iptables -t mangle -L -n >/dev/null 2>&1 ;;
|
||||||
*) return 1 ;;
|
*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -130,8 +216,8 @@ fw_setup() {
|
|||||||
local kind; kind="$(fw_kind)"
|
local kind; kind="$(fw_kind)"
|
||||||
case "$kind" in
|
case "$kind" in
|
||||||
nft)
|
nft)
|
||||||
ip netns exec "$AWG_NS_GW" nft delete table inet "$NFT_TABLE" 2>/dev/null || true
|
ns_exec "$AWG_NS_GW" nft delete table inet "$NFT_TABLE" 2>/dev/null || true
|
||||||
ip netns exec "$AWG_NS_GW" nft -f - <<EOF
|
ns_exec "$AWG_NS_GW" nft -f - <<EOF
|
||||||
table inet $NFT_TABLE {
|
table inet $NFT_TABLE {
|
||||||
chain mark_out {
|
chain mark_out {
|
||||||
type route hook output priority mangle; policy accept;
|
type route hook output priority mangle; policy accept;
|
||||||
@@ -150,7 +236,7 @@ table inet $NFT_TABLE {
|
|||||||
EOF
|
EOF
|
||||||
;;
|
;;
|
||||||
iptables)
|
iptables)
|
||||||
local ipt="ip netns exec $AWG_NS_GW iptables"
|
local ipt="ns_exec $AWG_NS_GW iptables"
|
||||||
$ipt -t mangle -N "$IPT_CHAIN_MARK" 2>/dev/null || true
|
$ipt -t mangle -N "$IPT_CHAIN_MARK" 2>/dev/null || true
|
||||||
$ipt -t mangle -F "$IPT_CHAIN_MARK"
|
$ipt -t mangle -F "$IPT_CHAIN_MARK"
|
||||||
$ipt -t mangle -A "$IPT_CHAIN_MARK" -p udp --dport "$AWG_MOCK_PORT" \
|
$ipt -t mangle -A "$IPT_CHAIN_MARK" -p udp --dport "$AWG_MOCK_PORT" \
|
||||||
@@ -182,17 +268,17 @@ fw_set_killswitch() {
|
|||||||
local state="$1" kind; kind="$(fw_kind)"
|
local state="$1" kind; kind="$(fw_kind)"
|
||||||
case "$kind:$state" in
|
case "$kind:$state" in
|
||||||
nft:on)
|
nft:on)
|
||||||
ip netns exec "$AWG_NS_GW" nft delete table inet "$NFT_TABLE" 2>/dev/null || true
|
ns_exec "$AWG_NS_GW" nft delete table inet "$NFT_TABLE" 2>/dev/null || true
|
||||||
fw_setup
|
fw_setup
|
||||||
;;
|
;;
|
||||||
nft:off)
|
nft:off)
|
||||||
ip netns exec "$AWG_NS_GW" nft flush chain inet "$NFT_TABLE" forward_ks
|
ns_exec "$AWG_NS_GW" nft flush chain inet "$NFT_TABLE" forward_ks
|
||||||
;;
|
;;
|
||||||
iptables:on)
|
iptables:on)
|
||||||
fw_setup
|
fw_setup
|
||||||
;;
|
;;
|
||||||
iptables:off)
|
iptables:off)
|
||||||
ip netns exec "$AWG_NS_GW" iptables -t filter -F "$IPT_CHAIN_KS"
|
ns_exec "$AWG_NS_GW" iptables -t filter -F "$IPT_CHAIN_KS"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
fail "fw_set_killswitch: неизвестное состояние '$state' (ожидается on|off)"
|
fail "fw_set_killswitch: неизвестное состояние '$state' (ожидается on|off)"
|
||||||
|
|||||||
+25
-9
@@ -27,6 +27,12 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
# shellcheck source=tests/netns/lib.sh
|
# shellcheck source=tests/netns/lib.sh
|
||||||
source "$HERE/lib.sh"
|
source "$HERE/lib.sh"
|
||||||
|
|
||||||
|
# Контракт кодов возврата: 0 — успех, 1 — тесты упали, 3 — заблокировано
|
||||||
|
# окружением, 4 — ошибка стенда. Неожиданные коды утилит (например 255 от
|
||||||
|
# `ip netns exec` в контейнере — issue #56) нормализуем в 4, чтобы CI #17
|
||||||
|
# различал «нет прав» и «сломался стенд».
|
||||||
|
trap 'rc=$?; case "$rc" in 0|1|3|4) exit "$rc" ;; *) warn "run.sh: непредвиденный код ${rc} — трактую как ошибку стенда (4)"; exit 4 ;; esac' ERR
|
||||||
|
|
||||||
KEEP_TOPO=0
|
KEEP_TOPO=0
|
||||||
NO_TESTS=0
|
NO_TESTS=0
|
||||||
REPORT=""
|
REPORT=""
|
||||||
@@ -52,6 +58,9 @@ require_root
|
|||||||
require_cmd ip
|
require_cmd ip
|
||||||
require_cmd python3
|
require_cmd python3
|
||||||
require_netns_capability
|
require_netns_capability
|
||||||
|
NS_EXEC_RESOLVED="$(select_ns_exec_mode)"
|
||||||
|
export AWG_NS_EXEC_MODE="$NS_EXEC_RESOLVED"
|
||||||
|
log "вход в netns: ${NS_EXEC_RESOLVED}"
|
||||||
|
|
||||||
if [ -n "$WORKDIR_OPT" ]; then
|
if [ -n "$WORKDIR_OPT" ]; then
|
||||||
export AWG_NETNS_WORKDIR="$WORKDIR_OPT"
|
export AWG_NETNS_WORKDIR="$WORKDIR_OPT"
|
||||||
@@ -81,10 +90,16 @@ cleanup() {
|
|||||||
[ "$CLEANED" -eq 1 ] && return 0
|
[ "$CLEANED" -eq 1 ] && return 0
|
||||||
CLEANED=1
|
CLEANED=1
|
||||||
for pid in "$MOCK_PID" "$SINK_PID"; do
|
for pid in "$MOCK_PID" "$SINK_PID"; do
|
||||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
[ -n "$pid" ] || continue
|
||||||
|
kill -0 "$pid" 2>/dev/null || continue
|
||||||
kill -TERM "$pid" 2>/dev/null || true
|
kill -TERM "$pid" 2>/dev/null || true
|
||||||
|
# до 2с на штатное завершение, затем SIGKILL — иначе прогон «висит» на wait
|
||||||
|
for _ in $(seq 1 20); do
|
||||||
|
kill -0 "$pid" 2>/dev/null || break
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
kill -KILL "$pid" 2>/dev/null || true
|
||||||
wait "$pid" 2>/dev/null || true
|
wait "$pid" 2>/dev/null || true
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
if [ "$KEEP_TOPO" -eq 1 ]; then
|
if [ "$KEEP_TOPO" -eq 1 ]; then
|
||||||
warn "топология сохранена (--keep-topology): ${AWG_NS_CLIENT} ${AWG_NS_GW} ${AWG_NS_UPLINK}"
|
warn "топология сохранена (--keep-topology): ${AWG_NS_CLIENT} ${AWG_NS_GW} ${AWG_NS_UPLINK}"
|
||||||
@@ -100,24 +115,25 @@ log "рабочий каталог: ${AWG_NETNS_WORKDIR}"
|
|||||||
bash "$HERE/topology.sh" up
|
bash "$HERE/topology.sh" up
|
||||||
|
|
||||||
log "запускаю мок WireGuard-endpoint (udp/${AWG_MOCK_PORT}) в ${AWG_NS_UPLINK}"
|
log "запускаю мок WireGuard-endpoint (udp/${AWG_MOCK_PORT}) в ${AWG_NS_UPLINK}"
|
||||||
ip netns exec "$AWG_NS_UPLINK" python3 "$HERE/mock_wg_endpoint.py" \
|
mapfile -t NS_PREFIX < <(ns_prefix "$AWG_NS_UPLINK")
|
||||||
|
( exec "${NS_PREFIX[@]}" python3 "$HERE/mock_wg_endpoint.py" \
|
||||||
--bind "0.0.0.0:${AWG_MOCK_PORT}" --log "$AWG_MOCK_LOG" --ready "$AWG_MOCK_READY" \
|
--bind "0.0.0.0:${AWG_MOCK_PORT}" --log "$AWG_MOCK_LOG" --ready "$AWG_MOCK_READY" \
|
||||||
--mode-file "$AWG_MOCK_MODE_FILE" --mode respond &
|
--mode-file "$AWG_MOCK_MODE_FILE" --mode respond ) &
|
||||||
MOCK_PID=$!
|
MOCK_PID=$!
|
||||||
|
|
||||||
log "запускаю детектор утечки (${AWG_LEAK_IP}:${AWG_LEAK_PORT}) в ${AWG_NS_UPLINK}"
|
log "запускаю детектор утечки (${AWG_LEAK_IP}:${AWG_LEAK_PORT}) в ${AWG_NS_UPLINK}"
|
||||||
ip netns exec "$AWG_NS_UPLINK" python3 "$HERE/leak_sink.py" \
|
( exec "${NS_PREFIX[@]}" python3 "$HERE/leak_sink.py" \
|
||||||
--ip "$AWG_LEAK_IP" --port "$AWG_LEAK_PORT" --log "$AWG_LEAK_LOG" --ready "$AWG_LEAK_READY" &
|
--ip "$AWG_LEAK_IP" --port "$AWG_LEAK_PORT" --log "$AWG_LEAK_LOG" --ready "$AWG_LEAK_READY" ) &
|
||||||
SINK_PID=$!
|
SINK_PID=$!
|
||||||
|
|
||||||
# ждём готовности обоих процессов (файлы-флаги пишутся после bind)
|
# ждём готовности обоих процессов (файлы-флаги пишутся после bind)
|
||||||
for _ in $(seq 1 100); do
|
for _ in $(seq 1 100); do
|
||||||
if [ -f "$AWG_MOCK_READY" ] && [ -f "$AWG_LEAK_READY" ]; then break; fi
|
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
|
if ! kill -0 "$MOCK_PID" 2>/dev/null; then stand_fail "мок WireGuard-endpoint упал при старте"; fi
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
done
|
done
|
||||||
[ -f "$AWG_MOCK_READY" ] || fail "мок WireGuard-endpoint не поднялся за 10с"
|
[ -f "$AWG_MOCK_READY" ] || stand_fail "мок WireGuard-endpoint не поднялся за 10с"
|
||||||
[ -f "$AWG_LEAK_READY" ] || fail "детектор утечки не поднялся за 10с"
|
[ -f "$AWG_LEAK_READY" ] || stand_fail "детектор утечки не поднялся за 10с"
|
||||||
ok "мок-endpoint и детектор утечки готовы"
|
ok "мок-endpoint и детектор утечки готовы"
|
||||||
|
|
||||||
# --- тесты -------------------------------------------------------------------
|
# --- тесты -------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user