feat(cloak): broaden mangler to RST/ICMP + L2 responder injection

Mangler now also rewrites egress RST (IP-ID + nonzero ack on bare RSTs → nmap
CI, T4/T6 A=O) and ICMP echo-reply (code=0 → IE.CD=Z), sharing one IP-ID counter
across SYN-ACK/RST/ICMP (reads as a shared sequence, SS=S). Responder injects at
L2 (reflecting probe MACs) so its own RST replies bypass the OUTPUT/NFQUEUE chain
— otherwise the new RST rule re-processed and dropped them. T3 reply ack now A=O.

Live: windows_server decky reads Microsoft Windows Server 2012 (94%, up from 89%);
T2/T3 R=Y, IE.CD=Z, T4/T6 A=O all confirmed coexisting.
This commit is contained in:
2026-06-20 00:35:51 -04:00
parent 65d33bc611
commit 4798a9eb9c
3 changed files with 100 additions and 33 deletions

View File

@@ -15,7 +15,7 @@ from decnet.cloak import (
classify_probe,
next_ipid,
)
from decnet.cloak.mangler import _is_synack
from decnet.cloak.mangler import _is_synack, _rst_needs_ack
from decnet.os_fingerprint import OS_MANGLE, MangleProfile, get_os_mangle
WIN = OS_MANGLE["windows"]
@@ -101,6 +101,15 @@ def test_is_synack(flags, expected):
assert _is_synack(flags) is expected
@pytest.mark.parametrize("flags,expected", [
(0x04, True), # bare RST (T4/T6 ACK-probe response) → fill ack (A=O)
(0x14, False), # RST+ACK (T5/T7) → already A=S+, leave
(0x12, False), # SYN+ACK
])
def test_rst_needs_ack(flags, expected):
assert _rst_needs_ack(flags) is expected
# ── probe classification ────────────────────────────────────────────────────
OPEN = frozenset({22, 80, 443})
@@ -125,6 +134,14 @@ def test_classify_ignores_normal_traffic():
# ── reply field shaping ─────────────────────────────────────────────────────
def test_reply_fields_windows_shape():
f = build_reply_fields(probe_seq=0xDEAD)
def test_reply_fields_t2_ack_equals_probe_seq():
# T2: A=S (ack == probe seq)
f = build_reply_fields(0xDEAD, ProbeKind.T2)
assert f == {"seq": 0, "ack": 0xDEAD, "flags": "RA", "window": 0, "df": True}
def test_reply_fields_t3_ack_is_other():
# T3: A=O (other — not zero, not the probe seq)
f = build_reply_fields(0xDEAD, ProbeKind.T3)
assert f["ack"] not in (0, 0xDEAD)
assert f["seq"] == 0 and f["flags"] == "RA"