#!/bin/sh
#
# sonomos-redirect — arm/disarm the Sonomos transparent-capture nftables
# redirect, add/remove this device's own MITM trust anchor, AND reset the
# invoking user's connections that arming cannot capture.
#
# HARDENED, ROOT-OWNED helper. This is the ONLY thing granted passwordless sudo
# (see /etc/sudoers.d/sonomos-redirect), so it is deliberately tiny and does
# ONLY those three things — NOT group creation, NOT arbitrary certificate
# installation, NOT resetting arbitrary sockets, nothing else. It is installed to
# /usr/local/libexec/sonomos-redirect as root:root mode 0755, so a non-root user
# can neither edit it nor re-point the sudo rule at other code. That closes the
# local privilege-escalation hole of granting NOPASSWD sudo to a script living
# in the user's writable tree.
#
# WHY CA TRUST IS HERE NOW, AND WHY THAT IS NOT A WIDENING
# -------------------------------------------------------
# An earlier revision of this file said, emphatically, that it does "NOT CA
# trust". That was the right call for the shape trust had then: the only way to
# express it would have been "install the certificate at the path the caller
# names", and a NOPASSWD grant to THAT is a root-certificate-injection primitive
# — strictly worse than the problem it solves.
#
# The reason it can live here now is that the shape changed. A trusted MITM root
# that outlives the session which created it is a standing silent-interception
# capability for as long as its private key exists; removing it on every session
# exit is the whole point, and removal has to be password-free or people turn
# the feature off. So `ca-trust` and `ca-untrust` were built to take NO input at
# all:
#
#   * `ca-trust` installs EXACTLY $PINNED_CA — a ROOT-OWNED copy of this
#     device's CA, written once by `linux.sh install-redirect-helper`, which is
#     itself a password-gated step. The caller cannot name a path, cannot name a
#     nickname, and cannot change the bytes: $PINNED_CA lives outside every
#     user-writable directory, and on EVERY call it is re-verified to be a
#     regular file (not a symlink, which `stat` would follow to some other
#     root-owned certificate), root-owned, not group/other-writable, and sitting
#     in a directory with those same properties. The user's own
#     ~/.sonomos/ca/proxy-ca-cert.pem is NEVER read by this helper. So the set
#     of certificates this passwordless grant can ever trust has exactly one
#     member, chosen at a moment that DID require a password — two during a
#     CA renewal (decision D-16): the password-gated step then pins the
#     replacement beside the active CA, so both are trusted until the proxy
#     has switched, and `ca-retire` narrows the pin back to the newest.
#
#   * `ca-retire` only ever REMOVES certificates from the pin (it keeps the
#     newest), input-free like the rest. Its worst case is the same as
#     `ca-untrust`'s: captured TLS fails, which is the direction that fails
#     safe.
#
#   * `ca-untrust` removes anchors it identifies ITSELF, by reading each
#     candidate certificate and matching the subject inside the signed
#     certificate (see `_subject_is_ours`) — never a store label, never a
#     filename, never anything from argv. The worst a caller can do with it is
#     remove Sonomos' own anchors, which is the direction that fails safe.
#
# Both verbs are argv-free for the same reason SHIM_PORT and PROXY_GROUP are
# baked-in constants: sudo strips the caller's environment, and anything a
# caller could still influence in a privileged helper is an escalation surface.
#
# KNOWN, DELIBERATE CONSEQUENCE: regenerating the device CA (deleting
# ~/.sonomos/ca) makes $PINNED_CA stale, and `ca-trust` will then keep trusting
# the OLD certificate while the proxy presents the new one — every captured
# handshake fails, loudly and identically to "nothing is trusted". Re-running
# `linux.sh install-redirect-helper $USER` re-pins it. That failure direction is
# chosen on purpose: the alternative (following the user-writable path) is the
# injection primitive above.
#
# WHY RESETTING CONNECTIONS IS HERE, AND WHY THAT IS NOT A WIDENING EITHER
# ------------------------------------------------------------------------
# The redirect is an nftables `nat` hook, and a nat hook only rewrites a
# connection as it is ESTABLISHED. A socket that was already open when
# `redirect-on` ran keeps egressing straight to its destination for the rest of
# its life, and an HTTP/2 client holds one for hours. Live, 2026-09-03: a Claude
# Code prompt carrying two phone numbers reached the provider in the clear
# through a connection opened before the mesh came up, while every surface said
# capture was on (sonomoshq/Proxy#76). The only fix that does not depend on the
# user restarting every AI client is to tear those sockets down so the client
# reconnects — and a reconnect goes through the redirect.
#
# Destroying a socket needs CAP_NET_ADMIN (`ss -K`, INET_DIAG_DESTROY), so it
# lives here. `reset-uncovered` takes NO input at all, exactly like `ca-trust`,
# and computes its whole scope from facts the caller cannot choose:
#
#   * WHOSE sockets: only those whose owner uid is $SUDO_UID. sudo sets that
#     variable itself, after resetting the environment, so it is the invoking
#     user and nobody else. Run without it (root, directly) the verb REFUSES
#     rather than reset every account's connections — and it refuses a
#     $SUDO_UID of 0 too (sudo run by root), which is no user either and
#     which `ss` could not match anyway: it prints no `uid:` for root.
#   * WHICH sockets: ESTABLISHED, destination port 443, destination not
#     loopback — the shape the redirect bends and nothing else.
#   * WHERE TO: the destination address must be one the baked-in $API_HOSTS
#     resolve to right now. That list is a constant in this root-owned file,
#     the same trust pattern as SHIM_PORT and PINNED_CA; it is never read from
#     the user's tree, and a test fails when it drifts from the catalog.
#   * NOT the capture path's own: a socket held by a process whose fsgid is
#     $PROXY_GROUP or $BYPASS_GROUP is skipped — the same `meta skgid`
#     exclusion the ruleset makes. Resetting the proxy's upstream or a bypassed
#     app would only cost them a reconnect the redirect never bends anyway.
#
# So the worst any caller can do with it is make THEIR OWN connections to AI
# API hosts reconnect. That is strictly less than they can already do without
# privilege (kill their own processes), and a reconnect lands INSIDE capture,
# which is the direction that fails safe.
#
# It is a FAITHFUL PORT of linux.sh's redirect_on / redirect_off:
#   redirect-on  = install the one dedicated `sonomos_proxy` inet table that
#                  bends outbound tcp/443 to the shim — but ONLY if the shim is
#                  actually listening (fail-safe; skips with exit 3 otherwise).
#   redirect-off = delete that table (traffic returns to the normal path).
#   redirect-status = READ-ONLY: print "armed" or "disarmed" and exit 0 either
#                  way. Added so the shim itself can periodically confirm the
#                  redirect it depends on is still there — see the "arming
#                  happens once" gap in docs/TRANSPARENT-PROXY.md. Purely
#                  additive: it can only ever ADD a way to detect the redirect
#                  was silently torn down (e.g. by another `sudo -n … redirect-
#                  off` call, which this same sudoers grant already permits
#                  password-free), never a way to change kernel state. A
#                  machine whose sudoers drop-in predates this verb simply has
#                  no grant for it — `sudo -n` fails closed to "not authorised"
#                  exactly as it does for any other unlisted argv, and the
#                  caller must treat that as "cannot verify", never as "OFF".
#   reset-uncovered = (helper-only; linux.sh reaches it solely through here)
#                  reset the invoking user's ESTABLISHED :443 connections
#                  to AI API hosts, verify each one is gone, and print ONE
#                  line on stdout: `reset <n> <comm>(<k>),...` — process names
#                  only, never an endpoint. Meant to run right after an arm
#                  (linux.sh `redirect_on` calls it only when the table was not
#                  already there), because run on an armed machine it resets
#                  captured connections too: the socket table cannot tell a
#                  redirected socket from one that predates the redirect.
#
# It runs as root (via `sudo -n`), so it calls `nft` DIRECTLY — no nested sudo,
# no capability probe, no tty. Keep it in lockstep with linux.sh's redirect_on /
# redirect_off: SAME table, SAME fail-safe, SAME exit 3, SAME gid exclusion.
#
# Verbs — the ONLY accepted argv (anything else -> usage + exit 2):
#   sonomos-redirect redirect-on
#   sonomos-redirect redirect-off
#   sonomos-redirect redirect-status
#   sonomos-redirect ca-trust
#   sonomos-redirect ca-untrust
#   sonomos-redirect ca-status
#   sonomos-redirect ca-retire          (D-16: narrow the pin to its newest certificate)
#   sonomos-redirect reset-uncovered
#   sonomos-redirect team-lock-set      (signed token on stdin, never argv)
#   sonomos-redirect team-lock-status
#   sonomos-redirect team-lock-boot     (root's boot unit only; see below)
#
# THE TEAM LOCK (Teams V1), AND WHY IT IS HERE
# --------------------------------------------
# A Teams member's capture must not be switchable off by a process running as
# that member. Every passwordless door this helper opens was opened for the
# machine's own user, so on a member's machine each one is a way out: the
# grant above lets any same-user process run `redirect-off`; `redirect-on`'s
# self-heal deletes the table whenever the shim is not listening (and the user
# can kill the shim); and the gid exclusions skip any socket whose gid is
# `sonomos-proxy` or `sonomos-bypass` — groups the user is a MEMBER of, so
# `sg`/`newgrp`, or a setgid file the user made while holding the gid, put any
# program outside capture.
#
# `team-lock-set` closes those doors, and only on the word of the Sonomos
# server: it reads a token from stdin, verifies its Ed25519 signature ITSELF
# against the public keys baked into this root-owned file ($SIGNING_PUBLIC_KEYS,
# lockstep with the desktop app's LOCKE_SIGNING_PUBLIC_KEYS), and records it in
# $TEAM_LOCK_FILE, which only root can write. While that record says locked:
#
#   * `redirect-off` is REFUSED (exit $EXIT_TEAM_LOCKED). Capture stays on.
#   * `redirect-on` never deletes a table, not even a stale one: a killed shim
#     leaves :443 bent at a dead port. HTTPS stops, visibly — fail closed — and
#     the shim's restart re-arms in place. (Since 2026-10-06 this holds on
#     every machine, locked or not: see `_drop_stale_table`. What the lock adds
#     is that nobody can follow it with `redirect-off`.)
#   * the ruleset stops exempting `sonomos-bypass`, and on a system install
#     (root-owned engine under $SYSTEM_FLEET) stops exempting `sonomos-proxy`
#     too: its :443 sockets are REJECTED, because the user has held that gid and
#     may have planted a setgid file with it. The proxy's own exemption moves to
#     $LOCK_GROUP, a group with no members, reachable only through
#     $LOCKED_LAUNCHER — a copy of the setgid launcher that admits only
#     Locke's own binaries under $SYSTEM_FLEET, root-owned all the way up
#     (Proxy launch/ 0.5).
#   * outbound udp/443 (QUIC, HTTP/3) is REJECTED for everything but the
#     capture path's own gid: the redirect bends tcp only, and an h3 client
#     reached AI APIs unscreened (Teams V1 review, a real `.deb` run). A
#     rejected QUIC attempt falls back to TCP, which is captured. Every armed
#     table carries this rule since 2026-10-06 (INTERCEPT-06.1); an unlocked
#     one also exempts the bypass group, a locked one does not.
#   * the proxy's own port ($PROXY_PORT) accepts connections only from the
#     capture path's gid. The proxy's upstream is exempt from the redirect, so
#     a member process that dialled it directly with an IP-literal CONNECT got
#     an opaque tunnel to an AI API (same review). The shim, its only client on
#     Linux, holds that gid.
#   * the capture path's processes still holding the old gids are stopped, so
#     the supervisor restarts them through the locked launcher.
#   * a root systemd unit ($BOOT_UNIT) arms the redirect at every boot
#     (`team-lock-boot`), before the network and before anything the member
#     controls runs: nft tables do not survive a reboot, and the session-side
#     arm (Locke, the shim's user unit) is the member's to switch off. Armed
#     with no shim behind it is the fail-closed state: HTTPS stops until the
#     capture path is up. Unlocking disables and removes the unit.
#
# Unlocking takes a NEWER verified token for the SAME account saying
# `lock:false`; another account's token cannot unlock a lock it did not set.
# Without a token, clearing the lock means a password (uninstall, or root):
# whoever holds the administrator password is outside this threat model.
# Locked or not, the record changes nothing for a machine that never received
# a token: no record, no change — every verb behaves exactly as before.
#
# TUNABLES ARE BAKED IN, NOT read from the environment. sudo strips the caller's
# environment anyway, and trusting it in a privileged helper would be an
# escalation vector — so SHIM_PORT / group names are constants below. Running the
# shim on a NON-DEFAULT port or group requires REGENERATING this helper with the
# new values (and re-running `linux.sh install-redirect-helper`).
#
# HTTP/3: every table this helper writes REJECTS outbound udp/443 (QUIC) for
# everything but the capture path's own gid (and, unlocked, the bypass group),
# so an h3 client falls back to TCP, which the redirect bends into capture
# (INTERCEPT-06.1, 2026-10-06; it used to be linux.sh's opt-in QUIC_REJECT=1,
# which this helper could not express). The reject lives in the SAME table as
# the redirect, so `redirect-off` — which deletes the whole table by name —
# removes both in one call, and nothing can leave one without the other.

