capture_cq2.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. """Capture cq2 TCP payloads via Npcap. NO BPF — Python-side filtering.
  2. Usage:
  3. uv run python scripts/capture_cq2.py --duration 120
  4. uv run python scripts/capture_cq2.py --show cq2_wire.jsonl
  5. """
  6. import argparse
  7. import json
  8. import struct
  9. import sys
  10. import time
  11. GAME_HOSTS = {"47.118.169.147", "47.118.169.153", "47.118.172.233"}
  12. GAME_PORT = 12000
  13. def make_handler(out: str, counter: dict):
  14. from scapy.layers.l2 import Ether
  15. from scapy.layers.inet import IP, TCP
  16. def on_pkt(pkt):
  17. try:
  18. # The WLAN iface reports linktype=1 which confuses scapy's
  19. # auto-detection; packets arrive as Raw. Re-parse as Ether.
  20. if not pkt.haslayer(IP):
  21. try:
  22. raw = bytes(pkt)
  23. if len(raw) < 34:
  24. return
  25. # try Ether parse of the raw bytes
  26. pkt = Ether(raw)
  27. if not pkt.haslayer(IP):
  28. return
  29. except Exception:
  30. return
  31. if not pkt.haslayer(TCP):
  32. return
  33. ip, tcp = pkt[IP], pkt[TCP]
  34. src_ip, dst_ip = ip.src, ip.dst
  35. sport, dport = int(tcp.sport), int(tcp.dport)
  36. if not (sport == GAME_PORT or dport == GAME_PORT
  37. or src_ip in GAME_HOSTS or dst_ip in GAME_HOSTS):
  38. return
  39. payload = bytes(tcp.payload)
  40. flags = int(tcp.flags)
  41. seq = int(tcp.seq)
  42. # Record control packets (FIN/RST/SYN) even without payload —
  43. # needed to understand the server's close semantics.
  44. if not payload:
  45. if flags & 0x01 or flags & 0x04 or flags & 0x02: # FIN/RST/SYN
  46. rec = {
  47. "ts": round(time.time(), 3),
  48. "src": f"{src_ip}:{sport}",
  49. "dst": f"{dst_ip}:{dport}",
  50. "len": 0,
  51. "flags": flags,
  52. "seq": seq,
  53. "ctrl": True,
  54. }
  55. with open(out, "a", encoding="utf-8") as f:
  56. f.write(json.dumps(rec) + "\n")
  57. fnames = []
  58. if flags & 0x01:
  59. fnames.append("FIN")
  60. if flags & 0x04:
  61. fnames.append("RST")
  62. if flags & 0x02:
  63. fnames.append("SYN")
  64. print(f" [CTRL] {rec['src']:>22} -> {rec['dst']:<22} {'+'.join(fnames)} seq={seq}")
  65. return
  66. msg_id = None
  67. if len(payload) >= 4:
  68. (mid,) = struct.unpack("<i", payload[:4])
  69. msg_id = mid
  70. rec = {
  71. "ts": round(time.time(), 3),
  72. "src": f"{src_ip}:{sport}",
  73. "dst": f"{dst_ip}:{dport}",
  74. "len": len(payload),
  75. "flags": flags,
  76. "seq": seq,
  77. "msg_id_guess": msg_id,
  78. "hex": payload.hex(),
  79. }
  80. with open(out, "a", encoding="utf-8") as f:
  81. f.write(json.dumps(rec) + "\n")
  82. counter["n"] += 1
  83. m = msg_id
  84. tag = ""
  85. if m == 2001:
  86. tag = " *** CS 2001 ***"
  87. elif m == 529:
  88. tag = " *** SC 529 ***"
  89. elif m and 0 < m < 100000:
  90. tag = f" (msg {m})"
  91. print(f" [{counter['n']:>4}] {rec['src']:>22} -> {rec['dst']:>22} len={rec['len']:>4}{tag}")
  92. except Exception as e:
  93. print("pkt err:", e)
  94. return on_pkt
  95. def capture(out: str, duration: int) -> int:
  96. from scapy.config import conf
  97. from scapy.sendrecv import sniff
  98. dev = conf.iface
  99. print(f"capturing on: {dev}")
  100. print(f"python filter: port {GAME_PORT} / game hosts; {duration}s -> {out}")
  101. print()
  102. print(">>> NOW: open 996box, login, 玖扬复古176, 启动 <<<")
  103. print()
  104. counter = {"n": 0}
  105. sniff(iface=dev, prn=make_handler(out, counter), timeout=duration, store=False)
  106. print(f"\ntotal game payloads: {counter['n']} -> {out}")
  107. return 0
  108. def show(path: str) -> int:
  109. from pathlib import Path
  110. p = Path(path)
  111. if not p.exists():
  112. print(f"{path} not found", file=sys.stderr)
  113. return 1
  114. print(f"=== {path} ===")
  115. i = 0
  116. with p.open(encoding="utf-8") as f:
  117. for line in f:
  118. line = line.strip()
  119. if not line:
  120. continue
  121. try:
  122. rec = json.loads(line)
  123. except json.JSONDecodeError:
  124. continue
  125. i += 1
  126. m = rec.get("msg_id_guess")
  127. tag = ""
  128. if m == 2001:
  129. tag = " *** CS 2001 ***"
  130. elif m == 529:
  131. tag = " *** SC 529 ***"
  132. elif m and 0 < m < 100000:
  133. tag = f" (msg {m})"
  134. print(f" [{i:>4}] {rec['src']:>22} -> {rec['dst']:>22} len={rec['len']:>4}{tag}")
  135. if m == 2001:
  136. print(f" HEX: {rec['hex']}")
  137. try:
  138. print(f" ASCII: {bytes.fromhex(rec['hex'])!r}")
  139. except Exception:
  140. pass
  141. return 0
  142. def main() -> int:
  143. ap = argparse.ArgumentParser()
  144. ap.add_argument("--out", default="cq2_wire.jsonl")
  145. ap.add_argument("--duration", type=int, default=120)
  146. ap.add_argument("--show", default=None)
  147. args = ap.parse_args()
  148. if args.show:
  149. return show(args.show)
  150. return capture(args.out, args.duration)
  151. if __name__ == "__main__":
  152. sys.exit(main())