
Shell scripts that check Linux hosts for CVE-2026-98432 and apply a temporary iptables u32 rule dropping IPv4 TCP packets with IP options, with systemd unit, self-test and revert.
This repository holds three scripts that check a Linux host for CVE-2026-98432 and, where no fixed kernel is available, drop IPv4 TCP packets carrying IP options with a single iptables rule. It is the iptables counterpart of typhoon1217/cve-2026-98432-mitigation, which does the same with nftables. It is a temporary mitigation, not a fix. It was not tested against a real exploit (there is no public PoC), and no vendor endorses it. The author is an individual administrator, not a vendor.
This is for hosts that cannot get a fixed kernel and cannot run the nftables variant:
5.15.0-203, for 24.04 for 6.8.0-149. Until that kernel is booted, the mitigation closes the gap.iptables but without nft, or with an nft too old for ip hdrlength.It is not for Proxmox VE hosts, which have their own fixed kernel (7.0.14-25-pve and later) and bridged traffic that needs the bridge rule from the nftables repo. It is not for hosts with SELinux or Smack NetLabel CIPSO labeling, which can reach the vulnerable code without IP options. It is not for kernels older than 4.4, which do not contain the bug.
If your distribution ships a fixed kernel, install that, reboot, and run cve-98432-revert-iptables.sh if you applied this.
CVE-2026-98432 is a race in IPv4 TCP connection setup. The kernel keeps the IP options of an incoming SYN in ireq_opt, and under the right timing one CPU frees that object while another still reads it. In mainline source, ireq_opt is only set when the SYN carried IPv4 header options, meaning the IP header is longer than 20 bytes (IHL > 5), or through CIPSO labeling. With no options there is nothing to free or read. So the rule drops every IPv4 TCP packet whose header is longer than 20 bytes:
iptables -t raw -I PREROUTING -p tcp -m u32 --u32 "0>>24&0xF=6:15" -j DROP
The u32 match reads the first four bytes of the IP header, shifts the first byte down and keeps its low nibble, which is the IHL field in 32-bit words. 6:15 matches every value above 5. The raw table's PREROUTING hook runs before the kernel parses the options, and before conntrack. Loopback traffic passes the same hook, which is what the self-test relies on.
In a network namespace with a loopback listener, a plain SYN connected and a SYN with a Record Route option timed out, with the rule counter at 2 packets (SYN and one retransmit). The nftables rule ip hdrlength gt 5 behaved identically. Both iptables-legacy and iptables-nft were tested.
Legitimate TCP traffic with IP options is rare. On nine production hosts (mail, DNS, web, XMPP) the counter stayed at the self-test packets over the observation window.
cve-98432-check.sh. It is read-only. Look at status, kernel_upstream_base and kernel_changelog_cve. Vendor kernels carry the fix under their own version numbers, so when the changelog of the running kernel names the CVE, that wins over the version comparison.iptables_u32_probe. It must say ok. The probe runs iptables -C, which only asks whether the rule exists and therefore adds nothing; the answer tells whether the u32 match and the raw table are usable on this kernel.selftest_ipopt. CONNECTED means a SYN with IP options currently gets through. FAIL means it is already dropped.bash, iptables with xt_u32 and iptable_raw, systemd. Python 2 or 3 for the self-test. Hosts without systemd are not covered.Tested on Debian 9 (iptables 1.6.0, legacy), Debian 10 and 11 (iptables 1.8.x, nf_tables backend) and Ubuntu 22.04 (iptables 1.8.7). The boot path was tested on Debian 10.
The scripts are sent to the host over SSH on standard input, so nothing has to be copied first.
ssh root@HOST 'bash -s' < cve-98432-check.sh
ssh root@HOST 'bash -s' < cve-98432-apply-iptables.sh
It is idempotent. It installs /usr/local/sbin/cve-2026-98432-mitigation (start|stop|status), a systemd oneshot unit cve-2026-98432-mitigation.service that loads the rule before network-pre.target, and, only when the kernel knows MPTCP, /etc/sysctl.d/99-cve-2026-98432.conf with net.mptcp.enabled = 0. It exits before changing anything if it is not root, if iptables or systemd is missing, or if the u32 probe fails.host=... svc=active enabled=enabled mptcp=n/a
-A PREROUTING -p tcp -m u32 --u32 "0x0>>0x18&0xf=0x6:0xf" -j DROP
selftest_plain=CONNECTED
selftest_ipopt=FAIL TimeoutError
mptcp=0 on kernels 5.6 and later. Older Python prints FAIL timeout, which is the same result.ssh root@HOST 'systemctl is-active cve-2026-98432-mitigation; /usr/local/sbin/cve-2026-98432-mitigation status'
while read -r h; do echo "##### $h"; ssh root@$h 'bash -s' < cve-98432-apply-iptables.sh; done < hosts.txt
Run the check again on every mitigated host and look at two fields:
| Field | Expected |
|---|---|
cve_service | enabled/active |
selftest_ipopt | FAIL ... (the IP-options SYN was dropped) |
The self-test uses loopback. Traffic from the network passes the same PREROUTING hook, so the result carries over. If you want to confirm from outside, check-blocked.py and verify-synack.sh from the nftables repo work unchanged against a host mitigated this way.
To watch the counters:
while read -r h; do
printf '%-28s ' "$h"
ssh -n root@$h '/usr/local/sbin/cve-2026-98432-mitigation status' 2>&1 | awk '{print "pkts="$1, "bytes="$2}'
done < hosts.txt
Each self-test adds 2 packets. A counter that grows beyond that means real traffic with IP options reaches the host; look at that before deciding anything. The -n matters: without it, ssh reads the rest of the host list from standard input and the loop stops after the first host.
The symptom is a TCP connection that hangs (times out) rather than being refused, together with a rising counter.
| Command | Effect |
|---|---|
systemctl stop cve-2026-98432-mitigation | off immediately, comes back on at reboot |
systemctl disable --now cve-2026-98432-mitigation | off now and across reboots |
Compatibility measured on the tested hosts: the rule sits in the raw table, which fail2ban does not touch. ufw was checked in source: its flush_builtins in ufw-init-functions flushes filter, mangle and nat only, so ufw reload and ufw disable leave the rule in place. A firewall script of your own that runs iptables -t raw -F would remove it; re-run the apply script or systemctl restart cve-2026-98432-mitigation afterwards.