set -eu

# Pin PATH to trusted system dirs so an attacker-controlled PATH can't shadow
# nft/ss/getent/awk/grep/cut with a hostile binary. (nft/ss live in /usr/sbin on
# some distros and /usr/bin on others; this fixed list covers both.)
PATH='/usr/sbin:/usr/bin:/sbin:/bin'
export PATH

# --- Baked-in constants (deliberately NOT read from the environment) --------
NFT_TABLE='sonomos_proxy'      # the ONE dedicated table; redirect-off == delete it
SHIM_PORT='17343'              # where the shim listens / we redirect to (linux.sh default)
PROXY_PORT='17342'             # the proxy's own listener (the shim's --upstream); lock-gid only while locked
PROXY_GROUP='sonomos-proxy'    # the group the proxy+shim run under; excluded by gid
BYPASS_GROUP='sonomos-bypass'  # apps run in this group are excluded from capture
LOCK_GROUP='sonomos-teamlock'  # while team-locked: the ONLY exempt gid (no members)

# The team-lock record (see the header). Root-owned directory and file; the
# record is world-readable so Locke and the launcher can see the lock, and only
# this helper writes it.
TEAM_LOCK_DIR='/var/lib/sonomos'
TEAM_LOCK_FILE="$TEAM_LOCK_DIR/team-lock.json"

# The redirect journal (D-15, Locke docs/CAPTURE-PAUSE.md §10): one line per
# redirect-on and per redirect-off that actually changed the table, so a pause
# started by running this helper directly (no pause record, no desktop app) is
# still visible in reports. World-readable, root-written, best effort: a failure
# to journal never changes what the verb does.
REDIRECT_JOURNAL="$TEAM_LOCK_DIR/redirect-events.log"

# Where the setgid launchers live (Proxy docs/LAUNCHER-CONTRACT.md §1).
LIBEXEC_DIR='/usr/local/libexec'
LAUNCHER="$LIBEXEC_DIR/sonomos-launch"
LOCKED_LAUNCHER="$LIBEXEC_DIR/sonomos-launch-locked"

# The root-owned engine payload the desktop app's .deb installs: `locke
# bundle`'s artifact, unmodified, so its binaries sit at
# prefix/<Repo>/target/release/ (Locke docs/BUNDLE.md). When it is there (and
# root-owned all the way up), a lock moves the proxy's exemption to
# $LOCK_GROUP and Locke runs this copy; when it is not (a per-user bundle or
# AppImage), the user owns the proxy binary itself, no gid can tell it from
# anything else the user runs, and the lock is PARTIAL (`team-lock-status`
# says `mode user`).
SYSTEM_FLEET='/opt/Locke/resources/fleet'

# The Sonomos server's Ed25519 public keys, kid -> base64url of the raw 32
# bytes: the same map as the desktop app's LOCKE_SIGNING_PUBLIC_KEYS (Teams V1
# CONTRACTS §4). ONE line, compact JSON. Baked in for the reason every other
# constant here is: a key this helper read from anywhere the user can write
# would let the user sign their own unlock. Rotation = add the new kid here,
# ship, re-run the one-time setup (password-gated, like every helper update).
SIGNING_PUBLIC_KEYS='{"2026-10b":"ACnTNbJo_POreTZvnUb-Ay0p_8TDv-ggPUq7BrACg1A"}'

# Where the same key map is published, root-owned, for the desktop app to verify
# tokens with instead of the copy in its own (per-user, member-writable) app
# directory (Teams V1 CONTRACTS §4). Written from SIGNING_PUBLIC_KEYS by every
# `team-lock-set`; never read by this helper.
KEYS_DIR='/etc/sonomos'

# Where `team-lock-set` installs the boot-time arm (see the header), root-owned.
SYSTEMD_UNIT_DIR='/etc/systemd/system'
BOOT_UNIT='sonomos-teamlock-boot.service'

# Exit codes for the team lock. 8: refused because the machine is team-locked
# (redirect-off, or an unlock this token may not perform). 9: the token was
# rejected (bad signature, wrong shape, not newer than the stored one).
EXIT_TEAM_LOCKED=8
EXIT_TOKEN_REJECTED=9

# The certificates `ca-trust` may ever install. Root-owned, outside every
# user-writable directory, written by `linux.sh install-redirect-helper` or
# `first-time-setup` (password-gated steps) and re-verified on every call. See
# the header. ONE certificate, except during a CA renewal (D-16), when the
# password-gated step pins the replacement beside it and `ca-retire` later
# narrows the pin back to the newest.
PINNED_CA='/usr/local/share/sonomos/device-ca.pem'
# Two years and a day, in seconds (the proxy's `LIFETIME_LIMIT_DAYS`, 731):
# a pinned CA valid for longer was minted before two-year CAs (decision D-16)
# and is the OLDER generation for `ca-retire`, whatever its notAfter.
CA_LIFETIME_LIMIT_SECS=63158400

# The stem of the anchor filename we write. Used for the file WE create; removal
# never keys off it (see `_subject_is_ours`) — an anchor from older tooling under
# any other name is found by reading the certificate, which is the whole point.
ANCHOR_STEM='sonomos-proxy-ca'

# Exit codes for the CA verbs, distinct so a supervisor can tell three different
# facts apart instead of collapsing them into "nonzero":
#   0  nothing of ours is trusted (or it was removed and that was VERIFIED)
#   1  a hard error — including "ours is still trusted and we could not remove it"
#   4  could not establish the answer (no openssl / no readable store). NOT
#      "clean": a caller must report this, never swallow it.
EXIT_UNVERIFIABLE=4

# Exit codes for `reset-uncovered`, one per reason nothing (or not everything)
# was reset, so a caller can say WHY rather than "nonzero". 0 is "every
# matching connection was reset and verified gone, possibly none"; 1 a hard
# error, including a reset whose re-read failed and so cannot be verified; 2
# "no user to scope by" ($SUDO_UID unset: not run through sudo; or 0: sudo run
# by root).
EXIT_SS_MISSING=5       # no `ss` (iproute2) to enumerate or destroy sockets with
EXIT_KERNEL_REFUSED=6   # `ss -K` ran and a matching socket was still there
EXIT_UNRESOLVABLE=7     # not one of $API_HOSTS resolved, so no scope exists

# Where process state is read. A constant like every tunable here, never an
# environment variable: the installed helper reads `/proc`, full stop. The
# test suite runs a COPY of this file with this line (and PATH) rewritten,
# which is why the value is on a line of its own.
PROC_ROOT='/proc'

# How long ONE host lookup may take. The lookups run in parallel, so this
# also bounds the whole resolution step: `redirect-on` runs from a supervisor's
# reconcile tick, and a stalled resolver must not stall it.
RESOLVE_TIMEOUT_SECS=3

# The AI API hosts whose connections `reset-uncovered` may reset: every
# `api_hosts` entry in sonomos-vocab's ai-surfaces.json, one per line. Baked in
# for the reason the header gives. LOCKSTEP: tests/redirect_helper_api_hosts_test.rs
# fails when this list and the catalog the proxy is built against disagree, in
# either direction.
#
# Exact names only. The catalog also matches SUBDOMAINS of an entry by rule
# (`host_matches`), and those cannot be enumerated to resolve — a connection to
# `x.githubcopilot.com` on an address `githubcopilot.com` does not share is not
# reset. Named in LINUX.md as a residual.
#
# Three catalog entries carry a WILDCARD (`bedrock-runtime.*.amazonaws.com`,
# `bedrock-runtime-fips.*.amazonaws.com`, `*-aiplatform.googleapis.com`), which
# `getent` cannot resolve. They are baked here as the concrete regional names
# they stand for — the common regions, not all of them — so pre-arm connections
# to those regions are reset; a region not listed here is still CAPTURED after
# arming (the catalog's wildcard decides that), it just is not reset. The
# lockstep test accepts a baked name that matches a catalog wildcard. The FIPS
# regions are the seven AWS publishes a `bedrock-runtime-fips` endpoint for
# (US commercial, Canada, GovCloud; each verified to resolve 2026-10-01) — a
# FIPS name in a region without one would only warn "did not resolve" on every
# arm. The three Azure OpenAI apexes (`openai.azure.com`,
# `cognitiveservices.azure.com`, `services.ai.azure.com`) and the Hugging Face
# Inference Endpoints apex (`endpoints.huggingface.cloud`) are deliberately
# NOT here: every real endpoint is `{resource}.<apex>`, the apex has no
# address, and resource names are unknowable — the test records them as an
# accepted residual rather than listing a name that never resolves.
API_HOSTS='
api.openai.com
chatgpt.com
files.oaiusercontent.com
api.anthropic.com
claude.ai
generativelanguage.googleapis.com
aiplatform.googleapis.com
us-central1-aiplatform.googleapis.com
us-east1-aiplatform.googleapis.com
us-east4-aiplatform.googleapis.com
us-east5-aiplatform.googleapis.com
us-west1-aiplatform.googleapis.com
us-west4-aiplatform.googleapis.com
northamerica-northeast1-aiplatform.googleapis.com
europe-west1-aiplatform.googleapis.com
europe-west2-aiplatform.googleapis.com
europe-west3-aiplatform.googleapis.com
europe-west4-aiplatform.googleapis.com
europe-west9-aiplatform.googleapis.com
asia-northeast1-aiplatform.googleapis.com
asia-southeast1-aiplatform.googleapis.com
asia-south1-aiplatform.googleapis.com
australia-southeast1-aiplatform.googleapis.com
cloudcode-pa.googleapis.com
api.x.ai
api.deepseek.com
api.perplexity.ai
www.perplexity.ai
api.mistral.ai
codestral.mistral.ai
dashscope.aliyuncs.com
dashscope-intl.aliyuncs.com
githubcopilot.com
copilot-proxy.githubusercontent.com
models.github.ai
api.cohere.com
api.cohere.ai
openrouter.ai
api.together.xyz
api.together.ai
api.groq.com
api.fireworks.ai
api-inference.huggingface.co
router.huggingface.co
api.deepinfra.com
api.nebius.com
api.octoai.cloud
api.endpoints.anyscale.com
api.runpod.ai
api.cerebras.ai
api.sambanova.ai
api.novita.ai
api.lepton.ai
api.lambdalabs.com
api.replicate.com
api.ai21.com
api.minimax.io
api.minimaxi.com
api.minimax.chat
api.moonshot.ai
api.moonshot.cn
open.bigmodel.cn
api.z.ai
bedrock-runtime.us-east-1.amazonaws.com
bedrock-runtime.us-east-2.amazonaws.com
bedrock-runtime.us-west-2.amazonaws.com
bedrock-runtime.ca-central-1.amazonaws.com
bedrock-runtime.sa-east-1.amazonaws.com
bedrock-runtime.eu-central-1.amazonaws.com
bedrock-runtime.eu-west-1.amazonaws.com
bedrock-runtime.eu-west-2.amazonaws.com
bedrock-runtime.eu-west-3.amazonaws.com
bedrock-runtime.eu-north-1.amazonaws.com
bedrock-runtime.ap-northeast-1.amazonaws.com
bedrock-runtime.ap-northeast-2.amazonaws.com
bedrock-runtime.ap-south-1.amazonaws.com
bedrock-runtime.ap-southeast-1.amazonaws.com
bedrock-runtime.ap-southeast-2.amazonaws.com
bedrock-runtime-fips.us-east-1.amazonaws.com
bedrock-runtime-fips.us-east-2.amazonaws.com
bedrock-runtime-fips.us-west-1.amazonaws.com
bedrock-runtime-fips.us-west-2.amazonaws.com
bedrock-runtime-fips.ca-central-1.amazonaws.com
bedrock-runtime-fips.us-gov-west-1.amazonaws.com
bedrock-runtime-fips.us-gov-east-1.amazonaws.com
api.stability.ai
api.elevenlabs.io
api.assemblyai.com
api.deepgram.com
api.voyageai.com
api.jina.ai
api.jetbrains.ai
api.app.prod.grazie.aws.intellij.net
api.ai.jetbrains.com.cn
'

