Quando um site não carrega, o navegador exibe a mensagem "Não foi possível estabelecer a conexão". Essa é a única informação que ele fornece. No entanto, "não carrega" pode significar uma dúzia de cenários distintos. O provedor de internet (ISP) pode ter alterado a resposta DNS. O provedor pode estar filtrando conexões TCP por IP. O TSPU pode estar inspecionando o SNI (Server Name Indication) no TLS ClientHello e encerrando a conexão. O site pode carregar, mas retornar um código 200 OK com uma página de bloqueio "acesso restrito". Cada situação exige uma abordagem diferente e, mais importante, indica precisamente onde o filtro está posicionado.
Neste artigo, detalharemos como quatro métodos principais de bloqueio operam e apresentaremos um pequeno utilitário de linha de comando (CLI) em Python que os verifica sequencialmente, informando "sua camada N está quebrada". A ferramenta está disponível no GitHub sob a licença MIT e pode ser instalada via pip install rkn-block-checker. No entanto, o mais interessante não é a ferramenta em si, mas o que acontece nos bastidores.
Por que diagnosticar em vez de contornar?
É importante esclarecer desde o início o que este artigo não aborda. Não se trata de contornar bloqueios, nem de VPNs, fronting ou técnicas de evasão de DPI (Deep Packet Inspection) como zapret ou GoodbyeDPI. O foco é na diagnóstico: entender o que exatamente está quebrado para saber o que consertar. A utilidade de tal diagnóstico pode não ser óbvia até que você se depare com uma situação onde "não funciona" para cinco pessoas diferentes na mesma sala, e as causas sejam distintas para cada uma. Um pode ter o DNS envenenado (resolvido trocando o resolvedor DNS), outro pode ter DPI no SNI (DNS não ajudará, exigindo fronting ou VPN), e um terceiro pode estar recebendo uma página de bloqueio via HTTP do ISP (indicando que os pacotes chegam ao site, mas o problema está em uma camada superior). Sem entender "onde" está o problema, você pode tentar soluções aleatórias sem sucesso.
Um segundo cenário é a verificação da qualidade do canal. Se você está se mudando para um novo apartamento com um novo provedor, é útil em 30 segundos entender quais bloqueios estão ativos: apenas DPI no SNI? Somado a DNS poisoning? Páginas de bloqueio? Isso influencia a escolha da estratégia (DoH será suficiente ou um túnel completo é necessário?). Alternativas existentes como o OONI Probe fazem muito mais, coletando medições em um banco de dados público para análise de longo prazo. No entanto, para a questão "o que está quebrado para mim agora?", isso é excessivo: um cliente pesado, registro obrigatório de medições e uma saída complexa. Buscamos algo que possa ser instalado com um simples pip install e forneça um veredito em meio minuto.
Quatro Camadas, Quatro Métodos de Quebra
Uma requisição HTTPS a um site envolve quatro etapas independentes, cada uma passível de ataque. Vamos analisar de baixo para cima:
Camada 1: DNS
O método mais antigo e barato para bloquear um site é fazer com que o DNS minta. Quando você digita protonvpn.com, seu computador pergunta ao resolvedor DNS (geralmente o fornecido pelo DHCP do seu ISP) o endereço IP. Se o resolvedor mentir, retornando, por exemplo, 0.0.0.0 ou o endereço de uma página de bloqueio, o navegador nunca chegará ao destino. Este método não requer nenhum equipamento de DPI por parte do provedor, apenas ajustes em seu próprio DNS.
É fácil identificar um bloqueio DNS se houver algo com o qual comparar. Pegamos o resultado do resolvedor do sistema (controlado pelo ISP) e o comparamos com o DNS-over-HTTPS (DoH), por exemplo, https://cloudflare-dns.com/dns-query. O ISP não pode interceptar uma requisição DoH, pois ela trafega dentro de uma conexão HTTPS normal com a Cloudflare. Se o DNS do sistema disser "não conheço este host", enquanto o DoH retornar o IP sem problemas, isso é um sinal claro.
No código, isso se parece com:
pythonimport 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
Lógica do veredito:
- Sistema retornou IP, DoH retornou o mesmo IP - DNS limpo.
- Sistema retornou IP, DoH retornou outro IP - há suspeita de manipulação, mas não é garantido (pode ser apenas uma CDN com geolocalização diferente).
- Sistema retornou None, DoH retornou IP - bloqueio DNS, corrigível trocando o DNS para 1.1.1.1 ou 8.8.8.8 (ou usando DoH permanentemente).
- Ambos retornaram None - o site está realmente fora do ar ou não existe.
Este método cobre bloqueios antigos das décadas de 2000 e 2010 e ainda é relevante para algumas regiões. No entanto, em grandes cidades, ele é raramente encontrado, pois os bloqueios ocorrem em níveis superiores.
Camada 2: TCP
Um passo mais avançado: bloquear por IP. O ISP pode enviar um pacote RST para qualquer pacote destinado a um endereço específico, ou simplesmente descartá-lo silenciosamente. Isso é feito no roteador do provedor e não requer análise do conteúdo dos pacotes – ACLs (Access Control Lists) são suficientes.
A verificação é trivial: tentamos estabelecer uma conexão TCP na porta 443 (HTTPS) e observamos o que acontece.
pythonimport 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}"
Três resultados possíveis:
- Tudo OK: O handshake TCP foi concluído com sucesso – prosseguimos para TLS.
- Connection reset na fase de handshake – bloqueio em nível de IP, o provedor está enviando
RST. Isso é raro atualmente, pois umRSTem massa por IP é inconveniente (CDNs, hospedagens compartilhadas). Geralmente é aplicado a alvos específicos. - Timeout – os pacotes são descartados silenciosamente. Novamente, isso é mais comum para endereços IP individuais do que para sites inteiros.
Na prática, em 2026, um RST TCP puro por IP é incomum – os provedores preferem operar em camadas superiores. No entanto, para servidores específicos (como nós de saída Tor), isso ainda é relevante.
Camada 3: TLS
Aqui as coisas ficam mais interessantes. Equipamentos TSPU modernos não bloqueiam TCP. Eles permitem a passagem de SYN, SYN-ACK, ACK – a conexão é estabelecida. Somente quando o cliente envia o primeiro pacote TLS (ClientHello), o middlebox o analisa, lê o campo SNI e toma uma decisão.
Server Name Indication (SNI) é uma extensão TLS na qual o cliente informa ao servidor, em texto claro, a qual host ele está se conectando. Isso é necessário para que um único IP possa servir centenas de sites: o servidor precisa saber qual certificado apresentar. O ClientHello é enviado antes que a conexão seja criptografada, portanto, o SNI pode ser lido por qualquer um no caminho.
Em seguida, o middlebox faz uma de duas coisas: envia um RST para ambos os lados, ou simplesmente para de encaminhar os pacotes. Do ponto de vista do cliente, isso se manifesta da seguinte forma: a conexão TCP foi estabelecida sem problemas (o ping-pong foi bem-sucedido), o ClientHello foi enviado, e então – ou um ConnectionResetError ou um socket.timeout ocorre.
Esta é a assinatura de DPI no SNI:
TCP_OK + TLS_FAILED → provavelmente TSPU
Nenhuma outra combinação se apresenta dessa forma. Se o bloqueio fosse em nível de DNS, não chegaríamos ao TCP. Se fosse por IP, o TCP não seria estabelecido. Mas "conexão existe, mas assim que você diz para quem – tudo se rompe" – isso se refere especificamente à inspeção de SNI.
Código de verificação:
pythonimport 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"
Um ponto importante: server_hostname=host – isso é a transmissão do SNI. Sem ele (ou com um SNI falsificado), o middlebox não verá o nome proibido e permitirá a passagem. É nisso que se baseiam algumas técnicas de evasão: domain fronting, ECH (Encrypted Client Hello), fragmentação do ClientHello. Mas isso é assunto para outro artigo.
No TLS 1.3, houve uma chance de eliminar o SNI como atributo – foi criado o ECH, que criptografa o ClientHello inteiro. No entanto, sua implementação ainda é majoritariamente experimental, e os middleboxes aprenderam a reagir à própria presença do ECH (por exemplo, encerrando a conexão se virem a extensão ECH). Por enquanto, o SNI continua sendo o principal ponto de inspeção.
Camada 4: HTTP
Às vezes, o bloqueio permite a passagem de tudo – DNS, TCP, TLS – mas retorna algo incorreto. Isso ocorre em dois cenários.
HTTP 451: O código "Unavailable For Legal Reasons" foi adicionado pela RFC 7725 especificamente para esses casos. Em teoria, é uma forma honesta de dizer "acesso fechado por decisão judicial". Na prática, é raro, mas se ocorrer – é um marcador explícito.
Página de bloqueio do ISP: O ISP intercepta o HTTPS, apresenta seu próprio certificado (o que causaria um erro TLS, mas – não, geralmente fazem isso apenas para requisições não-HTTPS ou falsificam o DNS para que você acesse o servidor deles) e entrega uma página com um texto como "Acesso restrito por decisão do Roskomnadzor" com status 200 OK. O navegador exibe isso como uma página normal – nenhum erro ocorre, apenas o conteúdo está incorreto.
A verificação continua simples: fazer um GET na URL desejada e verificar o corpo da resposta. Se marcadores de página de bloqueio forem encontrados – então é uma página de bloqueio.
pythonSTUB_MARKERS = ( "acesso restrito", "decisão do roskomnadzor", "decisão judicial", "bloqueado", "blocked by", "rkn.gov.ru", "registro unificado", ) def looks_like_stub(body: str) -> bool: body_lower = body.lower() return any(marker in body_lower for marker in STUB_MARKERS)
A precisão não é de 100%. Teoricamente, pode-se imaginar um site que contenha acidentalmente a frase "acesso restrito" em um contexto normal. Na prática, nunca vi um falso positivo, mas em produção, eu não confiaria em tal heurística para decisões críticas.
Como o veredito é construído?
A lógica de "verificação" por camadas é direta: vamos de baixo para cima e paramos no primeiro ponto de falha.
DNS resolve (sistema) ↓ ok DNS resolve (DoH) ↓ coincide TCP connect :443 ↓ ok TLS handshake (com SNI) ↓ ok HTTP GET ↓ status 200 + não é página de bloqueio = OK
A cada etapa, se algo falhar, emitimos nosso veredito:
- DNS do sistema falhou, DoH OK → DNS_BLOCK
- TCP RST → TCP_RESET
- TCP timeout → TIMEOUT
- TCP OK, TLS RST/timeout → TLS_BLOCK (assinatura de DPI)
- HTTP 451 ou marcadores no corpo → HTTP_STUB
- Tudo OK → OK
Para transformar "um site está quebrado" em um veredito "você está em uma rede bloqueada", é preciso testar um conjunto de sites. Usei duas listas:
- Whitelist (controle) – sites que definitivamente deveriam funcionar:
gosuslugi,yandex,sberbank,vk,ozon,mos.ru, etc. Se eles não carregarem, não é um bloqueio, é a internet quebrada. - Blacklist – sites bloqueados na Rússia:
Instagram,X(Twitter),LinkedIn,Discord,Tor Project,ProtonVPN,Patreon,rutracker, etc.
Se a whitelist carregar 100% e a blacklist não carregar em mais de 70%, emitimos "você está em uma zona bloqueada, e aqui está a divisão por tipos de bloqueio".
Testes paralelos e streaming de saída
A primeira versão do CLI realizava os testes sequencialmente, o que era desagradável: 36 sites × tempo médio por teste – um minuto ou mais. A solução óbvia é o paralelismo via ThreadPoolExecutor:
pythonfrom 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() ))
Isso tornou o processo 10 vezes mais rápido – mas surgiu outro problema. pool.map() retorna os resultados apenas quando todas as tarefas são concluídas. Ou seja, o usuário inicia o CLI, vê o cabeçalho "RKN Block Checker", e então 10 segundos de silêncio – e depois imediatamente toda a parede de resultados. A experiência do usuário (UX) é ruim.
Isso foi corrigido com uma simples troca de pool.map() para as_completed() – uma função geradora que produz resultados assim que chegam:
pythonfrom 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()
No CLI, o loop ficou assim:
pythonprint_section("Whitelist (should always work)") for r in iter_check_urls(WHITE_URLS, workers, timeout): print_result(r) sys.stdout.flush() # importante - senão o python bufferiza o stdout
Um detalhe sutil: as_completed retorna os resultados na ordem em que são concluídos, não na ordem de entrada. Para saída interativa, isso é aceitável (sites rápidos aparecem no topo, os lentos embaixo). No entanto, para o modo --json, onde se espera uma ordem estável para reprodutibilidade, mantive um wrapper separado:
pythondef 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]
A segunda artimanha é que ambos os grupos (whitelist e blacklist) são executados no mesmo pool de threads imediatamente. Enquanto as linhas da whitelist estão sendo exibidas, a blacklist já está rodando em paralelo em segundo plano. Quando a whitelist termina de ser impressa, a blacklist já está pronta ou quase pronta – a seção blacklist é preenchida quase instantaneamente. O tempo total não aumentou, mas a percepção é significativamente mais ágil.
O que foi obtido como resultado
Uma execução típica se parece com isto:
======================================================================
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
======================================================================
Informações importantes de forma compacta: quais sites, o que exatamente está errado com eles, em que camada o problema ocorreu. Para scripting, há o modo --json – ele fornece as mesmas informações, além de um rastreamento completo da sonda para cada site (quais IPs os resolvedores retornaram, qual certificado foi recebido, timings). É conveniente para alimentar no jq:
bash# Nomes de todos os sites bloqueados rkn-check --json | jq -r '.blacklist[] | select(.verdict != "OK") | .name' # Apenas bloqueios DPI (TCP vivo, TLS morto) rkn-check --json | jq '.blacklist[] | select(.verdict == "TLS_BLOCK" and .tcp_ok)'
O que não foi feito e por quê
Para antecipar perguntas nos comentários, vou esclarecer explicitamente.
- IPv6: Não implementado. Na prática, o tráfego IPv6 na Rússia ainda é tratado com menos rigor pelo TSPU – alguns provedores permitem via v6 o que é bloqueado no v4. Este é um tópico interessante e separado, mas requer diagnóstico e semântica de vereditos próprios ("v4 bloqueado, v6 aberto" – isso já não é uma resposta binária). Possivelmente na próxima versão.
- QUIC e HTTP/3: Sites modernos estão migrando cada vez mais para QUIC (UDP, porta 443). O TSPU opera com QUIC sob suas próprias regras – tanto quanto sei, atualmente mais através do bloqueio completo de UDP/443 em momentos de endurecimento, do que via DPI no conteúdo. O suporte a QUIC exigiria sua própria pilha de sondagem.
- Bloqueios pontuais dentro de um mesmo site: Muitos bloqueios agora operam não no nível "domínio inteiro", mas "URL específico" ou "sub-redes CDN específicas". Por exemplo, o YouTube não está totalmente bloqueado – apenas um prefixo CDN específico é cortado. Esta ferramenta não detectará isso – se a página principal carregar, então está OK.
- TLS 1.3 ECH: Quando (se) o ECH se tornar massivo, a lógica atual de
TLS_BLOCK = DPI on SNIdeixará de ser precisa – o SNI será criptografado. Atualmente, isso não é um problema, pois o ECH não está ativado por padrão em muitos lugares. - Monitoramento longitudinal: Uma única execução é um snapshot. Para rastrear "quando exatamente o bloqueio apareceu/desapareceu", é preciso rodar
rkn-check --jsonvia cron e coletar em séries temporais. Talvez faça sentido adicionar umdocker-composepronto com Grafana, mas isso já é outro projeto.
Onde obter e o que ler
- GitHub: github.com/MayersScott/rkn-block-checker
- PyPI:
pip install rkn-block-checker, depoisrkn-check.
Sobre o tema de bloqueios e DPI, recomendo:
- GFW Report: A melhor fonte em russo e inglês sobre o funcionamento de bloqueios DPI (com exemplos da China, mas muitos princípios são aplicáveis).
- OONI: Ferramenta de nível acadêmico para medir censura com um banco de dados público.
- bol-van/zapret e sua Wiki: Fonte prática sobre como exatamente o TSPU inspeciona o SNI e quais técnicas de evasão funcionam.
Se você tiver histórias sobre bloqueios não óbvios em sua região, ficarei feliz em ouvi-las nos comentários. Casos especialmente interessantes são aqueles em que o veredito da ferramenta não corresponde à realidade: falsos positivos ou, inversamente, sites falsamente verdes que na verdade não funcionam.





