«Если ваш VPN не детектирует зонды — он уже скомпрометирован»
Стандартная схема Reality+Xray работает по принципу «не пойман — не вор». Зонд подключается без правильного SNI → получает редирект на легитимный сайт → уходит. Но что если зонд знает ваш точный SNI? Что если он стучится не один раз, а методично, каждые 5 минут, из разных подсетей?
Я столкнулся с этим в Беларуси: за трое суток работы скрипта/детектора — ни одного «глупого» зонда с пустым SNI, только аккуратные коннекты с правильным google.com и аномальным поведением. Пришлось добавить два слоя защиты поверх стандартной схемы:
-
Детектор — анализирует nginx-лог и pcap-файлы, считает «очки подозрительности» по поведенческим признакам
-
Серый список —автоматически перенаправляет выявленные зонды на fallback-сайт, не давая им добраться до VPN
Ключевая идея: зонд не должен понимать, что его поймали. Он должен получить то, что ожидает увидеть, и уйти с мыслью «тут ничего интересного». То есть обнаруженный зонд помещается в серый список и при всех повторных попытках сканировать 443 порт, даже с верным SNI, будет выполнен принудительный редирект на fallback.
Почему просто забанить через iptables не вариант?
Казалось бы: поймали подозрительный IP → iptables -A INPUT -s 1.2.3.4 -j DROP. Но это ошибка:
|
Действие |
Что видит зонд |
Что думает оператор / система |
|---|---|---|
|
|
Таймаут или RST |
«Тут что-то скрывают → помечаем какустойчивый к зондам → добавляем вблок-лист» |
|
Перенаправление на fallback |
Обычный сайт с валидным сертификатом,нормальный TLS-хендшейк |
«Просто сайт, идём дальше» |
Вывод: лучший способ скрыть, что вас детектируют — дать зонду ожидаемый ответ.
Архитектура