usage() {
  echo "Usage: sonomos-redirect {redirect-on|redirect-off|redirect-status|ca-trust|ca-untrust|ca-status|ca-retire|reset-uncovered|team-lock-set|team-lock-status|team-lock-boot}" >&2
  exit 2
}

redirect_on() {
  command -v nft >/dev/null 2>&1 || {
    echo "sonomos-redirect: nft (nftables) not installed." >&2
    exit 1
  }

  # FAIL-SAFE: never bend :443 at a shim that isn't there — that would black-hole
  # ALL outbound HTTPS (every connection redirected to a dead port). If nothing
  # is listening on the shim port, DON'T install, and exit with a DISTINCT
  # non-zero code (3 = "skipped, not armed") so a retrying supervisor (the shim
  # unit's ExecStartPost loop) can tell a skip from a real arm.
  # `ss` brackets an IPv6 local address (`[::1]:17343`) and leaves an IPv4 one
  # bare (`127.0.0.1:17343`), so a bracket is the family test. The two families
  # are counted SEPARATELY — a single pattern matching either spelling passed
  # on a v4-only listener, which is the half-bound case handled below.
  listeners="$(ss -ltnH 2>/dev/null | awk '{print $4}' || true)"
  v4="$(printf '%s\n' "$listeners" | grep -cE "^[^][]*[:.]${SHIM_PORT}\$" || true)"
  v6="$(printf '%s\n' "$listeners" | grep -cE "^\[.*\]:${SHIM_PORT}\$" || true)"

  if [ "$v4" -eq 0 ] && [ "$v6" -eq 0 ]; then
    # A table already here with no shim behind it is HELD, not removed: the
    # shim is restarting, and until it is back every :443 connection is
    # refused rather than sent direct (see `_drop_stale_table`). Then exit 3
    # (skip): nothing new is installed at a dead port.
    _drop_stale_table "shim not listening on :${SHIM_PORT})"
    echo "sonomos-redirect: shim not listening on :${SHIM_PORT} — NOT installing the redirect." >&2
    exit 3
  fi

  # HALF-BOUND SHIM (lockstep with linux.sh's redirect_on, which carries the
  # full reasoning). The table below is `inet`, so the rule bends IPv6 :443 as
  # well; the shim's IPv6 bind is best-effort and can legitimately fail. Arming
  # against a v4-only listener therefore black-holes every IPv6 HTTPS
  # connection on the machine while every health surface reports capture on.
  # Refuse to arm rather than arm v4-only, which would let v6 traffic to a
  # protected AI API host leave completely unscreened and silently.
  # `/proc/net/if_inet6` is absent with no kernel IPv6 and EMPTY when every
  # interface has disable_ipv6=1 — either way there is no v6 traffic to strand.
  if [ "$v6" -eq 0 ] && [ -s /proc/net/if_inet6 ]; then
    _drop_stale_table "shim not listening on IPv6 :${SHIM_PORT})"
    echo "sonomos-redirect: shim listening on IPv4 :${SHIM_PORT} but NOT on IPv6 — NOT installing the redirect (it would black-hole all IPv6 HTTPS)." >&2
    exit 3
  fi

  # THE MIRROR CASE (lockstep with linux.sh's redirect_on, which carries the
  # full reasoning). A v6-primary `--listen [::1]:PORT` with a failed
  # best-effort IPv4 sibling bind (shim/src/server.rs `loopback_sibling`)
  # yields a v6-ONLY listener, and the same inet-family rule would then
  # black-hole every IPv4 :443 connection instead. No /proc probe here, on
  # purpose: unlike IPv6, an IPv4 loopback effectively always exists on
  # Linux, so there is always v4 traffic to strand.
  if [ "$v4" -eq 0 ]; then
    _drop_stale_table "shim not listening on IPv4 :${SHIM_PORT})"
    echo "sonomos-redirect: shim listening on IPv6 :${SHIM_PORT} but NOT on IPv4 — NOT installing the redirect (it would black-hole all IPv4 HTTPS)." >&2
    exit 3
  fi

  local was_armed=1
  nft list table inet "$NFT_TABLE" >/dev/null 2>&1 || was_armed=0
  _write_table
  # Only an arm that changed something is an event (D-15).
  [ "$was_armed" = 1 ] || _journal redirect-on
}

# A table the shim is not behind is HELD, on every machine (INTERCEPT-05.1,
# 2026-10-06). With the shim down — crashed and restarting, say — the redirect
# still bends :443 at its port, so every connection is REFUSED at the kernel
# instead of going out direct and unscreened for the restart gap; the
# restarted shim is then behind it again. It used to be deleted here on an
# unlocked machine ("self-heal"), which is what opened that gap: the
# supervisor's next arm attempt, made while the shim was still down, took the
# redirect away. A table stranded by a hard-killed session is removed by
# `redirect-off` — Locke's start-up sweep, its teardown sentinel, a unit's
# ExecStopPost, `linux.sh off` — never by an arm. Locked, nothing removes it
# (`redirect-off` is refused). <why> is the parenthesised reason.
_drop_stale_table() { # <why>
  if nft list table inet "$NFT_TABLE" >/dev/null 2>&1; then
    if _team_locked; then
      echo "sonomos-redirect: team-locked — leaving the redirect armed ($1; HTTPS is held until the shim is back)." >&2
    else
      echo "sonomos-redirect: leaving the redirect armed ($1 — HTTPS is refused until the shim is back; run redirect-off to remove it instead." >&2
    fi
  fi
  return 0
}

# Install (or replace) the ONE table. No listener check: callers decide that
# (`redirect_on` checks the shim; the team-lock verbs only ever REWRITE a table
# that is already there, so its exemptions change without capture going off).
# The exemptions follow the team-lock record — see the header:
#   unlocked          proxy group + bypass group (+ the lock group if it exists)
#   locked, user mode proxy group only (the bypass group is captured);
#                     also a damaged record
#   locked, system    the lock group only; the proxy group's :443 is REJECTED
_write_table() {
  # LOOP PREVENTION: the proxy's OWN upstream HTTPS must NOT be redirected back
  # into the shim. Exclude it by gid — the proxy (and shim) run in a dedicated
  # group whose traffic this rule skips. The proxy still runs as the login user;
  # only its primary group differs, so the user's own apps stay captured.
  gid=$(getent group "$PROXY_GROUP" | cut -d: -f3)
  if [ -z "$gid" ]; then
    echo "sonomos-redirect: group '$PROXY_GROUP' does not exist — run 'linux.sh setup' first." >&2
    exit 1
  fi
  lock_gid=$(getent group "$LOCK_GROUP" 2>/dev/null | cut -d: -f3 || true)

  # Optional bypass group: any process run in it is EXCLUDED from the redirect
  # (the assistant / cert-pinned apps). Added only if the group exists, and
  # never while team-locked: a member's "uncaptured" apps are captured.
  bypass_gid=''
  if ! _team_locked; then
    bypass_gid=$(getent group "$BYPASS_GROUP" 2>/dev/null | cut -d: -f3 || true)
  fi
  bypass_rule=''
  if [ -n "$bypass_gid" ]; then
    bypass_rule="    # Bypass group '$BYPASS_GROUP' — traffic chosen NOT to capture.
    meta skgid $bypass_gid return
"
  fi

  # The proxy's exemption. Locked on a system install it is the lock group's
  # (members: none; reached only through the locked launcher), and the old
  # proxy gid is not merely captured but REFUSED at :443: bending it would
  # loop a still-running old proxy into the shim, and refusing it is the
  # fail-closed answer for anything else still holding that gid.
  proxy_rule="    meta skgid $gid return
"
  reject_chain=''
  quic_note=''
  label="gid $gid"
  # While locked (either mode, a damaged record included): the rules every
  # locked table carries after its exemptions — see the header. QUIC first,
  # then the proxy's port. Loopback udp is left alone (nothing AI is there).
  locked_refusals="    oifname != \"lo\" udp dport 443 reject
    oifname \"lo\" tcp dport $PROXY_PORT reject with tcp reset
"
  # A damaged record is locked but names no mode: it gets the user-mode rules
  # (bypass captured, proxy group exempt) rather than rules that need a lock
  # group that may not exist — which would refuse to arm at all.
  if _team_locked && [ "$(_lock_field mode)" = "system" ]; then
    if [ -z "$lock_gid" ]; then
      echo "sonomos-redirect: team-locked but group '$LOCK_GROUP' does not exist — run team-lock-set again." >&2
      exit 1
    fi
    proxy_rule="    meta skgid $lock_gid return
    meta skgid $gid return
"
    reject_chain="
  # Team lock: '$PROXY_GROUP' is no longer the capture path's own, so its
  # :443 is refused rather than bent (or let through). Then, for everything
  # but '$LOCK_GROUP': no QUIC, and no direct dial of the proxy's port.
  chain team_lock {
    type filter hook output priority 0; policy accept;
    meta skgid $lock_gid return
    meta skgid $gid oifname != \"lo\" tcp dport 443 reject with tcp reset
$locked_refusals  }"
    label="gid $lock_gid; refusing $gid, udp/443 and :$PROXY_PORT"
  elif [ -n "$lock_gid" ]; then
    # A proxy started under a lock keeps working after it lifts.
    proxy_rule="$proxy_rule    meta skgid $lock_gid return
"
    label="gid $gid + $lock_gid"
  fi
  # Locked in user mode (or a damaged record): the same two refusals, with the
  # proxy group (and the lock group, if it exists) as the capture path's own.
  if [ -z "$reject_chain" ] && _team_locked; then
    reject_chain="
  # Team lock (user mode): no QUIC, and no direct dial of the proxy's port,
  # for everything but the capture path's own gid.
  chain team_lock {
    type filter hook output priority 0; policy accept;
$proxy_rule$locked_refusals  }"
    label="$label; refusing udp/443 and :$PROXY_PORT"
  fi
  # Unlocked: no QUIC either (INTERCEPT-06.1). The redirect bends tcp only, so
  # an HTTP/3 client reached AI hosts over udp/443 unscreened; rejected, it
  # falls back to TCP, which is captured. `reject`, not `drop`: an ICMP
  # port-unreachable fails the h3 attempt in one round trip instead of a
  # timeout. Exempt: the capture path's own gid and the bypass group — the
  # same traffic the redirect leaves alone. Loopback udp is untouched.
  if [ -z "$reject_chain" ]; then
    reject_chain="
  # No QUIC (HTTP/3) while capture is armed: h3 clients fall back to TCP.
  chain quic_reject {
    type filter hook output priority 0; policy accept;
$proxy_rule$bypass_rule    oifname != \"lo\" udp dport 443 reject
  }"
    quic_note="; refusing udp/443"
  fi

  # Idempotent: drop any prior copy of our table first, so re-arming is clean.
  nft delete table inet "$NFT_TABLE" 2>/dev/null || true
  nft -f - <<EOF
table inet $NFT_TABLE {
  chain output {
    type nat hook output priority -100; policy accept;

    # 1. Never redirect the capture path's own traffic ('$PROXY_GROUP', or
    #    while team-locked '$LOCK_GROUP'), or we loop forever. The user's apps
    #    keep their own gid and stay captured.
$proxy_rule$bypass_rule
    # 2. Leave loopback alone (this also covers the shim -> proxy hop).
    oifname "lo" return

    # 3. Bend outbound HTTPS to the shim. :80 and everything else flow normally.
    tcp dport 443 redirect to :$SHIM_PORT
  }$reject_chain
}
EOF
  echo "sonomos-redirect: ON — tcp/443 -> shim :$SHIM_PORT (excluding $label${bypass_gid:+ + $bypass_gid}$quic_note)."
}

