Когда сайт не открывается, браузер показывает «Не удалось установить соединение». Это всё, что он знает. Но «не открывается» - это десяток разных историй. ISP подменил DNS-ответ. Провайдер режет TCP по IP. ТСПУ читает SNI в TLS ClientHello и сбрасывает соединение. Сайт открывается, но возвращает 200 OK с заглушкой «доступ ограничен». Каждый случай требует своих действий - и, что важнее, означает разные вещи о том, где именно стоит фильтр.
В статье разберу, как именно работают четыре основных способа блокировки, и покажу маленький CLI на Python, который проверяет их по очереди и говорит «у тебя сломан слой N». Инструмент - на гитхабе, под MIT, поставить можно через pip install rkn-block-checker. Но интереснее не сам он, а то, что под капотом.
Зачем вообще диагностировать, а не обходить
Сразу проговорю, чего здесь нет. Это не про обход блокировок. Не про VPN, не про fronting, не про DPI-evasion типа zapret или GoodbyeDPI. Это про диагностику: понять, что именно сломано, чтобы знать, что чинить.
Польза от такой диагностики неочевидна, пока не попадёшь в ситуацию, когда «у меня не работает» одновременно у пяти разных людей в комнате - и причины у всех разные. У одного отравленный DNS (лечится сменой DNS-резолвера), у другого DPI на SNI (DNS не поможет, нужен fronting или VPN), у третьего вообще ISP вернул заглушку через HTTP (значит, до сайта пакеты доходят, проблема выше). Без понимания «где» - ткнуться можно в любую сторону и не угадать.
Второй сценарий - проверка качества канала. Если вы переезжаете на новую квартиру с новым провайдером, полезно за 30 секунд понять, какие именно блокировки тут активны: только DPI на SNI? Плюс DNS-poisoning? Заглушки? Это влияет на выбор стратегии (DoH хватит или нужен полный туннель).
Существующие альтернативы вроде OONI Probe делают много больше - собирают измерения в публичную базу для долгосрочного анализа. Но для вопроса «что у меня сейчас сломано» это перебор: тяжёлый клиент, обязательная регистрация измерений, непростой вывод. Хотелось чего-то размером в один pip install, что выдаёт вердикт за полминуты.
Четыре слоя, четыре способа сломать
HTTPS-запрос к сайту - это четыре независимых этапа, каждый из которых может быть атакован отдельно. Я буду идти снизу вверх.
Слой 1: DNS
Самый старый и дешёвый способ заблокировать сайт - заставить DNS соврать. Когда вы вводите protonvpn.com, ваш компьютер спрашивает у DNS-резолвера (обычно - у того, что выдал DHCP провайдера) IP-адрес. Если резолвер врёт - например, возвращает 0.0.0.0 или адрес заглушки - браузер никогда никуда не сходит. Этот метод не требует от провайдера никакого DPI-оборудования, только подкручивать свой DNS.
Распознать DNS-блокировку легко, если есть с чем сравнить. Берём результат от системного резолвера (тот, который провайдер контролирует) и сравниваем с DNS-over-HTTPS - например, https://cloudflare-dns.com/dns-query. ISP не может перехватить DoH-запрос, потому что он ходит внутри обычного HTTPS-соединения с Cloudflare. Если системный DNS сказал «не знаю такого хоста», а DoH спокойно вернул IP - это смокинг ган.
В коде это выглядит так:
import socket import requests def resolve_system(host: str) -> str | None: try: return socket.gethostbyname(host) except socket.gaierror: return None def resolve_doh(host: str) -> str | None: r = requests.get( "https://cloudflare-dns.com/dns-query", params={"name": host, "type": "A"}, headers={"accept": "application/dns-json"}, timeout=5, ) for ans in r.json().get("Answer", []): if ans.get("type") == 1: # A record return ans.get("data") return None
Логика вердикта:
система вернула IP, DoH вернул тот же IP - DNS чистый;
система вернула IP, DoH вернул другой IP - есть подозрение на manipulation, но не факт (это может быть просто CDN с разной геолокацией);
система вернула
None, DoH вернул IP - DNS-блокировка, лечится сменой DNS на 1.1.1.1 или 8.8.8.8 (или DoH на постоянной основе);оба вернули
None- сайт реально лежит или его нет.
Этот метод покрывает старые блокировки нулевых-десятых годов и до сих пор актуален для части регионов. Но в крупных городах его уже почти не встретишь - там работают на другом уровне.
Слой 2: TCP
Шаг сложнее: блокировать по IP. ISP может слать RST на любой пакет, идущий на определённый адрес, или просто молча дропать. Это делается на маршрутизаторе провайдера и не требует разбора содержимого пакетов - достаточно ACL.
Проверяется тривиально: пробуем установить TCP-соединение на порт 443 (HTTPS) и смотрим, что происходит.
import socket import time def check_tcp(host: str, port: int = 443, timeout: float = 5.0): start = time.monotonic() try: with socket.create_connection((host, port), timeout=timeout): return True, (time.monotonic() - start) * 1000, None except socket.timeout: return False, None, "timeout" except ConnectionResetError: return False, None, "connection reset" except OSError as e: return False, None, f"{type(e).__name__}: {e}"
Три исхода:
Всё ок, TCP-handshake завершился - переходим к TLS;
Connection reset на стадии handshake - IP-уровневая блокировка, провайдер шлёт
RST. Сейчас редкость, потому что массовый RST по IP неудобен (CDN, общие хостинги). Применяется обычно к точечным целям;Timeout - пакеты молча дропаются. Опять же, для отдельных IP-адресов, а не для целых сайтов.
На практике в 2026 году чистый TCP-RST по IP встречается редко - провайдерам выгоднее работать выше по стеку. Но для отдельных серверов (например, выходных нод Tor) это до сих пор актуально.
Слой 3: TLS
Здесь начинается самое интересное. Современное ТСПУ-оборудование не блокирует TCP. Оно пропускает SYN, SYN-ACK, ACK - соединение открывается. И только когда клиент шлёт первый TLS-пакет (ClientHello), middlebox разбирает его, читает поле SNI и принимает решение.
Server Name Indication - это расширение TLS, в котором клиент в открытом виде сообщает серверу, к какому хосту он обращается. Нужно это для того, чтобы один IP мог обслуживать сотни сайтов: сервер должен знать, какой именно сертификат предъявить. ClientHello отправляется до того, как соединение зашифровано, поэтому SNI читается всеми, кто стоит на пути.
Дальше middlebox делает одно из двух: шлёт RST обеим сторонам, или просто перестаёт пропускать пакеты. С точки зрения клиента это выглядит так: TCP-соединение установилось чисто (пинг-понг успешен), отправили ClientHello, и тут - либо ConnectionResetError, либо socket.timeout.
Это и есть отпечаток DPI на SNI:
TCP_OK + TLS_FAILED → скорее всего, ТСПУ
Никакая другая комбинация так не выглядит. Если бы блокировка была на уровне DNS, мы бы не дошли до TCP. Если бы по IP - TCP не открылся бы. А вот «соединение есть, но как только сказал кому именно - всё рвётся» - это конкретно про инспекцию SNI.
Код проверки:
import socket import ssl import time def check_tls(host: str, port: int = 443, timeout: float = 5.0): ctx = ssl.create_default_context() start = time.monotonic() try: with socket.create_connection((host, port), timeout=timeout) as sock: with ctx.wrap_socket(sock, server_hostname=host) as ssock: return True, (time.monotonic() - start) * 1000, None except socket.timeout: return False, None, "timeout" except ssl.SSLError as e: return False, None, f"SSLError: {e.reason}" except ConnectionResetError: return False, None, "connection reset during TLS"
Важный момент: server_hostname=host - это и есть передача SNI. Без него (или с подменённым SNI) middlebox не увидит запрещённое имя и пропустит. На этом построены некоторые техники обхода: domain fronting, ECH (Encrypted Client Hello), фрагментация ClientHello. Но это уже про другую статью.
В TLS 1.3 был шанс убить SNI как атрибут - придумали ECH, который шифрует ClientHello целиком. Но deployment его пока скорее экспериментальный, и middleboxes научились реагировать на сам факт ECH (например, рвать соединение, если видят ECH-расширение). Пока что SNI остаётся главной точкой инспекции.
Слой 4: HTTP
Иногда блокировка пропускает всё - DNS, TCP, TLS - но возвращает не то, что должна. Это два сценария.
HTTP 451. Код «Unavailable For Legal Reasons», добавленный в RFC 7725 специально для таких случаев. По задумке - честный способ сказать «доступ закрыт по решению суда». На практике встречается редко, но если встретился - это явный маркер.
ISP stub-page. ISP перехватывает HTTPS, выдаёт свой сертификат (что вызвало бы ошибку TLS, но - нет, обычно делают это только для не-HTTPS-запросов или подменяют DNS, чтобы вы пришли на их сервер) и отдаёт страницу с текстом вроде «Доступ ограничен по решению Роскомнадзора» со статусом 200 OK. Браузер показывает её как обычную страницу - никакой ошибки нет, просто содержимое не то.
Проверка по-прежнему простая: сделать GET на нужный URL и посмотреть, что в теле. Если там встречаются маркеры заглушек - значит, заглушка.
STUB_MARKERS = ( "доступ ограничен", "решению роскомнадзора", "решением суда", "заблокирован", "blocked by", "rkn.gov.ru", "единый реестр", ) def looks_like_stub(body: str) -> bool: body_lower = body.lower() return any(marker in body_lower for marker in STUB_MARKERS)
Точность - не 100%. Теоретически можно представить сайт, который случайно содержит фразу «доступ ограничен» в обычном контексте. На практике false-positive я ни разу не видел, но в продакшене такой эвристике я бы не доверил критичные решения.
Как из этого собирается вердикт
Логика «обхода» по слоям получается прямая: идём снизу вверх и останавливаемся на первом сломанном.
DNS resolve (system) ↓ ok DNS resolve (DoH) ↓ совпадает TCP connect :443 ↓ ok TLS handshake (с SNI) ↓ ok HTTP GET ↓ статус 200 + не заглушка = OK
На каждом шаге, если что-то сломалось, выдаём свой вердикт:
DNS система failed, DoH ok →
DNS_BLOCKTCP RST →
TCP_RESETTCP timeout →
TIMEOUTTCP ok, TLS RST/timeout →
TLS_BLOCK(отпечаток DPI)HTTP 451 или маркеры в теле →
HTTP_STUBВсё ок →
OK
Чтобы из «один сайт сломан» получился вердикт «вы в блокированной сети», нужно прогнать пачку. Я взял два списка:
Whitelist (контрольный) - сайты, которые точно должны открываться: gosuslugi, yandex, sberbank, vk, ozon, mos.ru, и так далее. Если они не открываются - у вас не блокировка, у вас сломан интернет.
Blacklist - сайты, заблокированные в РФ: Instagram, X (Twitter), LinkedIn, Discord, Tor Project, ProtonVPN, Patreon, rutracker и пр.
Если whitelist открывается на 100%, а blacklist - больше чем на 70% не открывается, выдаём «вы в блокированной сети, и вот разбивка по типам блокировок».
Параллельные пробы и стриминг вывода
Первая версия CLI делала проверки последовательно, и это было неприятно: 36 сайтов × среднее время на одну пробу - минута и больше. Очевидное решение - параллелизм через ThreadPoolExecutor:
from concurrent.futures import ThreadPoolExecutor def check_urls_parallel(urls, max_workers=10, timeout=5.0): with ThreadPoolExecutor(max_workers=max_workers) as pool: return list(pool.map( lambda kv: check_url(kv[0], kv[1], timeout), urls.items() ))
Стало быстрее в 10 раз - но появилась другая проблема. pool.map() возвращает результаты только когда все задачи завершены. То есть юзер запускает CLI, видит шапку «RKN Block Checker», а потом 10 секунд тишины - и потом сразу вся стена результатов. UX так себе.
Починилось одним переключением с pool.map() на as_completed() - функция-генератор yield-ит результаты сразу, как они приходят:
from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Iterator def iter_check_urls(urls, max_workers=10, timeout=5.0) -> Iterator[CheckResult]: with ThreadPoolExecutor(max_workers=max_workers) as pool: futures = [ pool.submit(check_url, name, url, timeout) for name, url in urls.items() ] for fut in as_completed(futures): yield fut.result()
В CLI цикл стал такой:
print_section("Whitelist (should always work)") for r in iter_check_urls(WHITE_URLS, workers, timeout): print_result(r) sys.stdout.flush() # важно - иначе питон буферизует stdout
Один тонкий момент: as_completed отдаёт результаты в порядке готовности, не в порядке входа. Для интерактивного вывода это нормально (быстрые сайты сверху, медленные - внизу), а вот для --json режима, где ожидается стабильный порядок ради воспроизводимости, я оставил отдельный wrapper:
def check_urls_parallel(urls, max_workers=10, timeout=5.0) -> list[CheckResult]: name_order = list(urls.keys()) by_name = {r.name: r for r in iter_check_urls(urls, max_workers, timeout)} return [by_name[name] for name in name_order if name in by_name]
Вторая хитрость - обе группы (whitelist и blacklist) запускаются в одном пуле потоков сразу. Пока выводятся whitelist-строки, blacklist уже параллельно работает в фоне. Когда whitelist допечатается, blacklist уже либо готов, либо почти готов - секция blacklist наливается практически мгновенно. Общее время не выросло, а воспринимается всё ощутимо живее.
Что получилось на выходе
Обычный запуск выглядит так:
====================================================================== RKN Block Checker ====================================================================== IP: 95.165.xxx.xxx ISP: AS12389 Rostelecom Location: Moscow, Moscow, RU ---------------------------------------------------------------------- Whitelist (should always work) name verdict TCP TLS PLT status ------------------------------------------------------------ gosuslugi ✓ OK 18ms 42ms 380ms 200 yandex ✓ OK 8ms 25ms 95ms 200 sberbank ✓ OK 12ms 38ms 250ms 200 ... Blacklist (RKN-restricted) name verdict TCP TLS PLT status ------------------------------------------------------------ instagram ✗ TLS BLOCK 22ms - - - └ TLS reset - DPI cutting on SNI (typical RKN/TSPU) twitter/x ✗ TLS BLOCK 24ms - - - └ TLS timeout - silent drop after ClientHello rutracker ✗ HTTP STUB 18ms 45ms 120ms 200 └ response body matches an ISP stub-page marker protonvpn ✗ DNS BLOCK - - - - └ system DNS doesn't resolve, DoH does - DNS poisoning ====================================================================== Summary ---------------------------------------------------------------------- Whitelist: 21/21 working Blacklist: 3/15 open, 12/15 blocked → You ARE in an RKN-blocked zone. Block types in the blacklist: ✗ TLS BLOCK: 8 ✗ DNS BLOCK: 2 ✗ HTTP STUB: 2 ======================================================================
Важная информация компактно: какие сайты, что именно с ними не так, на каком слое сломалось. Для скриптинга есть --json - выдаёт ту же информацию, плюс полный probe trace на каждый сайт (какие IP вернули резолверы, какой сертификат пришёл, тайминги). Удобно скармливать в jq:
# имена всех заблокированных сайтов rkn-check --json | jq -r '.blacklist[] | select(.verdict != "OK") | .name' # только DPI-блокировки (TCP жив, TLS мертв) rkn-check --json | jq '.blacklist[] | select(.verdict == "TLS_BLOCK" and .tcp_ok)'
Что не сделано и почему
Чтобы предупредить вопросы в комментариях, проговорю явно.
IPv6. Не реализовано. На практике IPv6-трафик в России до сих пор обрабатывается ТСПУ менее тщательно - у некоторых провайдеров через v6 пропускают то, что блокируется на v4. Это интересный отдельный сюжет, но требует отдельной диагностики и отдельной семантики вердиктов («v4 заблокирован, v6 открыт» - это уже не бинарный ответ). Возможно, в следующей версии.
QUIC и HTTP/3. Современные сайты всё больше переходят на QUIC (UDP, порт 443). ТСПУ работает с QUIC по своим правилам - насколько мне известно, пока чаще через полную блокировку UDP/443 в моменты ужесточений, чем через DPI на содержимом. Поддержка QUIC потребовала бы своего отдельного probe-стека.
Точечные блокировки внутри одного сайта. Многие блокировки сейчас работают не на уровне «весь домен», а «конкретный URL» или «конкретные подсети CDN». Например, YouTube не заблокирован полностью - режется только определённый CDN-префикс. Эта тула такого не увидит - если главная страница открывается, то OK.
TLS 1.3 ECH. Когда (если) ECH станет массовым, текущая логика TLS_BLOCK = DPI on SNI перестанет быть точной - SNI будет зашифрован. Сейчас это не проблема, потому что ECH мало где включен по умолчанию.
Лонгитудинальный мониторинг. Один прогон - это снимок. Чтобы отслеживать «когда именно блокировка появилась/пропала», нужно гонять rkn-check --json по cron и собирать в timeseries. Возможно, имеет смысл добавить готовый docker-compose с Grafana, но это уже другой проект.
Где взять и что почитать
GitHub: github.com/MayersScott/rkn-block-checker PyPI: pip install rkn-block-checker, потом rkn-check.
По теме блокировок и DPI рекомендую:
GFW Report - лучший русско- и англоязычный источник про устройство DPI-блокировок (на примере Китая, но многие принципы применимы);
OONI - academic-grade инструмент для измерения цензуры с публичной базой данных;
bol-van/zapret и его Wiki - практический источник про то, как именно ТСПУ инспектирует SNI и какие техники evasion работают.
Если у вас есть истории про неочевидные блокировки в вашем регионе - буду рад услышать в комментариях. Особенно интересны случаи, когда вердикт инструмента не совпадает с реальностью: false positives или, наоборот, ложно-зелёные сайты, которые по факту не работают.
Комментарии (4)

Baton34
07.05.2026 12:02а как же блокировка по длинне 16kB для tls трафика? У меня один впс попал под такое, при этом тест iperf3 проходит в обе стороны без замедления.

dyadyaSerezha
07.05.2026 12:02Один маленький вопрос. Чем отличается статус working для whitelist от статуса open для blacklist? Если вдруг ничем, то почему разные названия?
Shaman_RSHU
Посмотрите bol-van/zapret2. Толмуд там конечно поменьше, но само решение работает намного эффективнее.