Контейнеры (docker-compose)
services: vless: # xray-core, слушает 1080 fallback: # nginx, отдаёт легитимный сайт на :443 nginx_stream: # фронтенд, слушает :443, SNI-роутинг + graylist nginx-proxy: # HTTP :80, ACME challenge для letsencrypt dockergen: # генерирует конфиг nginx-proxy по меткам контейнеров letsencrypt: # acme-companion, автовыпуск сертификатов watchtower: # автообновление образов
nginx-stream.conf
user nginx;worker_processes auto;events { worker_connections 1024;}stream { resolver 127.0.0.11 valid=10s ipv6=off; # === Серый список — читается из файла на хосте === geo $suspicious { default 0; include /etc/nginx/graylist.conf; # формат: "1.2.3.4 1;" } # === SNI-роутинг === map $ssl_preread_server_name $backend { google.com vless:1080; default fallback:443; } # === Финальное решение: зонды из серого списка — только на fallback === map $suspicious $final_backend { 0 $backend; 1 fallback:443; } log_format proxy '$remote_addr -> $backend [$time_local] $status ' 'bytes=$bytes_sent/$bytes_received conn=$connection ' 'sni=$ssl_preread_server_name duration=$session_time ' 'proto=$protocol'; access_log /var/log/nginx/stream-access.log proxy; server { listen 443; ssl_preread on; # читаем SNI, не терминируя TLS proxy_pass $final_backend; # ← ключевая директива proxy_connect_timeout 5s; proxy_timeout 3600s; proxy_socket_keepalive on; }}
Ключевые моменты:
-
ssl_prereadon— nginx читает SNI из ClientHello, нерасшифровывая трафик -
geo$suspicious— подгружает серый списокиз файла на хосте (не вконтейнере) -
$final_backend— финальное решение с учётом и SNI, исерого списка -
duration=$session_time—длительность сессии, критична длядетектора
Серый список: как это работает
Файл /etc/nginx/graylist.conf — обычный список IP в формате nginx geo:
# Graylist IPs — формат: "IP 1;"80.94.95.221 1; # added 2026-04-02T14:59:20193.232.56.12 1; # added 2026-04-03T09:15:44
После добавления IP детектор выполняет:
docker exec nginx_stream nginx -s reload
Это graceful reload: nginx перечитывает конфиг за ~50 мс, не разрывая активные соединения. Зонд с этого IP при следующем подключении получит fallback-сайт. С его точки зрения — обычный HTTPS-сервер, ничего интересного.
Детектор: поведенческий анализ вместо сигнатур
|
Источник |
Что даёт |
Частота обновления |
|---|---|---|
|
nginx stream log |
SNI, backend, bytes, duration |
В реальном времени |
|
tcpdump pcap |
TCP-fingerprint: window, MSS, TTL |
В реальном времени |
Захват пакетов: tcpdump-tls.sh
#!/usr/bin/env bash# /usr/local/sbin/tcpdump-tls.shexec /usr/bin/tcpdump \ -i eth0 \ -Z root \ -w /var/log/tcpdump/tls-%s.pcap \ -G 300 \ # ротация каждые 5 минут -n \ 'tcp port 443 and tcp[tcpflags] & tcp-syn != 0' # только SYN-пакеты
Scoring: система очков подозрительности
Детектор не принимает бинарных решений. Каждый признак добавляет очки, алерт срабатывает при достижении порога (score_threshold: 5).
|
Признак |
Баллы |
Комментарий |
|---|---|---|
|
|
+2 |
Не прошёл SNI-роутинг |
|
|
+3 |
Зонд без домена (браузеры так не делают) |
|
|
+1 |
Слабый сигнал, но в плюс |
|
|
+2 |
Только хендшейк, нет данных приложения |
|
|
+3 |
Соединение оборвано сразу |
|
|
+2 |
Частые коннекты с одного IP |
|
|
+3 |
Всплеск за короткое окно |
|
|
+5 |
Главный признак: зонд появился через 1-3с после легитимного пакета |
|
|
+2..+5 |
Повторные визиты того же IP |
|
|
+3 |
Из pcap: 0, 512, 1024 — типично для сканеров |
|
|
+2 |
Из pcap |
|
|
+1 |
Из pcap: 255 или <50 |
Корреляция по времени — самый весомый признак
DPI реагирует на трафик: зонд появляется через 1-3 секунды после легитимного пакета клиента. Случайный сканер такой корреляции не даст.
14:59:15 37.212.28.144 → vless:1080 sni=google.com ← ваш клиент14:59:16 80.94.95.221 → fallback sni=- ← зонд (+5 за correlation)
Как повторить
Требования
-
✅ РаботающийVLESS+Reality setup (xray-core)
-
✅ Регистрациясубдомена на ваш IP VPS
-
✅ Docker +docker-compose на хосте
-
✅ root-доступк хосту (для iptables/tcpdump)
-
✅ Python 3.10+ с пакетами
pyyaml,dpkt
$cd /home/$USER$git clone https://github.com/segflt-wq/vless.git$cd /home/$USER/vless
Правим в docker-compose.yml переменные окружения под ваш субдомен и токен, в fallback.conf правим субдомен, в server.json генерим uuid и privateKey, меняем shortIds на свои, что нибудь еще по желанию. Настройки клиента подгоняем под сервер. Правим файл telegram под своего бота, или не правим, скрипт что нибудь напишет в логи. Детектор настроен на белорусский сегмент сети, адаптация под ru в папке ./vless/dpi-alert/ru. После всех правок делаем $./setup.sh — создаст каталоги, скопирует файлы конфигурации.
$docker compose up -d
Смотрим логи контов, материмся, донастраиваем letsencrypt и что нибудь еще…
Заключение
Защита от DPI — это не «поставил и забыл», а постоянная игра в кошки-мышки. Но даже простые эвристики (SNI + время + объём трафика) позволяют отсеивать 90% автоматических зондов.
Три главных вывода:
-
Не баньте — обманывайте. Зонд, получивший нормальный сайт, не понимает,что его поймали.
-
Поведение важнее сигнатур. Корреляция повремени ловит даже «умных» зондов справильным SNI.
-
Автоматизация — ваш друг. Серый список + graceful reloadработают без вашего участия.
Может возникнуть вопрос — а есть ли в этом смысл, ведь xray сам может отправлять на fallback трафик который не прошел reality handshake? Смысл есть — это разные слои защиты. Xray fallback отвечает на вопрос «что показать если уже достучались», детектор отвечает на вопрос «кого вообще не пускать дальше nginx». Вместе это defence‑in‑depth: зонд не только видит легитимный контент, но и активно блокируется на сетевом уровне после обнаружения паттерна.
P.S. И да, я конечно пользовался AI для подготовки материала
ссылка на оригинал статьи https://habr.com/ru/articles/1022494/