# True when Locke (the supervisor or `locke pause`) is an ancestor of this
# helper: its own teardown and arm are not a pause started elsewhere. Walks the
# parent chain through $PROC_ROOT (sudo, shells and scripts sit in between).
_run_by_locke() {
  local pid="$PPID" i=0 comm
  while [ "$i" -lt 16 ] && [ -n "$pid" ] && [ "$pid" -gt 1 ] 2>/dev/null; do
    # The group carries the redirect: a bare `< missing 2>/dev/null` reports
    # the failed open before stderr is redirected (a process that already
    # exited is the normal case here).
    comm="$( { tr -d '\n' < "$PROC_ROOT/$pid/comm"; } 2>/dev/null || true)"
    [ "$comm" = "locke" ] && return 0
    pid="$(awk '/^PPid:/ { print $2; exit }' "$PROC_ROOT/$pid/status" 2>/dev/null || true)"
    i=$((i + 1))
  done
  return 1
}

# _journal <verb> — append "<unix-seconds> <verb> <locke|external> <uid> <user>".
_journal() {
  local who=external uid user
  _run_by_locke && who=locke
  uid="${SUDO_UID:-0}"
  user="$(printf '%s' "${SUDO_USER:-root}" | tr -c 'A-Za-z0-9._-' '_')"
  (
    umask 022
    mkdir -p "$TEAM_LOCK_DIR" 2>/dev/null || exit 0
    # Rotate rather than grow without bound.
    if [ "$(wc -c < "$REDIRECT_JOURNAL" 2>/dev/null || echo 0)" -gt 262144 ]; then
      mv -f "$REDIRECT_JOURNAL" "$REDIRECT_JOURNAL.1" 2>/dev/null || true
    fi
    printf '%s %s %s %s %s\n' "$(date +%s)" "$1" "$who" "$uid" "$user" >> "$REDIRECT_JOURNAL"
  ) 2>/dev/null || true
}

redirect_off() {
  command -v nft >/dev/null 2>&1 || {
    echo "sonomos-redirect: nft (nftables) not installed." >&2
    exit 1
  }
  # Team-locked: refused, whoever asks. This grant is passwordless, so without
  # this line any process running as the member could turn capture off.
  if _team_locked; then
    echo "sonomos-redirect: refused — this machine is team-locked by its Sonomos team policy, so capture stays on. Your team admin can change the policy." >&2
    exit "$EXIT_TEAM_LOCKED"
  fi
  # redirect-off = delete the table. Idempotent: a clean no-op if none is armed.
  if nft list table inet "$NFT_TABLE" >/dev/null 2>&1; then
    nft delete table inet "$NFT_TABLE"
    _journal redirect-off
    echo "sonomos-redirect: OFF — table '$NFT_TABLE' deleted; traffic takes the normal path."
  else
    echo "sonomos-redirect: already off (no '$NFT_TABLE' table present)."
  fi
}

# READ-ONLY. Prints exactly "armed" or "disarmed" on stdout and always exits 0
# (a nonzero exit here would be indistinguishable from "not authorised" to a
# caller using `sudo -n`, collapsing two different facts into one). Runs as
# root already (this whole script only ever runs via `sudo -n`), so there is
# no capability ambiguity the way linux.sh's own `redirect_status` has to
# handle for an unprivileged caller — `nft list table` either finds our table
# or it doesn't.
redirect_status() {
  command -v nft >/dev/null 2>&1 || {
    echo "sonomos-redirect: nft (nftables) not installed." >&2
    exit 1
  }
  if nft list table inet "$NFT_TABLE" >/dev/null 2>&1; then
    echo "armed"
  else
    echo "disarmed"
  fi
}

# --- CA trust: add/remove THIS DEVICE'S OWN anchor, and nothing else --------
#
# One exception, by LOCATION rather than certificate: `ca-untrust` also removes
# DANGLING links in $CERT_LINK_DIR whose target lies in an anchor dir, and the
# `<hash>.0` links that pointed at those — whoever's anchor they once named.
# Only links that no longer resolve, so it removes no trust from anything; the
# same class Debian's `update-ca-certificates` sweeps itself whenever its own
# tally is non-zero. See `_prune_dangling_anchor_links`.
#
# KEEP IN LOCKSTEP with linux.sh's `_subject_is_ours` / `ca_untrust_system` —
# same predicate, same anchor dirs, same verification. The unprivileged twin
# over there is the one the test harness can actually exercise (this file only
# ever runs as root via sudo, and deliberately has NO test-mode environment
# overrides, because a privileged helper that reads the caller's environment is
# the escalation surface it exists to prevent). If you change one, change both.

# Every anchor SOURCE directory any distro layout in scope uses. Removal sweeps
# ALL of them regardless of which one this distro writes to: a machine that was
# re-installed, distro-hopped, or served by older tooling can carry an anchor in
# a directory this distro's own refresh tool no longer writes.
ANCHOR_DIRS='/usr/local/share/ca-certificates /etc/ca-certificates/trust-source/anchors /etc/pki/ca-trust/source/anchors /etc/pki/trust/anchors'

# p11-kit's own persist format, which is where an anchor added by `trust anchor`
# (rather than by dropping a file in an anchor dir) lands on Arch-family systems.
P11_DIRS='/etc/ca-certificates/trust-source'

# The extracted, aggregate bundles. Read-only, and used ONLY as a fast
# certificate-level screen for "is there anything of ours trusted at all".
AGGREGATE_BUNDLES='/etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt'

# The hashed directory OpenSSL reads as its CApath, and which Debian's
# `update-ca-certificates` maintains: one `<name>.pem` link per anchor pointing
# INTO an anchor dir, and one `<hash>.0` link pointing at that. See
# `_prune_dangling_anchor_links`.
CERT_LINK_DIR='/etc/ssl/certs'

# Where this distro's extra anchors go, and what refreshes the derived bundles.
# A baked-in port of linux.sh's _detect_system_store (which reads
# /etc/os-release the same way). Sets SYS_ANCHOR_DIR / SYS_ANCHOR_EXT /
# SYS_UPDATE; leaves them empty when this distro is not one we know.
_detect_system_store() {
  SYS_ANCHOR_DIR=''; SYS_ANCHOR_EXT=''; SYS_UPDATE=''
  _id=''; _id_like=''
  if [ -r /etc/os-release ]; then
    _id="$(. /etc/os-release 2>/dev/null; printf '%s' "${ID:-}")"
    _id_like="$(. /etc/os-release 2>/dev/null; printf '%s' "${ID_LIKE:-}")"
  fi
  case " $_id $_id_like " in
    *" arch "*|*" manjaro "*|*" endeavouros "*)
      if command -v update-ca-trust >/dev/null 2>&1; then
        SYS_ANCHOR_DIR=/etc/ca-certificates/trust-source/anchors; SYS_ANCHOR_EXT=crt
        SYS_UPDATE='update-ca-trust'
      elif command -v trust >/dev/null 2>&1; then
        SYS_ANCHOR_DIR=/etc/ca-certificates/trust-source/anchors; SYS_ANCHOR_EXT=crt
        SYS_UPDATE='trust extract-compat'
      fi ;;
    *" debian "*|*" ubuntu "*)
      if command -v update-ca-certificates >/dev/null 2>&1; then
        SYS_ANCHOR_DIR=/usr/local/share/ca-certificates; SYS_ANCHOR_EXT=crt
        SYS_UPDATE='update-ca-certificates'
      fi ;;
    *" fedora "*|*" rhel "*)
      if command -v update-ca-trust >/dev/null 2>&1; then
        SYS_ANCHOR_DIR=/etc/pki/ca-trust/source/anchors; SYS_ANCHOR_EXT=pem
        SYS_UPDATE='update-ca-trust extract'
      fi ;;
    *" suse "*|*" opensuse "*)
      if command -v update-ca-certificates >/dev/null 2>&1; then
        SYS_ANCHOR_DIR=/etc/pki/trust/anchors; SYS_ANCHOR_EXT=pem
        SYS_UPDATE='update-ca-certificates'
      fi ;;
  esac
  if [ -z "$SYS_ANCHOR_DIR" ]; then
    if command -v update-ca-certificates >/dev/null 2>&1; then
      SYS_ANCHOR_DIR=/usr/local/share/ca-certificates; SYS_ANCHOR_EXT=crt
      SYS_UPDATE='update-ca-certificates'
    elif command -v update-ca-trust >/dev/null 2>&1; then
      if [ -d /etc/ca-certificates/trust-source/anchors ]; then
        SYS_ANCHOR_DIR=/etc/ca-certificates/trust-source/anchors; SYS_ANCHOR_EXT=crt
        SYS_UPDATE='update-ca-trust'
      else
        SYS_ANCHOR_DIR=/etc/pki/ca-trust/source/anchors; SYS_ANCHOR_EXT=pem
        SYS_UPDATE='update-ca-trust extract'
      fi
    fi
  fi
}

# Is this SUBJECT one of ours? The load-bearing predicate of the whole sweep.
#
# It reads the distinguished name INSIDE the signed certificate — not the NSS
# nickname, not the p11-kit label, not the filename. That distinction is the
# entire bug being fixed: `untrust` used to match one hardcoded nickname
# ("Sonomos Proxy CA"), so an anchor installed by older tooling under an older
# name ("Sonomos Privacy Firewall CA", trusted to 2036 on the machine this was
# written against) was invisible to every removal path this stack shipped. A
# label is a local string some tool chose at install time and can rename freely;
# the DN is what the certificate itself asserts, and it is covered by the
# signature.
#
# Both openssl DN spellings are accepted: the modern `CN=x, O=y` and the older
# `/CN=x/O=y`.
_subject_is_ours() {
  printf '%s\n' "$1" | grep -Eqi '(^|[,/])[[:space:]]*(CN|O|OU)[[:space:]]*=[^,/]*sonomos'
}

# Read one PEM/DER certificate file and answer whether it is ours AND
# self-signed (a root). Non-certificates and unreadable files answer "no".
_cert_file_is_ours() {
  _f="$1"
  _s="$(openssl x509 -in "$_f" -noout -subject 2>/dev/null | sed -n 's/^subject= *//p')" || _s=''
  _i="$(openssl x509 -in "$_f" -noout -issuer  2>/dev/null | sed -n 's/^issuer= *//p')"  || _i=''
  [ -n "$_s" ] || return 1
  [ "$_s" = "$_i" ] || return 1
  _subject_is_ours "$_s"
}

# Does the system store currently trust ANY certificate of ours?
# Returns 0 = yes, 1 = no, 4 ($EXIT_UNVERIFIABLE) = cannot tell.
#
# Three sources, unioned rather than tried in order, because each catches what
# the others miss: the anchor SOURCE directories (an anchor dropped there but
# never refreshed into the bundle), p11-kit's persist files (where `trust
# anchor` puts things, and what nothing this stack shipped before ever looked
# at), and the extracted aggregate bundle (the only place an anchor registered
# through some other module shows up). Every one of them is judged by reading
# the CERTIFICATE, never a label.
#
# "Cannot tell" is never rounded down to "no". A green light standing in for a
# check that did not happen is exactly how a trusted root outlives the product.
_system_store_has_ours() {
  command -v openssl >/dev/null 2>&1 || return "$EXIT_UNVERIFIABLE"
  _read_any=0
  for _d in $ANCHOR_DIRS; do
    [ -d "$_d" ] || continue
    _read_any=1
    for _a in "$_d"/*; do
      [ -f "$_a" ] || continue
      if _cert_file_is_ours "$_a"; then return 0; fi
    done
  done
  for _d in $P11_DIRS; do
    [ -d "$_d" ] || continue
    _read_any=1
    for _p in "$_d"/*.p11-kit; do
      [ -f "$_p" ] || continue
      if _cert_file_is_ours "$_p"; then return 0; fi
    done
  done
  for _b in $AGGREGATE_BUNDLES; do
    [ -r "$_b" ] || continue
    _read_any=1
    # One openssl pipeline prints the subject of EVERY root in the bundle
    # (~120 of them) in a single call — a per-certificate loop here would cost
    # a couple of seconds on every session start and every pause/resume.
    _subs="$(openssl crl2pkcs7 -nocrl -certfile "$_b" 2>/dev/null \
             | openssl pkcs7 -print_certs -noout 2>/dev/null \
             | sed -n 's/^subject= *//p')" || _subs=''
    if printf '%s\n' "$_subs" | grep -Eqi '(^|[,/])[[:space:]]*(CN|O|OU)[[:space:]]*=[^,/]*sonomos'; then
      return 0
    fi
  done
  [ "$_read_any" = 1 ] || return "$EXIT_UNVERIFIABLE"
  return 1
}

