惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
月光博客
月光博客
AI
AI
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
CXSECURITY Database RSS Feed - CXSecurity.com
WordPress大学
WordPress大学
GbyAI
GbyAI
L
Lohrmann on Cybersecurity
O
OpenAI News
Schneier on Security
Schneier on Security
P
Palo Alto Networks Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Troy Hunt's Blog
V2EX - 技术
V2EX - 技术
W
WeLiveSecurity
L
LINUX DO - 最新话题
人人都是产品经理
人人都是产品经理
S
Security Affairs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
A
Arctic Wolf
Recorded Future
Recorded Future
Microsoft Security Blog
Microsoft Security Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
G
GRAHAM CLULEY
N
Netflix TechBlog - Medium
TaoSecurity Blog
TaoSecurity Blog
C
Check Point Blog
Scott Helme
Scott Helme
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Vercel News
Vercel News
The Hacker News
The Hacker News
Y
Y Combinator Blog
Latest news
Latest news
SecWiki News
SecWiki News
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
Cisco Blogs
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
宝玉的分享
宝玉的分享
L
LINUX DO - 热门话题

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Что именно сломалось: разбираем блокировки РКН/ТСПУ по слоям сетевого стека. Rkn Block Checker
MayersScott · 2026-05-07 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение10 мин

Охват и читатели1.6K

Туториал

Когда сайт не открывается, браузер показывает «Не удалось установить соединение». Это всё, что он знает. Но «не открывается» - это десяток разных историй. 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_BLOCK

  • TCP RST → TCP_RESET

  • TCP timeout → TIMEOUT

  • TCP 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 или, наоборот, ложно-зелёные сайты, которые по факту не работают.