feat(sniffer): ISN sequence classifier (reuses seq_class helper)

Mirrors the IP-ID classifier for TCP ISN values: per-source-IP rolling
deque (maxlen=8) populated from each inbound SYN's tcp.seq, classified
on every emission. A 'random' verdict is the modern norm; 'incremental',
'zero', or 'constant' indicates legacy stacks or hand-rolled raw-socket
tooling — a strong fingerprint signal.

Active prober now also captures server_isn (single sample, not classified
in-flight; downstream consumers correlating multi-probe results can apply
seq_class.classify_sequence themselves).

Profiler rollup carries the latest non-'unknown' label into
attacker.tcp_fingerprint. Dedup key already covers isn_class from
the previous commit, so transitions emit cleanly.

UI surfaces ISN class as a colour-coded tag with a ⚠ glyph for
non-random verdicts, since they're the genuinely interesting case.
This commit is contained in:
2026-04-26 20:30:24 -04:00
parent 0e40cc8ae1
commit c595d039bd
7 changed files with 67 additions and 0 deletions

View File

@@ -139,6 +139,10 @@ def _parse_synack(resp: Any) -> dict[str, Any]:
# TCP fields
window_size = tcp_layer.window
# Server ISN: single sample from one probe — not classified here, but
# exported so a downstream consumer correlating multiple probes against
# the same target can apply seq_class.classify_sequence().
server_isn = int(getattr(tcp_layer, "seq", 0))
# Parse TCP options
mss = 0
@@ -165,6 +169,7 @@ def _parse_synack(resp: Any) -> dict[str, Any]:
"tos": tos,
"dscp": dscp,
"ecn": ecn,
"server_isn": server_isn,
"mss": mss,
"window_scale": window_scale,
"sack_ok": sack_ok,

View File

@@ -415,6 +415,7 @@ def _tcpfp_phase(
tos=str(result["tos"]),
dscp=str(result["dscp"]),
ecn=str(result["ecn"]),
server_isn=str(result["server_isn"]),
msg=f"TCPFP {ip}:{port} = {result['tcpfp_hash']}",
)
logger.info("prober: TCPFP %s:%d = %s", ip, port, result["tcpfp_hash"])