# Re-verify the pin's provenance on EVERY call, not just at install. This is
# the entire safety argument for granting `ca-trust` and `ca-retire`
# passwordless sudo, so it is checked here rather than assumed from how the
# file got there:
#
#   * not a symlink — `stat` follows one, so a symlink planted at this path
#     would report the OWNERSHIP OF ITS TARGET and let some other root-owned
#     certificate on the machine be installed as a trust anchor instead;
#   * owned by root, and not group- or other-writable — otherwise it is not a
#     pin, it is a drop box;
#   * in a directory with the same properties, since write permission on the
#     directory is enough to replace the file regardless of the file's own
#     mode.
_verify_pin() {
  [ -f "$PINNED_CA" ] || {
    echo "sonomos-redirect: no pinned device CA at $PINNED_CA." >&2
    echo "  Re-run:  sudo linux.sh install-redirect-helper \$USER" >&2
    exit 1
  }
  if [ -h "$PINNED_CA" ]; then
    echo "sonomos-redirect: $PINNED_CA is a symlink — refusing to trust what it points at." >&2
    exit 1
  fi
  _pin_dir="${PINNED_CA%/*}"
  for _p in "$_pin_dir" "$PINNED_CA"; do
    _owner="$(stat -c '%U' "$_p" 2>/dev/null || echo '?')"
    _mode="$(stat -c '%a' "$_p" 2>/dev/null || echo '777')"
    if [ "$_owner" != root ]; then
      echo "sonomos-redirect: $_p is owned by '$_owner', not root — refusing to trust it." >&2
      exit 1
    fi
    # Reject any group- or other-write bit (022, octal). Shell arithmetic reads
    # a leading zero as octal, which is why the mode is prefixed rather than
    # compared as a string.
    if [ "$(( 0$_mode & 022 ))" -ne 0 ]; then
      echo "sonomos-redirect: $_p is mode $_mode (group/other writable) — refusing to trust it." >&2
      exit 1
    fi
  done
}

# How many certificates a PEM file holds, and the Nth of them (1-based).
# The pin holds two during a CA renewal (decision D-16): the active CA and its
# replacement, until the proxy has switched and `ca-retire` narrows it again.
_pem_count() {
  _c="$(grep -c -- '-----BEGIN CERTIFICATE-----' "$1" 2>/dev/null)" || true
  printf '%s\n' "${_c:-0}"
}
_pem_nth() {
  awk -v n="$2" '/-----BEGIN CERTIFICATE-----/{i++} i==n{print} i==n && /-----END CERTIFICATE-----/{exit}' "$1"
}

# Install EXACTLY the pin, one anchor file per certificate — `<stem>.<ext>`,
# then `<stem>-2.<ext>`, … — because Debian's `openssl rehash` skips any file
# holding more than one. Any other `<stem>-*` anchor of ours that the pin no
# longer holds (the old CA after `ca-retire`) is removed in the same call.
ca_trust() {
  _verify_pin
  _detect_system_store
  if [ -z "$SYS_ANCHOR_DIR" ]; then
    echo "sonomos-redirect: don't know where this distro keeps extra trust anchors — nothing written." >&2
    exit 1
  fi
  _n="$(_pem_count "$PINNED_CA")"
  if [ "$_n" -lt 1 ]; then
    echo "sonomos-redirect: $PINNED_CA holds no certificate — nothing written." >&2
    exit 1
  fi
  _changed=0
  _keep=' '
  _i=1
  while [ "$_i" -le "$_n" ]; do
    if [ "$_i" -eq 1 ]; then
      _dst="$SYS_ANCHOR_DIR/$ANCHOR_STEM.$SYS_ANCHOR_EXT"
    else
      _dst="$SYS_ANCHOR_DIR/$ANCHOR_STEM-$_i.$SYS_ANCHOR_EXT"
    fi
    _one="$(mktemp)"
    _pem_nth "$PINNED_CA" "$_i" > "$_one"
    if ! [ -f "$_dst" ] || ! cmp -s "$_one" "$_dst"; then
      install -D -o root -g root -m 0644 "$_one" "$_dst"
      _changed=1
    fi
    rm -f "$_one"
    _keep="$_keep$_dst "
    _i=$((_i + 1))
  done
  for _a in "$SYS_ANCHOR_DIR/$ANCHOR_STEM"-*."$SYS_ANCHOR_EXT"; do
    [ -f "$_a" ] || continue
    case "$_keep" in *" $_a "*) continue ;; esac
    _cert_file_is_ours "$_a" || continue
    rm -f "$_a" && _changed=1
  done
  if [ "$_changed" = 0 ]; then
    echo "sonomos-redirect: CA already trusted ($_n pinned certificate(s) in $SYS_ANCHOR_DIR)."
    return 0
  fi
  # Deliberately unquoted: SYS_UPDATE is a COMMAND plus its argument
  # ("update-ca-trust extract"), set from the fixed table above, never from
  # argv or the environment.
  # shellcheck disable=SC2086
  $SYS_UPDATE >/dev/null
  _prune_dangling_anchor_links
  echo "sonomos-redirect: CA trusted ($_n pinned certificate(s) in $SYS_ANCHOR_DIR)."
}

