#!/usr/bin/env bash # run.sh — точка входа netns-стенда awg-dash (вызывается из `make test-netns`). # # tests/netns/run.sh [опции] # # Что делает: # 1) preflight: root/CAP_NET_ADMIN/ip/python3; # 2) поднимает netns-топологию (topology.sh up); # 3) поднимает мок WireGuard-endpoint и детектор утечки внутри netns `uplink`; # 4) прогоняет интеграционные тесты стенда (tests/netns/test_netns_stand.py); # 5) сносит стенд и проверяет, что namespace'ы удалены. # # Опции: # --keep-topology не удалять топологию после прогона (для отладки: make test-netns-keep) # --keep-logs не удалять рабочий каталог с логами (по умолчанию сохраняется всегда # при провале или при --keep-topology) # --no-tests только поднять стенд и напечатать сводку # --report PATH путь к JSON-отчёту (по умолчанию $AWG_NETNS_WORKDIR/report.json) # --workdir PATH рабочий каталог стенда (логи, флаги готовности) # -h|--help эта справка # # Коды возврата: 0 — успех; 1 — тесты упали; 3 — заблокировано окружением # (нет root/CAP_NET_ADMIN); 4 — ошибка стенда. set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=tests/netns/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 NO_TESTS=0 REPORT="" WORKDIR_OPT="" usage() { sed -n '2,25p' "$HERE/run.sh" | sed 's/^# \{0,1\}//'; } while [ $# -gt 0 ]; do case "$1" in --keep-topology) KEEP_TOPO=1 ;; --keep-logs) : ;; --no-tests) NO_TESTS=1 ;; --report) REPORT="${2:?--report требует путь}"; shift ;; --workdir) WORKDIR_OPT="${2:?--workdir требует путь}"; shift ;; -h|--help) usage; exit 0 ;; *) fail "неизвестная опция: $1 (см. --help)" ;; esac shift done # --- preflight --------------------------------------------------------------- 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}" if [ -n "$WORKDIR_OPT" ]; then export AWG_NETNS_WORKDIR="$WORKDIR_OPT" else export AWG_NETNS_WORKDIR="${AWG_NETNS_WORKDIR:-$(mktemp -d -t awg-netns-XXXXXX)}" fi mkdir -p "$AWG_NETNS_WORKDIR" : "${REPORT:=${AWG_NETNS_WORKDIR}/report.json}" # пути артефактов процессов — в дочерние процессы уходят через окружение export AWG_MOCK_LOG="${AWG_NETNS_WORKDIR}/mock-endpoint.jsonl" export AWG_MOCK_READY="${AWG_NETNS_WORKDIR}/mock-endpoint.ready" export AWG_MOCK_MODE_FILE="${AWG_NETNS_WORKDIR}/mock-mode" export AWG_LEAK_LOG="${AWG_NETNS_WORKDIR}/leak-sink.jsonl" export AWG_LEAK_READY="${AWG_NETNS_WORKDIR}/leak-sink.ready" # чистое состояние артефактов прошлого прогона rm -f "$AWG_MOCK_LOG" "$AWG_MOCK_READY" "$AWG_LEAK_LOG" "$AWG_LEAK_READY" printf 'respond\n' > "$AWG_MOCK_MODE_FILE" MOCK_PID="" SINK_PID="" TEST_RC=0 CLEANED=0 cleanup() { [ "$CLEANED" -eq 1 ] && return 0 CLEANED=1 for pid in "$MOCK_PID" "$SINK_PID"; do [ -n "$pid" ] || continue kill -0 "$pid" 2>/dev/null || continue 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 done if [ "$KEEP_TOPO" -eq 1 ]; then warn "топология сохранена (--keep-topology): ${AWG_NS_CLIENT} ${AWG_NS_GW} ${AWG_NS_UPLINK}" warn "снести: bash tests/netns/topology.sh down" else bash "$HERE/topology.sh" down >/dev/null 2>&1 || true fi } trap cleanup EXIT INT TERM # --- стенд ------------------------------------------------------------------- log "рабочий каталог: ${AWG_NETNS_WORKDIR}" bash "$HERE/topology.sh" up log "запускаю мок WireGuard-endpoint (udp/${AWG_MOCK_PORT}) в ${AWG_NS_UPLINK}" 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" \ --mode-file "$AWG_MOCK_MODE_FILE" --mode respond ) & MOCK_PID=$! log "запускаю детектор утечки (${AWG_LEAK_IP}:${AWG_LEAK_PORT}) в ${AWG_NS_UPLINK}" ( exec "${NS_PREFIX[@]}" python3 "$HERE/leak_sink.py" \ --ip "$AWG_LEAK_IP" --port "$AWG_LEAK_PORT" --log "$AWG_LEAK_LOG" --ready "$AWG_LEAK_READY" ) & SINK_PID=$! # ждём готовности обоих процессов (файлы-флаги пишутся после bind) for _ in $(seq 1 100); do if [ -f "$AWG_MOCK_READY" ] && [ -f "$AWG_LEAK_READY" ]; then break; fi if ! kill -0 "$MOCK_PID" 2>/dev/null; then stand_fail "мок WireGuard-endpoint упал при старте"; fi sleep 0.1 done [ -f "$AWG_MOCK_READY" ] || stand_fail "мок WireGuard-endpoint не поднялся за 10с" [ -f "$AWG_LEAK_READY" ] || stand_fail "детектор утечки не поднялся за 10с" ok "мок-endpoint и детектор утечки готовы" # --- тесты ------------------------------------------------------------------- if [ "$NO_TESTS" -eq 1 ]; then log "--no-tests: стенд поднят, тесты пропущены" else log "прогон интеграционных тестов стенда" AWG_NETNS_RUN=1 AWG_NETNS_WORKDIR="$AWG_NETNS_WORKDIR" \ python3 "$HERE/run_tests.py" --report "$REPORT" || TEST_RC=$? fi # --- проверка сноса ---------------------------------------------------------- TOPOLOGY_REMOVED=true if [ "$KEEP_TOPO" -eq 0 ]; then cleanup trap - EXIT INT TERM for ns in "$AWG_NS_CLIENT" "$AWG_NS_GW" "$AWG_NS_UPLINK"; do if ns_exists "$ns"; then warn "после сноса остался netns ${ns} — стенд не очищен" TOPOLOGY_REMOVED=false TEST_RC=4 fi done [ "$TOPOLOGY_REMOVED" = true ] && ok "топология снесена, namespace'ы удалены" fi if [ "$NO_TESTS" -eq 1 ] && [ "$TOPOLOGY_REMOVED" = true ]; then ok "стенд поднят успешно (AC-3: netns + мок-endpoint без реального провайдера)" exit 0 fi if [ "$TEST_RC" -eq 0 ]; then ok "netns-стенд: тесты пройдены; отчёт: ${REPORT}" else warn "netns-стенд: тесты/очистка упали (код ${TEST_RC}); логи: ${AWG_NETNS_WORKDIR}" fi exit "$TEST_RC"