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

@@ -32,6 +32,7 @@ def _make_synack(
tcp_flags: int = 0x12, # SYN-ACK
options: list | None = None,
ack: int = 1,
seq: int = 0,
) -> SimpleNamespace:
"""Build a fake scapy-like SYN-ACK packet for testing."""
if options is None:
@@ -52,6 +53,7 @@ def _make_synack(
options=options,
dport=12345,
ack=ack,
seq=seq,
)
ip_layer = SimpleNamespace(
ttl=ttl,
@@ -198,6 +200,11 @@ class TestParseSynack:
assert result["dscp"] == 10
assert result["ecn"] == 2
def test_server_isn_captured(self):
resp = _make_synack(seq=0xDEADBEEF)
result = _parse_synack(resp)
assert result["server_isn"] == 0xDEADBEEF
def test_tos_ce_marked(self):
# ECN CE bit set, no DSCP marking → ToS = 0x03
resp = _make_synack(tos=0x03)