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