diff --git a/decnet/prober/tcpfp.py b/decnet/prober/tcpfp.py index a9c0b82b..62284634 100644 --- a/decnet/prober/tcpfp.py +++ b/decnet/prober/tcpfp.py @@ -133,6 +133,9 @@ def _parse_synack(resp: Any) -> dict[str, Any]: ttl = ip_layer.ttl df_bit = 1 if (ip_layer.flags & 0x2) else 0 # DF = bit 1 ip_id = ip_layer.id + tos = int(getattr(ip_layer, "tos", 0)) + dscp = (tos >> 2) & 0x3F + ecn = tos & 0x3 # TCP fields window_size = tcp_layer.window @@ -159,6 +162,9 @@ def _parse_synack(resp: Any) -> dict[str, Any]: "window_size": window_size, "df_bit": df_bit, "ip_id": ip_id, + "tos": tos, + "dscp": dscp, + "ecn": ecn, "mss": mss, "window_scale": window_scale, "sack_ok": sack_ok, @@ -191,7 +197,8 @@ def _compute_fingerprint(fields: dict[str, Any]) -> tuple[str, str]: raw = ( f"{fields['ttl']}:{fields['window_size']}:{fields['df_bit']}:" f"{fields['mss']}:{fields['window_scale']}:{fields['sack_ok']}:" - f"{fields['timestamp']}:{fields['options_order']}" + f"{fields['timestamp']}:{fields['options_order']}:" + f"{fields.get('dscp', 0)}:{fields.get('ecn', 0)}" ) h = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:32] return raw, h diff --git a/decnet/prober/worker.py b/decnet/prober/worker.py index cdf6e6cb..5f53c2ac 100644 --- a/decnet/prober/worker.py +++ b/decnet/prober/worker.py @@ -412,6 +412,9 @@ def _tcpfp_phase( sack_ok=str(result["sack_ok"]), timestamp=str(result["timestamp"]), options_order=result["options_order"], + tos=str(result["tos"]), + dscp=str(result["dscp"]), + ecn=str(result["ecn"]), msg=f"TCPFP {ip}:{port} = {result['tcpfp_hash']}", ) logger.info("prober: TCPFP %s:%d = %s", ip, port, result["tcpfp_hash"]) diff --git a/decnet/profiler/fingerprint.py b/decnet/profiler/fingerprint.py index 12beb958..37c3efbc 100644 --- a/decnet/profiler/fingerprint.py +++ b/decnet/profiler/fingerprint.py @@ -181,6 +181,9 @@ def sniffer_rollup(events: list[LogEvent]) -> dict[str, Any]: "options_sig": e.fields.get("options_sig", ""), "has_sack": e.fields.get("has_sack") == "true", "has_timestamps": e.fields.get("has_timestamps") == "true", + "tos": _int_or_none(e.fields.get("tos")), + "dscp": _int_or_none(e.fields.get("dscp")), + "ecn": _int_or_none(e.fields.get("ecn")), } tcp_fp_context = "syn" @@ -236,6 +239,9 @@ def sniffer_rollup(events: list[LogEvent]) -> dict[str, Any]: "options_sig": e.fields.get("options_order", ""), "has_sack": e.fields.get("sack_ok") == "1", "has_timestamps": e.fields.get("timestamp") == "1", + "tos": _int_or_none(e.fields.get("tos")), + "dscp": _int_or_none(e.fields.get("dscp")), + "ecn": _int_or_none(e.fields.get("ecn")), } tcp_fp_context = "synack" # prober sent SYN, captured attacker's SYN-ACK diff --git a/decnet/sniffer/fingerprint.py b/decnet/sniffer/fingerprint.py index 5eb1e2ad..f287fee7 100644 --- a/decnet/sniffer/fingerprint.py +++ b/decnet/sniffer/fingerprint.py @@ -1056,6 +1056,9 @@ class SnifferEngine: options_sig=tcp_fp["options_sig"], has_sack=str(tcp_fp["sack_ok"]).lower(), has_timestamps=str(tcp_fp["has_timestamps"]).lower(), + tos=str(int(getattr(ip, "tos", 0))), + dscp=str((int(getattr(ip, "tos", 0)) >> 2) & 0x3F), + ecn=str(int(getattr(ip, "tos", 0)) & 0x3), os_guess=os_label, ) diff --git a/decnet_web/src/components/AttackerDetail.tsx b/decnet_web/src/components/AttackerDetail.tsx index c10eee94..28107991 100644 --- a/decnet_web/src/components/AttackerDetail.tsx +++ b/decnet_web/src/components/AttackerDetail.tsx @@ -19,6 +19,9 @@ interface AttackerBehavior { options_sig?: string; has_sack?: boolean; has_timestamps?: boolean; + tos?: number | null; + dscp?: number | null; + ecn?: number | null; } | null; retransmit_count: number; behavior_class: string | null; @@ -688,7 +691,7 @@ const BeaconBlock: React.FC<{ b: AttackerBehavior }> = ({ b }) => { const TcpStackBlock: React.FC<{ b: AttackerBehavior }> = ({ b }) => { const fp = b.tcp_fingerprint; - if (!fp || (!fp.window && !fp.mss && !fp.options_sig)) return null; + if (!fp || (!fp.window && !fp.mss && !fp.options_sig && fp.dscp == null && fp.ecn == null)) return null; return (