
Verifica se um host descarta SYN TCP IPv4 com IP options (mitigação do CVE-2026-98432)
Checks whether a host drops IPv4 TCP SYN with IP options, the recommended network mitigation for CVE-2026-98432 (use-after-free in the Linux kernel's IPv4 TCP handshake, CVSS 9.8, until the patched kernel is released).
For each port, it sends a normal SYN and a SYN with IPv4 Record Route and compares the responses. It is harmless: it exploits nothing, it only opens (and does not complete) a handshake.
What the test measures: whether the SYN with IP options arrives and is accepted by the target. It does not read the kernel version. An already-patched kernel still accepts IP options and outputs
EXPOSED— in that caseEXPOSEDonly means "no IP options filter", and the fix is confirmed withuname -r.
./verifica-cve-2026-98432.sh <host-ou-ip> [porta ...] # porta padrão: 443
./verifica-cve-2026-98432.sh exemplo.com 443 80 22
Without the script, directly through the image (the target must be IPv4):
docker run --rm --network host --cap-add NET_RAW \
ghcr.io/marcelofmatos/cve-2026-98432-check:latest 203.0.113.10 443 22
| Output | Exit | Meaning |
|---|---|---|
MITIGADO | 0 | Normal SYN accepted, SYN with IP options discarded — and no hop along the path filters IP options |
EXPOSTO | 1 | The SYN with IP options was also accepted: nothing along the path or at the target filters it |
INCONCLUSIVO | 2 | Port closed (RST) or filtered — use an open port |
SEM HOST | 2 | Target on the local network and nobody answers ARP for it: free IP or machine powered off. No TCP probe is sent |
NÃO CONFIÁVEL | 3 | Some hop along the path (VPN, carrier) already discards IP options. The "discarded" result proves nothing about the target: test from another point on the network |
| — | 64 | Incorrect usage, or name without IPv4 |
With multiple ports, the exit is the worst case: any EXPOSTO → 1.
Details:
getent), so VPN/internal DNS names work.prerouting as if it came from outside.CHECK_IMAGE (default ghcr.io/marcelofmatos/cve-2026-98432-check:latest). The 1st line
of the log is the version: cve-2026-98432-check <version> starting.Many VPNs and carriers discard packets with IP options. Testing from behind one of them,
every target appears mitigated. That is why the checker walks the path hop by hop (increasing TTL)
before testing and, if any hop discards IP options, returns NÃO CONFIÁVEL.
Limitation: a filter at a hop that does not respond to ICMP goes unnoticed. Validate the test point
on a target known to be unmitigated: it must come out EXPOSTO.
A vulnerable Linux returns the IP options in the SYN-ACK (the SYN with Record Route comes back with
ihl=6 and the RR option). At a test point with the rule from mitigacao/, that SYN-ACK
is discarded in prerouting. Even so the result is correct: scapy captures the response on the packet
socket, before netfilter. Measured: testing, from a host with the rule, a target without the
rule, the result was EXPOSTO and the rule counter went up by 1 (the SYN-ACK discarded after being
captured). The test target without the rule (container) -> EXPOSTO covers this case in CI, which runs the
tests with the runner without and with the rule.
Exception: targets behind a bridge of the test point itself with br_netfilter. With the module
loaded (net.bridge.bridge-nf-call-iptables = 1, default on Docker Swarm hosts and many
Proxmox), IPv4 prerouting runs still inside the bridge, before the capture. In that case, the rule
of the test point discards the SYN-ACK of a container or VM on that bridge, and the result is a
false MITIGADO. Targets reached through a regular network card (another machine on the network) are not affected.
To test containers or VMs on a host with br_netfilter, use a test point outside it, or one
without the rule. The tests skip this case (SKIP) when they detect this combination.
If the target port is published by Docker (DNAT to a container), the target itself responds with ICMP
"TTL exceeded" to probes with a low TTL, because it forwards the packet. The checker recognizes this ICMP
by the source IP (it is the target's IP) and does not treat it as a hop along the path. Up to version 1.1.0, it
gave a false NÃO CONFIÁVEL on a mitigated target when the 1st port in the list was forwarded. The
test forwarded port on the mitigated target -> MITIGADO covers this case.
In mitigacao/:
cve-2026-98432.nft: table ip cve_2026_98432 that discards IPv4 TCP with ip hdrlength > 5
in prerouting (priority -300). Idempotent.cve-2026-98432-mitigation.service: systemd unit that loads the rule before the network comes up.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
The tests spin up a temporary listener on the host (port 18098) and test it from a container
on the Docker bridge. In CI (test.yml) the runner runs twice:
without the rule (must output EXPOSTO) and with the rule applied (must output MITIGADO).
This validates the method in both directions.