montana/Node/External-Audit/scripts/audit-probe.sh

154 lines
5.7 KiB
Bash
Raw Normal View History

2026-05-21 03:44:38 +03:00
#!/usr/bin/env bash
# Внешний read-only probe VPN-сети Montana для аудитора.
# Не требует прав, не модифицирует ничего, работает с любого хоста.
#
# Запуск: bash audit-probe.sh
# Output: человекочитаемый отчёт + код выхода (0 = всё зелёное, 1 = есть failed)
set -u
HOST="cdn.montana.quest"
SNI="www.googletagmanager.com"
EXPECTED_PBK="EkTs2aGKnFNgFZ0f7wgft2sJp3VjwFQqIrwkZKM4gD8"
EXPECTED_SID="302805bc0c25e504"
EXPECTED_UUID="e6d355e2-2d79-4c96-a373-3b0e6b6f4b0d"
FAIL=0
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[0;33m'; NC='\033[0m'
pass() { echo -e " ${GREEN}${NC} $*"; }
fail() { echo -e " ${RED}${NC} $*"; FAIL=$((FAIL+1)); }
warn() { echo -e " ${YELLOW}!${NC} $*"; }
echo "=== Montana VPN Audit Probe ==="
echo "Время: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo
# P-1: multi-A DNS
echo "[P-1] DNS multi-A для $HOST"
mapfile -t IPS < <(dig @1.1.1.1 +short "$HOST" A 2>/dev/null | sort)
if [ "${#IPS[@]}" -ge 2 ]; then
pass "получено ${#IPS[@]} A-records: ${IPS[*]}"
else
fail "получено только ${#IPS[@]} A-records"
fi
TTL=$(dig @1.1.1.1 "$HOST" A 2>/dev/null | awk '/IN[ \t]+A[ \t]/ {print $2; exit}')
if [ -n "$TTL" ] && [ "$TTL" -le 300 ]; then
pass "TTL=${TTL}s ≤ 300"
else
fail "TTL=${TTL:-?} > 300 (медленная реакция на отказ)"
fi
echo
# P-2: TLS-handshake к каждому IP
echo "[P-2] TLS Reality probe на каждый IP"
for ip in "${IPS[@]}"; do
result=$(timeout 5 openssl s_client -connect "$ip:443" -servername "$SNI" -tls1_3 </dev/null 2>&1)
if echo "$result" | grep -q "Verification: OK" && echo "$result" | grep -q "google"; then
pass "$ip — Verification OK, google cert chain"
else
fail "$ip — handshake failed или не Google cert"
fi
done
echo
# P-3: subscription соответствует ожидаемому ключу
echo "[P-3] /vpn/sub отдаёт правильный URL"
sub=$(curl -s --max-time 10 https://montana.quest/vpn/sub | base64 -d 2>/dev/null)
if echo "$sub" | grep -q "$EXPECTED_UUID"; then pass "UUID совпадает"; else fail "UUID не совпадает"; fi
if echo "$sub" | grep -q "$EXPECTED_PBK"; then pass "pbk совпадает"; else fail "pbk не совпадает"; fi
if echo "$sub" | grep -q "$EXPECTED_SID"; then pass "sid совпадает"; else fail "sid не совпадает"; fi
url_count=$(echo "$sub" | grep -c "^vless://")
if [ "$url_count" = "1" ]; then pass "ровно одна vless:// ссылка (правило 1 ключ)"; else fail "$url_count vless:// ссылок"; fi
echo
# P-4: /vpn/node/pool согласован с DNS
echo "[P-4] /vpn/node/pool == DNS"
pool_ips=$(curl -s --max-time 10 https://montana.quest/vpn/node/pool 2>/dev/null \
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(" ".join(sorted([r["ip"] for r in d.get("records",[])])))' 2>/dev/null)
dns_ips=$(printf "%s\n" "${IPS[@]}" | sort | xargs)
if [ "$pool_ips" = "$dns_ips" ]; then
pass "pool == DNS: $pool_ips"
else
fail "pool ($pool_ips) != DNS ($dns_ips) — рассинхрон!"
fi
echo
# P-5: health endpoint
echo "[P-5] /vpn/node/health"
health=$(curl -s --max-time 5 https://montana.quest/vpn/node/health)
if echo "$health" | python3 -c 'import sys,json; d=json.load(sys.stdin); assert d.get("ok"); assert d.get("in_pool",0)>=1' 2>/dev/null; then
pass "health=$health"
else
fail "health=$health"
fi
echo
# P-6: Reality fallback на реальный Google
echo "[P-6] Reality fallback на www.googletagmanager.com"
for ip in "${IPS[@]}"; do
code=$(curl -sk --max-time 5 --resolve "$SNI:443:$ip" "https://$SNI/" -o /dev/null -w "%{http_code}")
if [ "$code" = "200" ]; then
pass "$ip$SNI HTTP $code"
else
warn "$ip$SNI HTTP $code (не критично, Google может отвечать редиректами)"
fi
done
echo
# S-1: нет утечек privateKey в публично-доступных артефактах
echo "[S-1] privateKey не утёк в /vpn/sub и /vpn/"
for url in https://montana.quest/vpn/sub https://montana.quest/vpn/ https://montana.quest/vpn/node/pool; do
body=$(curl -s --max-time 10 "$url")
if echo "$body" | grep -qE "cL7D6FCqH5|sAkv96ITECqm|montana-vpn-privkey"; then
fail "URL $url содержит подозреваемый privateKey fragment"
else
pass "URL $url — без privateKey"
fi
done
echo
# S-3: открытые порты узла (nmap)
echo "[S-3] открытые порты узлов (nmap)"
if command -v nmap >/dev/null 2>&1; then
for ip in "${IPS[@]}"; do
open=$(nmap -Pn -p 22,80,443,8444,3306,5432,6379,5000 --open -T4 --host-timeout 30s "$ip" 2>/dev/null \
| awk '/open/ {print $1}' | xargs)
pass "$ip open: $open"
if echo "$open" | grep -qE "3306|5432|6379|5000"; then
fail "$ip имеет подозрительные открытые порты (БД/admin)"
fi
done
else
warn "nmap не установлен, S-3 пропущен"
fi
echo
# Q-1: latency handshake
echo "[Q-1] средний TLS handshake"
for ip in "${IPS[@]}"; do
total=0; n=3
for i in $(seq 1 $n); do
t=$( { time timeout 4 openssl s_client -connect "$ip:443" -servername "$SNI" -tls1_3 </dev/null >/dev/null 2>&1; } 2>&1 | awk '/real/{print $2}')
ms=$(echo "$t" | awk -F'[ms]' '{print $1*60000+$2*1000}')
total=$((total+${ms%.*}))
done
avg=$((total/n))
if [ "$avg" -lt 1000 ]; then
pass "$ip avg=${avg}ms"
else
warn "$ip avg=${avg}ms (> 1s)"
fi
done
echo
# Итог
echo "=== Итог ==="
if [ "$FAIL" -eq 0 ]; then
echo -e "${GREEN}ВСЕ ПРОВЕРКИ ЗЕЛЁНЫЕ${NC}"
exit 0
else
echo -e "${RED}FAILED проверок: $FAIL${NC}"
exit 1
fi