Files
lou dea4339716 L1 伪装器: SYN 重写为 Windows 10 布局(纯函数核心 + NFQUEUE worker)
- rewrite.py: 出向 SYN 剥 TS/window 64240/wscale 8/MSS 1460, 重算 IP+TCP 校验和
- worker.py: NFQUEUE ctypes worker, 原包 ACCEPT 放行, 异常一律放行(9-06 断网复盘铁律)
- config/masquerader.json: 指纹模板外置
- 10 个单测覆盖 Linux/Windows SYN/非 SYN/UDP/IPv6/畸形包
2026-09-09 14:47:04 +08:00

219 lines
6.8 KiB
Python

"""rewrite_syn 单测: Linux SYN -> Windows 布局, 反向 Windows 包不破坏。"""
from __future__ import annotations
import struct
import unittest
from masquerader.rewrite import Fingerprint, rewrite_syn
FP = Fingerprint()
TCP_OPT_EOL = 0
TCP_OPT_NOP = 1
TCP_OPT_MSS = 2
TCP_OPT_WS = 3
TCP_OPT_SACK = 4
TCP_OPT_TS = 8
def _opt_mss(value: int = 1460) -> bytes:
return bytes((TCP_OPT_MSS, 4)) + struct.pack("!H", value)
def _opt_sack() -> bytes:
return bytes((TCP_OPT_SACK, 2))
def _opt_ts(tsval: int = 1000, tsecr: int = 0) -> bytes:
return struct.pack("!BBII", TCP_OPT_TS, 10, tsval, tsecr)
def _opt_nop() -> bytes:
return bytes((TCP_OPT_NOP,))
def _opt_ws(scale: int) -> bytes:
return bytes((TCP_OPT_WS, 3, scale))
def _checksum_fold(data: bytes) -> int:
if len(data) & 1:
data += b"\x00"
total = sum(struct.unpack(f"!{len(data) // 2}H", data))
total = (total >> 16) + (total & 0xFFFF)
total += total >> 16
return total
def _verify_ip(pkt: bytes) -> bool:
ihl = (pkt[0] & 0x0F) * 4
return _checksum_fold(pkt[:ihl]) == 0xFFFF
def _verify_tcp(pkt: bytes) -> bool:
ihl = (pkt[0] & 0x0F) * 4
tcp = pkt[ihl:]
off = (tcp[12] >> 4) * 4
pseudo = (
pkt[12:16] + pkt[16:20] + struct.pack("!BBH", 0, 6, off)
)
return _checksum_fold(pseudo + tcp[:off]) == 0xFFFF
def _build_syn(
options: bytes,
window: int = 29200,
ttl: int = 64,
flags: int = 0x02,
) -> bytes:
opt_pad = options + b"\x00" * ((4 - len(options) % 4) % 4)
tcp_len = 20 + len(opt_pad)
data_offset = tcp_len // 4
tcp = bytearray(20 + len(opt_pad))
struct.pack_into(
"!HHIIBBHHH", tcp, 0,
12345, # sport
443, # dport
1, # seq
0, # ack
data_offset << 4, # byte12: data offset + reserved
flags, # byte13: tcp flags
window, # byte14-15: window
0, # byte16-17: checksum, 下方填入
0, # byte18-19: urgent ptr
)
tcp[20:] = opt_pad
src = bytes([10, 0, 8, 47])
dst = bytes([1, 2, 3, 4])
cs = _checksum_fold(src + dst + struct.pack("!BBH", 0, 6, len(tcp)) + bytes(tcp))
tcp[16:18] = struct.pack("!H", (~cs) & 0xFFFF)
ip_len = 20 + len(tcp)
ip = bytearray(20)
struct.pack_into("!BBHHHBBH4s4s", ip, 0, 0x45, 0, ip_len, 0x1234, 0, ttl, 6, 0, src, dst)
ip_cs = _checksum_fold(bytes(ip))
ip[10:12] = struct.pack("!H", (~ip_cs) & 0xFFFF)
return bytes(ip) + bytes(tcp)
def _linux_options() -> bytes:
return _opt_mss(1460) + _opt_sack() + _opt_ts(0, 0) + _opt_nop() + _opt_ws(7)
def _windows_options() -> bytes:
return _opt_mss(1460) + _opt_sack() + _opt_ts(0, 0) + _opt_nop() + _opt_ws(8)
def _parse_opts(pkt: bytes) -> list[int]:
ihl = (pkt[0] & 0x0F) * 4
tcp = pkt[ihl:]
off = (tcp[12] >> 4) * 4
kinds: list[int] = []
i = 20
while i < off:
kind = tcp[i]
if kind == TCP_OPT_EOL:
break
if kind == TCP_OPT_NOP:
kinds.append(kind)
i += 1
continue
length = tcp[i + 1]
kinds.append(kind)
i += length
return kinds
def _tcp_field(pkt: bytes, offset: int, size: int) -> int:
ihl = (pkt[0] & 0x0F) * 4
return int.from_bytes(pkt[ihl + offset : ihl + offset + size], "big")
class RewriteSynTest(unittest.TestCase):
def test_linux_syn_rewritten_to_windows(self) -> None:
pkt = _build_syn(_linux_options(), window=29200, ttl=64)
out = rewrite_syn(pkt, FP)
assert out is not None
self.assertEqual(_tcp_field(out, 14, 2), 64240) # window
self.assertEqual(_tcp_field(out, 12, 1) >> 4, (20 + 12) // 4) # data offset
kinds = _parse_opts(out)
self.assertNotIn(TCP_OPT_TS, kinds) # TS 已剥
self.assertIn(TCP_OPT_WS, kinds)
self.assertTrue(_verify_ip(out)) # IP checksum 有效
self.assertTrue(_verify_tcp(out)) # TCP checksum 有效
def test_wscale_value_is_8(self) -> None:
pkt = _build_syn(_linux_options(), window=29200)
out = rewrite_syn(pkt, FP)
assert out is not None
ihl = (out[0] & 0x0F) * 4
tcp = out[ihl:]
off = (tcp[12] >> 4) * 4
i = 20
ws_val: int | None = None
while i < off:
kind = tcp[i]
if kind == TCP_OPT_EOL:
break
if kind == TCP_OPT_NOP:
i += 1
continue
length = tcp[i + 1]
if kind == TCP_OPT_WS:
ws_val = tcp[i + 2]
break
i += length
self.assertEqual(ws_val, 8)
def test_ip_total_length_shrinks_by_ts_size(self) -> None:
pkt = _build_syn(_linux_options(), window=29200)
out = rewrite_syn(pkt, FP)
assert out is not None
orig_len = int.from_bytes(pkt[2:4], "big")
new_len = int.from_bytes(out[2:4], "big")
# TS(10) 剥掉 + 补 pad 后 options 从 20 -> 12, 总长减 8
self.assertEqual(orig_len - new_len, 8)
def test_windows_syn_only_strips_ts(self) -> None:
pkt = _build_syn(_windows_options(), window=64240)
out = rewrite_syn(pkt, FP)
assert out is not None
self.assertEqual(_tcp_field(out, 14, 2), 64240) # window 不动
kinds = _parse_opts(out)
self.assertNotIn(TCP_OPT_TS, kinds)
self.assertTrue(_verify_ip(out))
self.assertTrue(_verify_tcp(out))
def test_non_syn_packet_untouched(self) -> None:
pkt = _build_syn(_linux_options(), flags=0x18) # PSH+ACK
self.assertIsNone(rewrite_syn(pkt, FP))
def test_syn_ack_untouched(self) -> None:
pkt = _build_syn(_linux_options(), flags=0x12) # SYN+ACK
self.assertIsNone(rewrite_syn(pkt, FP))
def test_udp_untouched(self) -> None:
pkt = bytearray(_build_syn(_linux_options()))
pkt[9] = 17 # proto UDP
self.assertIsNone(rewrite_syn(bytes(pkt), FP))
def test_ipv6_untouched(self) -> None:
pkt = bytearray(_build_syn(_linux_options()))
pkt[0] = 0x60
self.assertIsNone(rewrite_syn(bytes(pkt), FP))
def test_truncated_packet_untouched(self) -> None:
self.assertIsNone(rewrite_syn(b"\x45\x00\x00\x10", FP))
def test_custom_fingerprint_respected(self) -> None:
fp = Fingerprint(window=65535, wscale=7, strip_timestamps=False, ttl=64)
pkt = _build_syn(_linux_options(), window=29200)
out = rewrite_syn(pkt, fp)
assert out is not None
self.assertEqual(_tcp_field(out, 14, 2), 65535)
kinds = _parse_opts(out)
self.assertIn(TCP_OPT_TS, kinds) # strip_timestamps=False 保留 TS
if __name__ == "__main__":
unittest.main()