feat(fingerprint): ToS/DSCP/ECN extraction in active + passive TCP fingerprint

Active prober now reads ip.tos from the SYN-ACK and emits tos/dscp/ecn
alongside the existing TTL/window/options fields. dscp is folded into the
fingerprint hash so different DSCP markings produce distinct signatures.

Passive sniffer logs the same three fields on tcp_syn_fingerprint events;
profiler rollup carries them into the attacker tcp_fingerprint snapshot;
AttackerDetail's TCP STACK panel now surfaces DSCP and ECN cells.
This commit is contained in:
2026-04-26 20:25:37 -04:00
parent 453ab177b4
commit b0b08754d0
7 changed files with 131 additions and 26 deletions

View File

@@ -147,6 +147,22 @@ class TestSynFingerprintEmission:
fp_lines = [ln for ln in captured if _msgid(ln) == "tcp_syn_fingerprint"]
assert len(fp_lines) == 2
def test_tos_dscp_ecn_emitted(self):
engine, captured = _make_engine()
# ToS 0x2A → DSCP 10 (AF11), ECN 2 (ECT(0))
pkt = IP(src=_ATTACKER_IP, dst=_DECKY_IP, ttl=64, tos=0x2A) / TCP(
sport=45100, dport=22, flags="S", window=29200,
options=[("MSS", 1460), ("SAckOK", b""), ("Timestamp", (0, 0)),
("NOP", None), ("WScale", 7)],
)
engine.on_packet(pkt)
fp_lines = [ln for ln in captured if _msgid(ln) == "tcp_syn_fingerprint"]
assert len(fp_lines) == 1
f = _fields_from_line(fp_lines[0])
assert f["tos"] == "42"
assert f["dscp"] == "10"
assert f["ecn"] == "2"
def test_decky_source_does_not_emit(self):
"""Packets originating from a decky (outbound reply) should NOT
be classified as an attacker fingerprint."""