# `ca-retire` (D-16): narrow the pin to its NEWEST certificate, and — when
# this session's anchors are installed — re-apply them, so the CA the proxy
# switched away from leaves the system store now.
#
# Newest means the newest GENERATION (the proxy's `CertInfo::newer_than`): a
# certificate valid for at most CA_LIFETIME_LIMIT_SECS (two years and a day)
# is newer than any certificate valid for longer — a CA minted before two-year
# CAs (2024-01-01 .. 2034-01-01), which its two-year replacement supersedes
# although it expires years earlier; between two of the same kind, the later
# notAfter is newer.
#
# Input-free like every other verb, and it can only ever REMOVE a certificate
# from the pin, never add one: the worst a caller can do with it is retire the
# old CA before the proxy has switched, which makes captured TLS fail (closed)
# until the proxy switches to the replacement it is now the only one trusting.
# That is strictly less than `ca-untrust`, which this grant already allows.
ca_retire() {
  _verify_pin
  _n="$(_pem_count "$PINNED_CA")"
  if [ "$_n" -le 1 ]; then
    echo "sonomos-redirect: nothing to retire (the pin holds $_n certificate)."
    return 0
  fi
  command -v openssl >/dev/null 2>&1 || {
    echo "sonomos-redirect: openssl is missing — cannot tell which pinned certificate is newest; pin left as it is." >&2
    exit 1
  }
  _best=0
  _best_kind=-1
  _best_end=0
  _i=1
  while [ "$_i" -le "$_n" ]; do
    _dates="$(_pem_nth "$PINNED_CA" "$_i" | openssl x509 -noout -startdate -enddate 2>/dev/null)"
    _start="$(printf '%s\n' "$_dates" | sed -n 's/^notBefore=//p')"
    _end="$(printf '%s\n' "$_dates" | sed -n 's/^notAfter=//p')"
    _from="$(date -u -d "$_start" +%s 2>/dev/null)" || _from=''
    _secs="$(date -u -d "$_end" +%s 2>/dev/null)" || _secs=''
    if [ -z "$_start" ] || [ -z "$_end" ] || [ -z "$_from" ] || [ -z "$_secs" ]; then
      echo "sonomos-redirect: cannot read the validity of pinned certificate $_i — pin left as it is." >&2
      exit 1
    fi
    # 1 = a two-year CA, 0 = one valid for longer (minted before D-16).
    _kind=1
    if [ $((_secs - _from)) -gt "$CA_LIFETIME_LIMIT_SECS" ]; then _kind=0; fi
    if [ "$_kind" -gt "$_best_kind" ] || { [ "$_kind" -eq "$_best_kind" ] && [ "$_secs" -gt "$_best_end" ]; }; then
      _best="$_i"
      _best_kind="$_kind"
      _best_end="$_secs"
    fi
    _i=$((_i + 1))
  done
  _tmp="$(mktemp "${PINNED_CA%/*}/.device-ca.XXXXXX")"
  _pem_nth "$PINNED_CA" "$_best" > "$_tmp"
  if ! _cert_file_is_ours "$_tmp"; then
    rm -f "$_tmp"
    echo "sonomos-redirect: the newest pinned certificate is not a Sonomos root — pin left as it is." >&2
    exit 1
  fi
  chown root:root "$_tmp" && chmod 0644 "$_tmp" && mv -f "$_tmp" "$PINNED_CA" || {
    rm -f "$_tmp"
    echo "sonomos-redirect: could not rewrite $PINNED_CA — pin left as it is." >&2
    exit 1
  }
  echo "sonomos-redirect: retired $((_n - 1)) older certificate(s); the pin holds the newest only."
  # Installed right now (a capture session is up)? Re-apply from the narrowed
  # pin, which drops the retired anchor. Otherwise the next `ca-trust` does.
  _rc=0
  _system_store_has_ours || _rc=$?
  if [ "$_rc" = 0 ]; then
    ca_trust
  fi
}

# Remove EVERY Sonomos anchor this machine carries — the one we installed, and
# any left by older tooling under any other name. Verifies afterwards.
#
# Structured as widening passes with a cheap certificate-level screen between
# them, because this runs on EVERY session exit and every pause/resume, not once
# at uninstall time. The screen is one openssl pipeline over the already
# extracted bundle (~120 roots, tens of milliseconds); the exhaustive pkcs11 pass
# below extracts and parses each anchor individually and costs seconds. Doing the
# expensive one unconditionally would put a multi-second stall on Ctrl-C, which is
# how a teardown ends up being disabled.
#
# Widening never skips a case: pass 3 only exits early once the screen says
# nothing of ours is trusted ANY MORE, which is a statement about the store, not
# about which pass happened to remove it.
ca_untrust() {
  _removed=0
  _failed=0

  # 0. Already clean? The overwhelmingly common answer on a machine that never
  #    had capture on, and on the startup sweep of one that shut down properly.
  #    The links an EARLIER removal left dangling are swept here too — this is
  #    the only call a machine in that state still gets (see
  #    `_prune_dangling_anchor_links`).
  _rc=0; _system_store_has_ours || _rc=$?
  if [ "$_rc" = 1 ]; then
    _prune_dangling_anchor_links
    echo "sonomos-redirect: no Sonomos anchor in the system store."
    return 0
  fi

  # 1. Anchor SOURCE files, every distro layout, matched by reading the
  #    certificate rather than by filename.
  for _d in $ANCHOR_DIRS; do
    [ -d "$_d" ] || continue
    for _a in "$_d"/*; do
      [ -f "$_a" ] || continue
      if _cert_file_is_ours "$_a"; then
        if rm -f "$_a"; then
          echo "sonomos-redirect: removed anchor $_a"
          _removed=$((_removed + 1))
        else
          echo "sonomos-redirect: FAILED to remove anchor $_a" >&2
          _failed=$((_failed + 1))
        fi
      fi
    done
  done

  # 2. p11-kit persist files — where `trust anchor` puts things, and the form
  #    nothing this stack shipped before ever looked at. Each one carries the
  #    certificate inline, so the same certificate-level test applies.
  for _d in $P11_DIRS; do
    [ -d "$_d" ] || continue
    for _p in "$_d"/*.p11-kit; do
      [ -f "$_p" ] || continue
      if _cert_file_is_ours "$_p"; then
        if rm -f "$_p"; then
          echo "sonomos-redirect: removed p11-kit anchor $_p"
          _removed=$((_removed + 1))
        else
          echo "sonomos-redirect: FAILED to remove p11-kit anchor $_p" >&2
          _failed=$((_failed + 1))
        fi
      fi
    done
  done

  # Refresh, then re-screen. Deleting the source without refreshing leaves the
  # certificate live in the extracted bundles and the Java keystore — removed in
  # name only — so the refresh is part of the removal, not a tidy-up after it.
  _refresh_trust_store
  _rc=0; _system_store_has_ours || _rc=$?
  if [ "$_rc" = 1 ] && [ "$_failed" -eq 0 ]; then
    echo "sonomos-redirect: removed $_removed Sonomos anchor(s); system store verified clean."
    return 0
  fi

  # 3. Anchors p11-kit knows about that steps 1 and 2 did not own — resolved by
  #    pkcs11 URI, and selected by EXTRACTING each candidate's certificate and
  #    testing its subject. `trust list`'s label is used for nothing but the
  #    message; the decision is always the certificate.
  if command -v trust >/dev/null 2>&1 && command -v openssl >/dev/null 2>&1; then
    _tmp="$(mktemp 2>/dev/null || echo /tmp/sonomos-ca-probe.$$)"
    trust list 2>/dev/null | grep '^pkcs11:' | while IFS= read -r _uri; do
      trust extract --filter="$_uri" --format=pem-bundle "$_tmp" >/dev/null 2>&1 || continue
      _cert_file_is_ours "$_tmp" || continue
      if trust anchor --remove "$_uri" >/dev/null 2>&1; then
        echo "sonomos-redirect: removed p11-kit anchor $_uri"
      else
        echo "sonomos-redirect: FAILED to remove p11-kit anchor $_uri" >&2
      fi
    done
    rm -f "$_tmp"
  fi
  _refresh_trust_store

  # 4. VERIFY. This is the difference between "we ran some rm commands" and "the
  #    interception capability is gone", and it is why this verb has three
  #    outcomes instead of two.
  #
  #    Pass 3's own successes and failures are not counted: it runs inside a
  #    pipeline, so its subshell cannot report a tally back. It does not need to
  #    — the screen below asks the store directly, which is a stronger question
  #    than "did every command we issued exit 0".
  _rc=0; _system_store_has_ours || _rc=$?
  case "$_rc" in
    0)
      echo "sonomos-redirect: a Sonomos CA is STILL trusted after removal." >&2
      exit 1 ;;
    4)
      echo "sonomos-redirect: removed $_removed anchor(s) but could NOT verify the store is clean" >&2
      echo "  (openssl missing, or no readable CA bundle). Do not read this as clean." >&2
      exit "$EXIT_UNVERIFIABLE" ;;
    *)
      if [ "$_failed" -gt 0 ]; then
        echo "sonomos-redirect: $_failed anchor file(s) could not be removed." >&2
        exit 1
      fi
      echo "sonomos-redirect: removed $_removed Sonomos anchor(s); system store verified clean."
      return 0 ;;
  esac
}

# Re-extract the derived bundles with EVERY refresh tool present, then sweep the
# links the removal left dangling. Never fatal on its own — the verification
# screen that follows is what decides whether the removal actually took.
_refresh_trust_store() {
  for _t in update-ca-trust update-ca-certificates; do
    command -v "$_t" >/dev/null 2>&1 && "$_t" >/dev/null 2>&1 || true
  done
  _prune_dangling_anchor_links
}

# Remove the links in $CERT_LINK_DIR that removing an anchor left dangling.
#
# Debian's `update-ca-certificates` prunes orphaned links ONLY when its own
# added/removed tally is non-zero ("only run if set of files has changed", in
# /usr/sbin/update-ca-certificates), and deleting a LOCAL anchor counts as
# neither. So after every `ca-untrust` the directory kept
# `sonomos-proxy-ca.pem -> /usr/local/share/ca-certificates/sonomos-proxy-ca.crt`
# and its `<hash>.0` link, both dangling, indefinitely (seen 2026-09-25). They
# trust nothing — a link that does not resolve cannot be opened — but they are
# Sonomos-named residue in the trust directory that every audit trips over.
#
# Deliberately narrow; the same class `update-ca-certificates --fresh` itself
# clears, and nothing wider:
#   1. a link that no longer resolves AND whose target lies in one of the
#      anchor dirs this verb sweeps; then
#   2. a link that no longer resolves AND points at a link removed in step 1
#      (the `<hash>.0` -> `<name>.pem` hop).
# The one place removal is by LOCATION rather than by certificate, because the
# certificate is already gone — but never by name, and never a link that
# resolves: removing a dangling link removes no trust from anything.
#
# Not `--fresh`: that re-links every anchor on the machine and re-runs every
# update hook (the Java keystore among them) — seconds, on a path that runs at
# every session exit, which is exactly how a teardown ends up disabled.
# Never fatal: whatever it cannot remove still trusts nothing.
#
# KEEP IN LOCKSTEP with linux.sh's `dangling_anchor_links`.
_prune_dangling_anchor_links() {
  [ -d "$CERT_LINK_DIR" ] || return 0
  _gone=''
  for _l in "$CERT_LINK_DIR"/*; do
    [ -L "$_l" ] || continue
    [ -e "$_l" ] && continue
    _t="$(readlink "$_l" 2>/dev/null)" || continue
    for _d in $ANCHOR_DIRS; do
      case "$_t" in
        "$_d"/*)
          if rm -f "$_l"; then
            echo "sonomos-redirect: removed dangling link $_l -> $_t"
            _gone="$_gone
$_l"
          fi
          break ;;
      esac
    done
  done
  [ -n "$_gone" ] || return 0
  for _l in "$CERT_LINK_DIR"/*; do
    [ -L "$_l" ] || continue
    [ -e "$_l" ] && continue
    _t="$(readlink "$_l" 2>/dev/null)" || continue
    case "$_t" in
      /*) ;;
      *) _t="$CERT_LINK_DIR/$_t" ;;
    esac
    if printf '%s\n' "$_gone" | grep -Fxq -- "$_t"; then
      rm -f "$_l" && echo "sonomos-redirect: removed dangling link $_l -> $_t"
    fi
  done
  return 0
}

# READ-ONLY. Same three outcomes as ca-untrust, no side effects — so a caller
# can ask "is anything of ours still trusted?" without changing anything.
ca_status() {
  _rc=0; _system_store_has_ours || _rc=$?
  case "$_rc" in
    0) echo "trusted" ;;
    4) echo "unknown"; exit "$EXIT_UNVERIFIABLE" ;;
    *) echo "untrusted" ;;
  esac
}

# --- reset-uncovered: the connections arming could not capture -------------
#
# See the header for the whole argument. The order below is the scope, in the
# order it narrows: whose, which shape, which destination, not ours — and only
# then the reset, one 4-tuple at a time, each one re-read afterwards.

# Resolve every $API_HOSTS entry (A and AAAA), in parallel, each lookup bounded.
# Writes the distinct addresses, one per line, to "$1/ips" and the number of
# hosts that answered nothing to "$1/unresolved". An IPv4-mapped spelling is
# folded to plain IPv4, because `ss` prints a dual-stack socket's v4 peer as
# `[::ffff:a.b.c.d]` and `getent` prints the same host as `a.b.c.d`.
_resolve_api_hosts() {
  _bound=''
  if command -v timeout >/dev/null 2>&1; then
    _bound="timeout $RESOLVE_TIMEOUT_SECS"
  fi
  _n=0
  for _h in $API_HOSTS; do
    _n=$((_n + 1))
    # Deliberately unquoted: _bound is a command plus its argument, set just
    # above from a constant, never from argv or the environment.
    # shellcheck disable=SC2086
    ( $_bound getent ahosts "$_h" > "$1/ahosts.$_n" 2>/dev/null || true ) &
  done
  wait
  _missing=0
  for _f in "$1"/ahosts.*; do
    [ -s "$_f" ] || _missing=$((_missing + 1))
  done
  echo "$_missing" > "$1/unresolved"
  cat "$1"/ahosts.* 2>/dev/null \
    | awk '{ ip = tolower($1); sub(/^::ffff:/, "", ip); if (ip != "") print ip }' \
    | sort -u > "$1/ips"
}

reset_uncovered() {
  # WHOSE. sudo sets SUDO_UID itself; a caller cannot choose it. Absent means
  # this was run as root directly, and there is no user to scope to — refuse
  # rather than reset every account's connections.
  case "${SUDO_UID:-}" in
    ''|*[!0-9]*)
      echo "sonomos-redirect: reset-uncovered scopes to the invoking user and there is none (\$SUDO_UID unset) — run it through sudo. Nothing was reset." >&2
      exit 2 ;;
  esac
  # 0 means sudo was run BY ROOT (a root shell, or `sudo linux.sh …` calling
  # sudo again). That is no user either, and it cannot even be scoped: `ss`
  # prints no `uid:` for uid 0, so nothing would match and the verb would
  # print `reset 0` about connections it never looked for.
  case "$SUDO_UID" in
    *[!0]*) ;;
    *)
      echo "sonomos-redirect: reset-uncovered was run through sudo by root (\$SUDO_UID is 0), so there is no user to scope to — run it through sudo as the user whose connections these are. Nothing was reset." >&2
      exit 2 ;;
  esac
  _uid="$SUDO_UID"

  command -v ss >/dev/null 2>&1 || {
    echo "sonomos-redirect: ss (iproute2) not installed — cannot find or reset connections." >&2
    exit "$EXIT_SS_MISSING"
  }

  _work="$(mktemp -d)" || exit 1
  trap 'rm -rf "$_work"' EXIT

  # WHERE TO.
  _resolve_api_hosts "$_work"
  if [ ! -s "$_work/ips" ]; then
    echo "sonomos-redirect: none of the AI API hosts resolved (no DNS?) — cannot tell which connections to reset, so none were." >&2
    exit "$EXIT_UNRESOLVABLE"
  fi
  _unresolved="$(cat "$_work/unresolved")"
  if [ "$_unresolved" != 0 ]; then
    echo "sonomos-redirect: $_unresolved AI API host(s) did not resolve; connections to them cannot be told apart and are left alone." >&2
  fi

  # WHICH SHAPE, WHOSE, WHERE TO — from one read of the socket table. With a
  # single `state` filter `ss` omits the State column; the `off` test keeps
  # this right on a build that prints it anyway.
  #
  # The scan stops at the first `ino:`, and `uid:` counts only before it:
  # `ss -e` prints `[uid:N] ino:N sk:N [fwmark:N] [cgroup:PATH]`, in that
  # order. The cgroup path is the one field on the line a user can name
  # freely, and it may hold spaces, so a `uid:1000` inside it is its own awk
  # field. And `ss` leaves
  # `uid:` out ENTIRELY when the uid is 0, so "the first `uid:`" of a root
  # socket is whatever the cgroup path spells. A socket with no `uid:` before
  # its `ino:` is therefore root's; one with no `ino:` before `sk:` or
  # `cgroup:` is not read at all.
  #
  # Read into a file first, so a failed read is a failure: an empty table from
  # an `ss` that could not ask the kernel would otherwise print `reset 0`, an
  # all-clear nothing substantiated.
  if ! ss -tneH state established dport = :443 > "$_work/table" 2>/dev/null; then
    echo "sonomos-redirect: could not read the socket table (ss failed) — nothing was reset." >&2
    exit 1
  fi
  awk -v uid="$_uid" -v ipfile="$_work/ips" '
        BEGIN { while ((getline l < ipfile) > 0) want[l] = 1 }
        {
          off = ($1 ~ /^[0-9]+$/) ? 0 : 1
          src = $(3 + off); peer = $(4 + off)
          u = "0"; ino = ""
          for (i = 5 + off; i <= NF; i++) {
            if ($i ~ /^ino:[0-9]+$/) { ino = substr($i, 5); break }
            if ($i ~ /^(sk|cgroup):/) break
            if ($i ~ /^uid:[0-9]+$/) u = substr($i, 5)
          }
          if (u != uid || ino == "") next
          port = peer; sub(/^.*:/, "", port)
          if (port != "443") next
          ip = peer; sub(/:[0-9]+$/, "", ip); gsub(/^\[|\]$/, "", ip)
          ip = tolower(ip); sub(/^::ffff:/, "", ip)
          if (ip ~ /^127\./ || ip == "::1" || ip == "0.0.0.0" || ip == "::") next
          if (!(ip in want)) next
          # A socket bound to a device (SO_BINDTODEVICE) has its local end
          # printed as `addr%dev:port`, a spelling the ss filter itself
          # rejects ("an inet prefix is expected"). Split the device off here;
          # the reset names it with `dev =` instead.
          dev = ""
          if ((p = index(src, "%")) > 0) {
            lport = src; sub(/^.*:/, "", lport)
            dev = substr(src, p + 1); sub(/:[0-9]+$/, "", dev)
            src = substr(src, 1, p - 1) ":" lport
          }
          print ino, src, peer, dev
        }' "$_work/table" > "$_work/candidates"

  # NOT OURS. Socket inode -> owning pid in one pass over every fd table (a
  # per-fd `readlink` would fork thousands of times). First holder wins, as in
  # the shim's own index. A socket nobody holds any more has nothing to reset.
  find "$PROC_ROOT"/[0-9]*/fd -maxdepth 1 -lname 'socket:*' -printf '%h %l\n' \
    2>/dev/null > "$_work/owners" || true
  awk '
    NR == FNR {
      n = split($1, parts, "/"); pid = parts[n - 1]
      ino = $2; gsub(/[^0-9]/, "", ino)
      if (!(ino in owner)) owner[ino] = pid
      next
    }
    ($1 in owner) { print $1, owner[$1], $2, $3, $4 }
  ' "$_work/owners" "$_work/candidates" > "$_work/targets"

  _excluded=" $(getent group "$PROXY_GROUP" 2>/dev/null | cut -d: -f3) $(getent group "$BYPASS_GROUP" 2>/dev/null | cut -d: -f3) $(getent group "$LOCK_GROUP" 2>/dev/null | cut -d: -f3) "
  _reset=0
  _left=0
  _unverified=0
  : > "$_work/names"
  # fd 3, so nothing inside the loop can eat the list it is iterating. `_dev`
  # is empty unless the socket is bound to a device.
  while read -r _ino _pid _local _peer _dev <&3; do
    # The fsgid (4th `Gid:` column) is what the ruleset's `meta skgid` sees.
    _fsgid="$(awk '/^Gid:/ { print $5; exit }' "$PROC_ROOT/$_pid/status" 2>/dev/null || true)"
    [ -n "$_fsgid" ] || continue
    case "$_excluded" in *" $_fsgid "*) continue ;; esac
    # A process names itself (prctl PR_SET_NAME), so the name is reduced to a
    # charset that cannot break the one-line output contract.
    _comm="$(tr -d '\n' < "$PROC_ROOT/$_pid/comm" 2>/dev/null | tr -c 'A-Za-z0-9._+-' '_' || true)"
    [ -n "$_comm" ] || _comm='unknown'
    # Exactly this connection: its own local and peer spellings, as `ss`
    # printed them, so nothing is re-derived between read and reset — plus its
    # device, when it is bound to one.
    set -- src "$_local" dst "$_peer"
    [ -z "$_dev" ] || set -- "$@" dev = "$_dev"
    ss -K -tnH state established "$@" </dev/null >/dev/null 2>&1 || true
    # VERIFY by the same 4-tuple, not by re-counting: the client's reconnect
    # is ALSO an established :443 socket to this address (under the redirect,
    # with a new source port), and must not read as a refusal.
    #
    # And only a re-read that SUCCEEDED can say the connection is gone. An
    # `ss` that rejected its filter, or could not reach the kernel, prints
    # nothing too: counted as reset, that is a record saying "reset" about a
    # socket that is still open and unscreened.
    if ! ss -tnH state established "$@" </dev/null > "$_work/still" 2>/dev/null; then
      _unverified=$((_unverified + 1))
    elif [ -s "$_work/still" ]; then
      _left=$((_left + 1))
    else
      _reset=$((_reset + 1))
      printf '%s\n' "$_comm" >> "$_work/names"
    fi
  done 3< "$_work/targets"

  _summary="$(sort "$_work/names" | uniq -c | sort -k1,1nr -k2,2 \
    | awk '{ printf "%s%s(%s)", (NR > 1 ? "," : ""), $2, $1 }')"
  echo "reset $_reset${_summary:+ $_summary}"
  if [ "$_left" -gt 0 ]; then
    echo "sonomos-redirect: $_left connection(s) were still open after 'ss -K' — this kernel or ss cannot destroy sockets (CONFIG_INET_DIAG_DESTROY)." >&2
  fi
  if [ "$_unverified" -gt 0 ]; then
    echo "sonomos-redirect: $_unverified connection(s) could not be re-read after 'ss -K' (ss failed), so they are not counted as reset and may still be open." >&2
  fi
  # A refusal the kernel was seen to make is the more specific reason; either
  # way the connections named on stderr are not in the count.
  [ "$_left" -eq 0 ] || exit "$EXIT_KERNEL_REFUSED"
  [ "$_unverified" -eq 0 ] || exit 1
}

