montana/Node/External-Audit/scripts/audit-probe.sh
2026-05-21 03:44:38 +03:00

154 lines
5.7 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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