
Verifica se um host descarta SYN TCP IPv4 com IP options (mitigação do CVE-2026-98432)
Verifica se um host descarta SYN TCP IPv4 com IP options, a mitigação de rede recomendada para o CVE-2026-98432 (use-after-free no handshake TCP IPv4 do kernel Linux, CVSS 9.8, até sair o kernel corrigido).
Para cada porta, envia um SYN normal e um SYN com IPv4 Record Route e compara as respostas. É inofensivo: não explora nada, só abre (e não completa) um handshake.
O que o teste mede: se o SYN com IP options chega e é aceito pelo alvo. Ele não lê a versão do kernel. Um kernel já corrigido continua aceitando IP options e sai
EXPOSTO— nesse caso oEXPOSTOsignifica só "sem filtro de IP options", e a correção se confere comuname -r.
./verifica-cve-2026-98432.sh <host-ou-ip> [porta ...] # porta padrão: 443
./verifica-cve-2026-98432.sh exemplo.com 443 80 22
Sem o script, direto pela imagem (o alvo precisa ser IPv4):
docker run --rm --network host --cap-add NET_RAW \
ghcr.io/marcelofmatos/cve-2026-98432-check:latest 203.0.113.10 443 22
| Saída | Exit | Significado |
|---|---|---|
MITIGADO | 0 | SYN normal aceito, SYN com IP options descartado — e nenhum salto do caminho filtra IP options |
EXPOSTO | 1 | O SYN com IP options também foi aceito: nada no caminho nem no alvo filtra |
INCONCLUSIVO | 2 | Porta fechada (RST) ou filtrada — use uma porta aberta |
SEM HOST | 2 | Alvo na rede local e ninguém responde ARP por ele: IP livre ou máquina desligada. Nenhuma sonda TCP é enviada |
NÃO CONFIÁVEL | 3 | Algum salto do caminho (VPN, operadora) já descarta IP options. O "descartado" não prova nada sobre o alvo: teste de outro ponto da rede |
| — | 64 | Uso incorreto, ou nome sem IPv4 |
Com várias portas, o exit é o pior caso: qualquer EXPOSTO → 1.
Detalhes:
getent), então nomes de VPN/DNS interno funcionam.prerouting como se viesse de fora.CHECK_IMAGE (padrão ghcr.io/marcelofmatos/cve-2026-98432-check:latest). A 1ª linha
do log é a versão: cve-2026-98432-check <versão> iniciando.Muitas VPNs e operadoras descartam pacotes com IP options. Testando de trás de uma delas,
todo alvo parece mitigado. Por isso o verificador percorre o caminho salto a salto (TTL
crescente) antes de testar e, se algum salto descarta IP options, devolve NÃO CONFIÁVEL.
Limite: um filtro num salto que não responde ICMP passa despercebido. Valide o ponto de teste
num alvo sabidamente sem mitigação: ele precisa sair EXPOSTO.
Um Linux vulnerável devolve as IP options no SYN-ACK (o SYN com Record Route volta com
ihl=6 e a opção RR). Num ponto de teste com a regra de mitigacao/, esse SYN-ACK
é descartado no prerouting. Mesmo assim o resultado é correto: o scapy captura a resposta no
socket de pacote, antes do netfilter. Medido: testando, de um host com a regra, um alvo sem a
regra, o resultado foi EXPOSTO e o contador da regra subiu 1 (o SYN-ACK descartado depois de
capturado). O teste alvo sem a regra (container) -> EXPOSTO cobre esse caso no CI, que roda os
testes com o runner sem e com a regra.
Exceção: alvos atrás de uma bridge do próprio ponto de teste com br_netfilter. Com o módulo
carregado (net.bridge.bridge-nf-call-iptables = 1, padrão de host Docker Swarm e de muitos
Proxmox), o prerouting IPv4 roda ainda dentro da bridge, antes da captura. Nesse caso, a regra
do ponto de teste descarta o SYN-ACK de um container ou VM dessa bridge, e o resultado é um
MITIGADO falso. Alvos alcançados por uma placa comum (outra máquina da rede) não são afetados.
Para testar containers ou VMs de um host com br_netfilter, use um ponto de teste fora dele, ou um
sem a regra. Os testes pulam esse caso (SKIP) quando detectam essa combinação.
Se a porta do alvo é publicada pelo Docker (DNAT até um container), o próprio alvo responde ICMP
"TTL excedido" às sondas com TTL baixo, porque encaminha o pacote. O verificador reconhece esse ICMP
pelo IP de origem (é o IP do alvo) e não o trata como um salto do caminho. Até a versão 1.1.0, ele
dava um NÃO CONFIÁVEL falso num alvo mitigado quando a 1ª porta da lista era encaminhada. O
teste porta encaminhada no alvo mitigado -> MITIGADO cobre esse caso.
Em mitigacao/:
cve-2026-98432.nft: tabela ip cve_2026_98432 que descarta TCP IPv4 com ip hdrlength > 5
no prerouting (priority -300). Idempotente.cve-2026-98432-mitigation.service: unit systemd que carrega a regra antes da rede subir.sudo install -m 644 mitigacao/cve-2026-98432.nft /etc/cve-2026-98432.nft
sudo install -m 644 mitigacao/cve-2026-98432-mitigation.service /etc/systemd/system/
echo 'net.mptcp.enabled = 0' | sudo tee /etc/sysctl.d/90-cve-2026-98432-mptcp.conf
sudo sysctl -w net.mptcp.enabled=0
sudo systemctl daemon-reload && sudo systemctl enable --now cve-2026-98432-mitigation
A mitigação não substitui o kernel corrigido: instale-o e reinicie assim que a sua distro publicar.
flowchart LR
U["Operador"] --> L["verifica-cve-2026-98432.sh<br/>resolve o nome com getent<br/>escolhe a rede do container"]
L -->|"docker run --cap-add NET_RAW"| C["Container cve-2026-98432-check<br/>Alpine + Python + scapy"]
C -->|"rede host: alvo remoto"| N["Caminho de rede<br/>VPN, operadora, internet"]
C -->|"bridge: alvo é a própria máquina"| H["prerouting do host"]
N --> A["Alvo IPv4<br/>porta TCP"]
REG["GHCR<br/>ghcr.io/marcelofmatos/cve-2026-98432-check"] -.->|"docker pull"| C
flowchart TD
S["Início: alvo e portas"] --> AQ{"Alvo na rede local<br/>e sem resposta ARP?"}
AQ -->|"sim"| SH["SEM HOST — exit 2"]
AQ -->|"não"| H["TTL = 1, 2, 3 ... até o alvo<br/>SYN normal e SYN com RR"]
H --> HQ{"Algum salto responde ICMP ao normal<br/>e some com o RR?"}
HQ -->|"sim"| FL["caminho FILTRA no salto N"]
HQ -->|"não, chegou ao alvo, ICMP veio do próprio alvo<br/>ou 4 saltos mudos seguidos"| OK["nenhum filtro visto"]
FL --> P
OK --> P
P["Por porta: SYN normal"] --> PQ{"SYN-ACK?"}
PQ -->|"não"| INC["INCONCLUSIVO — exit 2"]
PQ -->|"sim"| R["SYN com RR"]
R --> RQ{"Alguma resposta?"}
RQ -->|"sim"| EXP["EXPOSTO — exit 1"]
RQ -->|"não, caminho filtra"| NC["NÃO CONFIÁVEL — exit 3"]
RQ -->|"não, caminho limpo"| MIT["MITIGADO — exit 0"]
docker build --build-arg APP_VERSION=dev -t cve-2026-98432-check:dev .
# ESPERADO_LOCAL=1 num host sem a regra nft; 0 num host com a regra aplicada
CHECK_IMAGE=cve-2026-98432-check:dev ESPERADO_LOCAL=1 VERSAO_ESPERADA=dev ./tests/testes.sh
Os testes sobem um listener temporário no host (porta 18098) e o testam a partir de um container
na bridge do Docker. No CI (test.yml) o runner roda duas vezes:
sem a regra (precisa sair EXPOSTO) e com a regra aplicada (precisa sair MITIGADO).
Isso valida o método nos dois sentidos.
Workflow manual release-and-build.yml (Actions →
Release and build → patch/minor/major): roda os testes, cria a tag SemVer e o release, e publica
a imagem no GHCR com as tags x.y.z, x.y, x e latest. A versão entra na imagem por
APP_VERSION.
MIT