# --- Team lock (Teams V1) — see the header ----------------------------------
#
# The record, written ONLY by `team-lock-set`, one line of compact JSON:
#   {"v":1,"sub":"user_..","uid":1000,"lock":true,"mode":"system","iat":..,
#    "stored_at":..,"token":"<the verified token>"}
# The token is kept so anyone can re-verify the record against the server's key.

# Print the value of <key> in the team-lock record ("" when absent). Reads only
# the file this helper writes itself, so a sed over its fixed shape is enough.
_lock_field() { # <key>
  [ -f "$TEAM_LOCK_FILE" ] || return 0
  sed -n -E "s/.*\"$1\":\"?([A-Za-z0-9_.-]*)\"?[,}].*/\1/p" "$TEAM_LOCK_FILE" 2>/dev/null | head -n 1
}

# Is this machine team-locked? FAIL CLOSED: a record that is present but not a
# well-formed `"lock":false` (unreadable, truncated, not ours) reads as locked —
# only root can write that directory, so a damaged record is damage, never the
# user's choice, and "unlocked" must be said explicitly.
_team_locked() {
  [ -e "$TEAM_LOCK_FILE" ] || [ -L "$TEAM_LOCK_FILE" ] || return 1
  [ "$(_lock_field lock)" = "false" ] && [ -n "$(_lock_field sub)" ] && return 1
  return 0
}

# base64url (no padding) -> raw bytes on stdout. Fails on anything else.
_b64url_decode() { # <text>
  case "$1" in '' | *[!A-Za-z0-9_-]*) return 1 ;; esac
  _pad=''
  case $((${#1} % 4)) in
    1) return 1 ;;
    2) _pad='==' ;;
    3) _pad='=' ;;
  esac
  printf '%s%s' "$1" "$_pad" | tr '_-' '/+' | base64 -d 2>/dev/null
}

# A string / integer / boolean field of a VERIFIED payload (compact JSON, as the
# server's JSON.stringify writes it). Only ever run on bytes whose signature
# checked out, so the shape is the server's, not an attacker's.
_jstr() { printf '%s' "$2" | sed -n -E "s/.*\"$1\":\"([^\"\\\\]*)\".*/\1/p"; }
_jnum() { printf '%s' "$2" | sed -n -E "s/.*\"$1\":([0-9]{1,12})[,}].*/\1/p"; }
_jbool() { printf '%s' "$2" | sed -n -E "s/.*\"$1\":(true|false)[,}].*/\1/p"; }

# Verify <token> (Teams V1 CONTRACTS §4) against $SIGNING_PUBLIC_KEYS. On
# success sets T_SUB, T_IAT, T_LOCK, T_KID and returns 0; otherwise prints why
# on stderr and returns 1. `exp` is deliberately not checked: an expired lock
# stays a lock, and ordering is by `iat` against the stored record.
_verify_team_lock_token() { # <token> <work dir>
  _tok="$1"; _w="$2"
  case "$_tok" in '' | *[!A-Za-z0-9_.-]*) echo "sonomos-redirect: team-lock token: not a token." >&2; return 1 ;; esac
  [ "${#_tok}" -le 2048 ] || { echo "sonomos-redirect: team-lock token: too long." >&2; return 1; }
  _head="${_tok%%.*}"; _sig="${_tok#*.}"
  case "$_sig" in '' | *.*) echo "sonomos-redirect: team-lock token: not a token." >&2; return 1 ;; esac
  [ -n "$_head" ] || { echo "sonomos-redirect: team-lock token: not a token." >&2; return 1; }
  command -v openssl >/dev/null 2>&1 || { echo "sonomos-redirect: team-lock token: openssl is not installed, so the signature cannot be checked." >&2; return 1; }
  printf '%s' "$_head" > "$_w/msg"
  _b64url_decode "$_sig" > "$_w/sig" || { echo "sonomos-redirect: team-lock token: bad signature encoding." >&2; return 1; }
  [ "$(wc -c < "$_w/sig")" -eq 64 ] || { echo "sonomos-redirect: team-lock token: bad signature length." >&2; return 1; }
  T_KID=''
  for _pair in $(printf '%s' "$SIGNING_PUBLIC_KEYS" | tr -d '{} ' | tr ',' '\n' \
      | sed -n -E 's/^"([A-Za-z0-9._-]{1,64})":"([A-Za-z0-9_-]{43})"$/\1=\2/p'); do
    _kid="${_pair%%=*}"; _key="${_pair#*=}"
    _b64url_decode "$_key" > "$_w/raw" || continue
    [ "$(wc -c < "$_w/raw")" -eq 32 ] || continue
    # SubjectPublicKeyInfo DER for an Ed25519 key: a fixed 12-byte prefix + key.
    { printf '\060\052\060\005\006\003\053\145\160\003\041\000'; cat "$_w/raw"; } > "$_w/pub.der"
    if openssl pkeyutl -verify -pubin -inkey "$_w/pub.der" -keyform DER -rawin \
        -in "$_w/msg" -sigfile "$_w/sig" >/dev/null 2>&1; then
      T_KID="$_kid"
      break
    fi
  done
  [ -n "$T_KID" ] || { echo "sonomos-redirect: team-lock token: the signature does not verify against any Sonomos key this helper knows." >&2; return 1; }
  _payload="$(_b64url_decode "$_head")" || { echo "sonomos-redirect: team-lock token: bad payload encoding." >&2; return 1; }
  case "$_payload" in *"
"*) echo "sonomos-redirect: team-lock token: payload is not one line." >&2; return 1 ;; esac
  T_SUB="$(_jstr sub "$_payload")"; T_IAT="$(_jnum iat "$_payload")"; T_LOCK="$(_jbool lock "$_payload")"
  if [ "$(_jnum v "$_payload")" != "1" ] || [ "$(_jstr typ "$_payload")" != "team-lock" ] \
      || [ "$(_jstr iss "$_payload")" != "locke-edge" ] || [ "$(_jstr aud "$_payload")" != "locke-desktop" ] \
      || [ "$(_jstr kid "$_payload")" != "$T_KID" ] || [ -z "$T_IAT" ] || [ -z "$T_LOCK" ]; then
    echo "sonomos-redirect: team-lock token: signed, but not a team-lock token." >&2
    return 1
  fi
  case "$T_SUB" in user_*) ;; *) echo "sonomos-redirect: team-lock token: no account." >&2; return 1 ;; esac
  case "$T_SUB" in *[!A-Za-z0-9_]*) echo "sonomos-redirect: team-lock token: no account." >&2; return 1 ;; esac
  # No "issued in the future" check: `iat` is the SERVER's clock, signed, and
  # only ever compared with the stored record's `iat` (also the server's). A
  # rule against this machine's clock bought nothing and cost the lock: a
  # clock running slow refused every token, and the machine stayed unlocked.
}

# Is the desktop app's engine installed root-owned (the .deb)? Every component
# of the path from / to each binary must be root's and writable by nobody else,
# or the user could swap the binary and the lock group would exempt their code.
# The binaries checked are exactly the locked launcher's targets (Proxy
# launch/src/validate.rs LOCKED_TARGETS, lockstep test there): the capture
# path, and the screening path that proves itself to it by the lock gid.
_system_install() {
  for _bin in "$SYSTEM_FLEET/prefix/Proxy/target/release/proxy" \
      "$SYSTEM_FLEET/prefix/Proxy/target/release/proxy-shim" \
      "$SYSTEM_FLEET/prefix/Bridge/target/release/proxy-bridge" \
      "$SYSTEM_FLEET/prefix/Bridge/target/release/extension-bridge" \
      "$SYSTEM_FLEET/prefix/Spliicer-Guard/target/release/spliicer-guard" \
      "$SYSTEM_FLEET/prefix/Spliicer/target/release/spliicer-service"; do
    _p="$_bin"
    [ -f "$_p" ] && [ ! -L "$_p" ] || return 1
    while :; do
      [ ! -L "$_p" ] || return 1
      [ "$(stat -c %u "$_p" 2>/dev/null)" = "0" ] || return 1
      case "$(stat -c %A "$_p" 2>/dev/null)" in ?????w???? | ????????w?) return 1 ;; esac
      [ "$_p" = "/" ] && break
      _p="$(dirname "$_p")"
    done
  done
}

