#!/usr/bin/env bash
# CVE-2026-98432 (Linux IPv4 TCP ireq_opt use-after-free): interim mitigation for Ubuntu.
# Designed for Fleet's run-script and policy automation. Idempotent. Needs root.
#
# No argument: choose automatically.
#   running kernel carries the fix  -> remove the mitigation
#   otherwise                       -> apply the mitigation
# Arguments: apply | revert | status   (status only reports; changes nothing)
#
# Mitigation = two controls, kept across reboots by one systemd unit:
#   1. nftables: drop inbound IPv4 TCP whose IP header carries options (IHL > 5).
#      The race needs a non-NULL ireq_opt, which exists only when the SYN has IP options.
#   2. sysctl net.mptcp.enabled=0: the published root exploit targets an MPTCP listener.
#
# Scope: the host network namespace. Docker bridge/NAT traffic to published ports passes the host
# prerouting hook first, so it is covered. Containers with their own NIC (macvlan, ipvlan,
# passthrough) are not, and neither is container-to-container traffic on one bridge (no bridge-family
# table here). net.mptcp.enabled is per network namespace and is set for the host namespace only.
# A `nftables.service` restart is followed by this unit (PartOf=). A reload that flushes the whole
# ruleset is not: run `systemctl restart cve-2026-98432-mitigation` afterwards, or re-run this script.
set -euo pipefail

TABLE=cve_2026_98432
NFT_FILE=/etc/cve-2026-98432.nft
SYSCTL_FILE=/etc/sysctl.d/99-cve-2026-98432.conf
UNIT_NAME=cve-2026-98432-mitigation.service
UNIT_FILE=/etc/systemd/system/$UNIT_NAME

# First kernel package version that carries the fix, per Ubuntu release. Generic kernels only.
# Provisional: these are the work-in-progress targets on https://ubuntu.com/security/CVE-2026-98432
# (24.04 and 26.04, as of 29 Oct 2026). Confirm them once Ubuntu releases the fix. Any other release
# or flavor (22.04, 26.10, HWE, cloud, low-latency) is treated as unpatched, so the mitigation stays
# in place and is never auto-removed; add a release here to change that.
OS_RELEASE="${OS_RELEASE:-/etc/os-release}"
[[ -r "$OS_RELEASE" ]] || { echo "cannot read $OS_RELEASE" >&2; exit 1; }
# shellcheck disable=SC1090
. "$OS_RELEASE"

fixed_for() {
  case "${VERSION_ID:-}" in
    24.04) echo "6.8.0-149.149" ;;
    26.04) echo "7.0.0-43.43" ;;
  esac
}

kernel_patched() {
  local pkg fixed
  [[ "$(uname -r)" == *-generic ]] || return 1
  fixed=$(fixed_for)
  [[ -n "$fixed" ]] || return 1
  # The running kernel must be in the same series as the fixed version: an HWE kernel such as
  # 6.17.0-5-generic would otherwise compare as newer than 6.8.0-149.149 without being checked.
  [[ "$(uname -r)" == "${fixed%%-*}"-*-generic ]] || return 1
  pkg=$(dpkg-query -W -f='${Version}' "linux-image-$(uname -r)" 2>/dev/null || true)
  [[ -n "$pkg" ]] || return 1
  dpkg --compare-versions "$pkg" ge "$fixed"
}

mitigation_present() { [[ -f "$NFT_FILE" || -f "$UNIT_FILE" || -f "$SYSCTL_FILE" ]]; }

apply() {
  command -v nft >/dev/null || { echo "nft not found: apt-get install -y nftables, then re-run" >&2; exit 1; }

  # add + delete + recreate keeps `nft -f` idempotent (no duplicate rules on re-run).
  cat > "$NFT_FILE" <<NFT
add table inet $TABLE
delete table inet $TABLE
table inet $TABLE {
  chain prerouting {
    type filter hook prerouting priority -310; policy accept;
    ip protocol tcp ip hdrlength gt 5 counter drop comment "CVE-2026-98432: drop IPv4 TCP with IP options"
  }
}
NFT

  cat > "$SYSCTL_FILE" <<SYSCTL
# CVE-2026-98432 interim mitigation. Listeners already open keep MPTCP until their service restarts.
net.mptcp.enabled = 0
SYSCTL

  cat > "$UNIT_FILE" <<UNIT
[Unit]
Description=CVE-2026-98432 interim mitigation (drop IPv4 TCP with IP options, MPTCP off)
After=nftables.service
PartOf=nftables.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/nft -f $NFT_FILE
ExecStart=/usr/sbin/sysctl -q -p $SYSCTL_FILE
ExecStop=/usr/sbin/nft delete table inet $TABLE

[Install]
WantedBy=multi-user.target
UNIT

  systemctl daemon-reload
  # restart, not `enable --now`: an already-active oneshot unit would skip ExecStart and leave a
  # flushed ruleset empty.
  systemctl enable "$UNIT_NAME"
  systemctl restart "$UNIT_NAME"

  # `nft list` normalises `gt` to `>`, so accept both. Fail the Fleet run if either control is missing.
  nft list table inet "$TABLE" | grep -qE 'hdrlength (>|gt) 5' || { echo "nft rule missing" >&2; exit 1; }
  [[ "$(sysctl -n net.mptcp.enabled)" == "0" ]] || { echo "net.mptcp.enabled is still 1" >&2; exit 1; }

  echo "mitigation applied on $(hostname), kernel $(uname -r)"
  nft list table inet "$TABLE" | grep hdrlength
  echo "net.mptcp.enabled=$(sysctl -n net.mptcp.enabled)"
}

revert() {
  systemctl disable --now "$UNIT_NAME" 2>/dev/null || true
  rm -f "$NFT_FILE" "$SYSCTL_FILE" "$UNIT_FILE"
  systemctl daemon-reload
  nft delete table inet "$TABLE" 2>/dev/null || true
  sysctl -q -w net.mptcp.enabled=1
  echo "mitigation removed on $(hostname), kernel $(uname -r); net.mptcp.enabled=1"
}

mode="${1:-auto}"

if [[ "$mode" == "status" ]]; then
  patched=no; kernel_patched && patched=yes
  present=no; mitigation_present && present=yes
  echo "kernel=$(uname -r) kernel_has_fix=$patched mitigation_present=$present"
  exit 0
fi

[[ $EUID -eq 0 ]] || { echo "must run as root" >&2; exit 1; }
[[ "${ID:-}" == "ubuntu" ]] || { echo "not Ubuntu (${ID:-unknown}): use your vendor's kernel update"; exit 0; }

case "$mode" in
  apply)  apply ;;
  revert) revert ;;
  auto)
    if kernel_patched; then
      if mitigation_present; then revert; else echo "kernel $(uname -r) has the fix; nothing to remove"; fi
    else
      apply
    fi
    ;;
  *) echo "usage: $0 [apply|revert|status]" >&2; exit 2 ;;
esac