# Stop the invoking user's processes whose fsgid (what `meta skgid` sees) is one
# of <gids…> — the capture path's old proxy and shim, and anything run in the
# bypass group. The supervisor restarts the capture path through the launcher
# the lock calls for; anything else was leaving capture and now does not.
_stop_gid_holders() { # <uid> <gid…>
  _uid="$1"; shift
  _gids=" $* "
  for _st in "$PROC_ROOT"/[0-9]*/status; do
    [ -r "$_st" ] || continue
    _ruid="$(awk '/^Uid:/ { print $2; exit }' "$_st" 2>/dev/null || true)"
    _fsgid="$(awk '/^Gid:/ { print $5; exit }' "$_st" 2>/dev/null || true)"
    [ "$_ruid" = "$_uid" ] && [ -n "$_fsgid" ] || continue
    case "$_gids" in *" $_fsgid "*) ;; *) continue ;; esac
    _pid="$(basename "$(dirname "$_st")")"
    kill -TERM "$_pid" 2>/dev/null || true
  done
}

# Install the locked launcher: the installed launcher's bytes, group $LOCK_GROUP,
# mode 2755. Only from a root-owned regular file at $LAUNCHER (written by the
# password-gated setup); a launcher older than 0.4 has no locked mode and admits
# nobody under this group, which fails closed (the proxy cannot start).
# The copy comes from the .deb's own payload when it carries one: that launcher
# is the same release as the binaries it must admit (an installed launcher from
# an older one-time setup may predate their paths, and would admit nothing).
# Only called in system mode, where the payload is root-owned all the way up.
_install_locked_launcher() {
  _src="$SYSTEM_FLEET/prefix/Proxy/target/release/sonomos-launch"
  if [ ! -f "$_src" ] || [ -L "$_src" ] || [ "$(stat -c %u "$_src" 2>/dev/null)" != "0" ]; then
    _src="$LAUNCHER"
  fi
  if [ ! -f "$_src" ] || [ -L "$_src" ] || [ "$(stat -c %u "$_src" 2>/dev/null)" != "0" ]; then
    echo "sonomos-redirect: no root-owned launcher at $LAUNCHER — the capture path cannot start while locked until the one-time setup is re-run." >&2
    return 0
  fi
  _new="$LOCKED_LAUNCHER.sonomos-new"
  rm -f "$_new"
  cp "$_src" "$_new"
  chown "root:$LOCK_GROUP" "$_new"
  chmod 2755 "$_new"
  mv -f "$_new" "$LOCKED_LAUNCHER"
}

# Publish $SIGNING_PUBLIC_KEYS as $KEYS_DIR/signing-keys.json (root:root 0644).
_publish_keys() {
  mkdir -p "$KEYS_DIR"
  chown root:root "$KEYS_DIR" 2>/dev/null || true
  chmod 0755 "$KEYS_DIR"
  _k="$KEYS_DIR/signing-keys.json.sonomos-new"
  rm -f "$_k"
  printf '%s\n' "$SIGNING_PUBLIC_KEYS" > "$_k"
  chmod 0644 "$_k"
  mv -f "$_k" "$KEYS_DIR/signing-keys.json"
}

# The boot-time arm: a oneshot root unit that runs `team-lock-boot` before the
# network comes up (and after nftables.service, whose `flush ruleset` would
# otherwise delete the table again). Installed while locked, removed on unlock.
# No systemd, no unit: said, and the lock still stands for the running session.
_install_boot_unit() {
  if ! command -v systemctl >/dev/null 2>&1; then
    echo "sonomos-redirect: no systemd — capture is NOT re-armed automatically after a reboot." >&2
    return 0
  fi
  mkdir -p "$SYSTEMD_UNIT_DIR"
  _u="$SYSTEMD_UNIT_DIR/$BOOT_UNIT.sonomos-new"
  rm -f "$_u"
  cat > "$_u" <<EOF
# Written by sonomos-redirect team-lock-set (Teams V1). Removed on unlock.
[Unit]
Description=Sonomos team lock: arm the capture redirect at boot (fail closed)
DefaultDependencies=no
After=local-fs.target nftables.service
Before=network-pre.target
Wants=network-pre.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=$LIBEXEC_DIR/sonomos-redirect team-lock-boot

[Install]
WantedBy=sysinit.target
EOF
  chmod 0644 "$_u"
  chown root:root "$_u" 2>/dev/null || true
  mv -f "$_u" "$SYSTEMD_UNIT_DIR/$BOOT_UNIT"
  systemctl daemon-reload >/dev/null 2>&1 || true
  systemctl enable "$BOOT_UNIT" >/dev/null 2>&1 \
    || echo "sonomos-redirect: could not enable $BOOT_UNIT — capture is NOT re-armed automatically after a reboot." >&2
}

_remove_boot_unit() {
  [ -e "$SYSTEMD_UNIT_DIR/$BOOT_UNIT" ] || return 0
  if command -v systemctl >/dev/null 2>&1; then
    systemctl disable "$BOOT_UNIT" >/dev/null 2>&1 || true
  fi
  rm -f "$SYSTEMD_UNIT_DIR/$BOOT_UNIT"
  if command -v systemctl >/dev/null 2>&1; then
    systemctl daemon-reload >/dev/null 2>&1 || true
  fi
}

# Root's boot unit: arm the redirect while team-locked, WITHOUT the shim check
# `redirect-on` makes — at boot no shim is up yet, and :443 bent at a dead
# port is the fail-closed state the lock wants until the capture path starts.
# Unlocked (or no record), it does nothing: the unit is a leftover.
team_lock_boot() {
  if ! _team_locked; then
    echo "team-lock: not locked — nothing armed at boot"
    return 0
  fi
  command -v nft >/dev/null 2>&1 || {
    echo "sonomos-redirect: nft (nftables) not installed — cannot arm at boot." >&2
    exit 1
  }
  _write_table >/dev/null
  echo "team-lock: redirect armed at boot (HTTPS waits for the capture path)"
}

team_lock_set() {
  case "${SUDO_UID:-}" in
    '' | 0 | *[!0-9]*)
      echo "sonomos-redirect: team-lock-set records the lock for the invoking user and there is none (\$SUDO_UID unset or 0) — run it through sudo. Nothing changed." >&2
      exit 2
      ;;
  esac
  # The token comes on stdin, never argv (argv is world-readable in /proc).
  _in="$(head -c 4097)"
  if [ "${#_in}" -gt 4096 ]; then
    echo "sonomos-redirect: team-lock token: more than 4 KiB on stdin. Nothing changed." >&2
    exit "$EXIT_TOKEN_REJECTED"
  fi
  _w="$(mktemp -d)"
  trap 'rm -rf "$_w"' EXIT
  _verify_team_lock_token "$_in" "$_w" || exit "$EXIT_TOKEN_REJECTED"

  if [ -e "$TEAM_LOCK_FILE" ] || [ -L "$TEAM_LOCK_FILE" ]; then
    _s_sub="$(_lock_field sub)"; _s_iat="$(_lock_field iat)"; _s_lock="$(_lock_field lock)"
    if [ -z "$_s_sub" ] || [ -z "$_s_iat" ] || { [ "$_s_lock" != "true" ] && [ "$_s_lock" != "false" ]; }; then
      # A damaged record reads as locked; only a lock may replace it.
      if [ "$T_LOCK" != "true" ]; then
        echo "sonomos-redirect: refused — the team-lock record is unreadable, so it stands as a lock; only an administrator can clear it. Nothing changed." >&2
        exit "$EXIT_TEAM_LOCKED"
      fi
    elif [ "$T_SUB" = "$_s_sub" ]; then
      if [ "$T_IAT" -le "$_s_iat" ]; then
        echo "sonomos-redirect: team-lock token: not newer than the one already recorded. Nothing changed." >&2
        exit "$EXIT_TOKEN_REJECTED"
      fi
    elif [ "$_s_lock" = "true" ]; then
      echo "sonomos-redirect: refused — this machine is team-locked for a different Sonomos account, and only that account's newer token can change it. Nothing changed." >&2
      exit "$EXIT_TEAM_LOCKED"
    fi
  fi

  _mode='none'
  if [ "$T_LOCK" = "true" ]; then
    _mode='user'
    _system_install && _mode='system'
  fi
  mkdir -p "$TEAM_LOCK_DIR"
  chown root:root "$TEAM_LOCK_DIR" 2>/dev/null || true
  chmod 0755 "$TEAM_LOCK_DIR"
  _tmp="$TEAM_LOCK_FILE.sonomos-new"
  rm -f "$_tmp"
  printf '{"v":1,"sub":"%s","uid":%s,"lock":%s,"mode":"%s","iat":%s,"stored_at":%s,"token":"%s"}\n' \
    "$T_SUB" "$SUDO_UID" "$T_LOCK" "$_mode" "$T_IAT" "$(date +%s)" "$_in" > "$_tmp"
  chmod 0644 "$_tmp"
  mv -f "$_tmp" "$TEAM_LOCK_FILE"

  _publish_keys
  _proxy_gid="$(getent group "$PROXY_GROUP" 2>/dev/null | cut -d: -f3 || true)"
  _bypass_gid="$(getent group "$BYPASS_GROUP" 2>/dev/null | cut -d: -f3 || true)"
  if [ "$_mode" = "system" ]; then
    getent group "$LOCK_GROUP" >/dev/null 2>&1 || groupadd -r "$LOCK_GROUP"
    _install_locked_launcher
  fi
  if [ "$T_LOCK" = "true" ]; then
    _install_boot_unit
  else
    _remove_boot_unit
  fi
  # Re-write an ARMED table at once, so the exemptions change without capture
  # ever going off; an unarmed machine gets the new rules at its next arm.
  if command -v nft >/dev/null 2>&1 && nft list table inet "$NFT_TABLE" >/dev/null 2>&1; then
    _write_table >/dev/null
  fi
  case "$_mode" in
    system) _stop_gid_holders "$SUDO_UID" $_proxy_gid $_bypass_gid ;;
    user) _stop_gid_holders "$SUDO_UID" $_bypass_gid ;;
  esac
  if [ "$T_LOCK" = "true" ]; then
    echo "team-lock: locked $_mode"
  else
    echo "team-lock: unlocked"
  fi
}

# READ-ONLY. One line on stdout, exit 0: `none` (no record), `unlocked <sub>`,
# `locked <mode> <sub>`, or `locked damaged` (a record that is not well formed,
# which stands as a lock).
team_lock_status() {
  if ! [ -e "$TEAM_LOCK_FILE" ] && ! [ -L "$TEAM_LOCK_FILE" ]; then
    echo "none"
  elif ! _team_locked; then
    echo "unlocked $(_lock_field sub)"
  elif [ -z "$(_lock_field sub)" ] || [ "$(_lock_field lock)" != "true" ]; then
    echo "locked damaged"
  else
    echo "locked $(_lock_field mode) $(_lock_field sub)"
  fi
}

# Accept EXACTLY one argument, and ONLY the verbs we allow.
[ "$#" -eq 1 ] || usage
case "$1" in
  redirect-on)     redirect_on ;;
  redirect-off)    redirect_off ;;
  redirect-status) redirect_status ;;
  ca-trust)        ca_trust ;;
  ca-untrust)      ca_untrust ;;
  ca-status)       ca_status ;;
  ca-retire)       ca_retire ;;
  reset-uncovered) reset_uncovered ;;
  team-lock-set)   team_lock_set ;;
  team-lock-status) team_lock_status ;;
  team-lock-boot)  team_lock_boot ;;
  *)               usage ;;
esac
