npcap_capture.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. """Capture TCP traffic between cq2.exe and gfgame.dhsf.xqhuyu.com:12000
  2. via Npcap on the loopback adapter. Zero process injection. Zero ban risk.
  3. Usage (after Npcap is installed and game is running):
  4. uv run python scripts/npcap_capture.py --out capture.pcap
  5. Output: standard .pcap (Wireshark-compatible). Then we open it in Python
  6. (scapy) to extract the cq2 <-> game-server TCP stream and look for the
  7. CS 2001 login frame.
  8. """
  9. import argparse
  10. import asyncio
  11. import os
  12. import sys
  13. import time
  14. from pathlib import Path
  15. sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, "src"))
  16. def require_pyshark():
  17. try:
  18. import pyshark # noqa
  19. except ImportError:
  20. print("pyshark not installed. run: uv add pyshark", file=sys.stderr)
  21. sys.exit(1)
  22. async def capture(out: Path, duration: int, host_filter: str, port: int) -> int:
  23. require_pyshark()
  24. import pyshark
  25. # Npcap loopback adapter captures ALL local TCP (127.0.0.1)
  26. # We filter for: TCP port == 12000 (cq2 game gateway)
  27. # OR: TCP containing dhsf.xqhuyu.com (server IP resolved to local)
  28. bpf = f"tcp port {port}"
  29. if host_filter:
  30. bpf = f"tcp and (port {port} or host {host_filter})"
  31. print(f"[npcap] capturing on loopback: '{bpf}'")
  32. print(f"[npcap] writing to {out} for {duration}s")
  33. print(f"[npcap] >>> NOW: open 996box, login, select 玖扬复古176, click 启动 <<<")
  34. # pyshark LiveCapture uses tshark under the hood; needs Npcap installed
  35. cap = pyshark.LiveCapture(
  36. interface=r"\Device\NPF_Loopback", # Npcap loopback; first/only loopback
  37. bpf_filter=bpf,
  38. output_file=str(out),
  39. )
  40. end = time.time() + duration
  41. cap.start()
  42. try:
  43. # cap.consume() in a thread; we wait then stop
  44. while time.time() < end:
  45. await asyncio.sleep(1)
  46. print(f" ... {int(end - time.time())}s left", end="\r")
  47. finally:
  48. cap.stop()
  49. print()
  50. print(f"[npcap] done -> {out}")
  51. return 0
  52. def parse_pcap_for_cs2001(pcap_path: Path) -> list[dict]:
  53. """Read the pcap and dump cq2 <-> game server TCP segments.
  54. We need to identify the CS 2001 first frame: 24-byte header, msg_id=2001
  55. in first int32 little-endian. If we see frames at all we know the
  56. network path is reachable.
  57. """
  58. import pyshark
  59. print(f"\n[parse] reading {pcap_path}")
  60. cap = pyshark.FileCapture(str(pcap_path), display_filter="tcp")
  61. out = []
  62. for i, pkt in enumerate(cap):
  63. try:
  64. ts = pkt.sniff_time.isoformat()
  65. src = pkt.ip.src + ":" + pkt.tcp.srcport
  66. dst = pkt.ip.dst + ":" + pkt.tcp.dstport
  67. payload_hex = bytes.fromhex(pkt.tcp.payload.replace(":", "")).hex() if pkt.tcp.payload else ""
  68. # Try parse header
  69. if len(payload_hex) >= 8:
  70. msg_id = int.from_bytes(bytes.fromhex(payload_hex[:8]), "little") & 0xFFFFFFFF
  71. else:
  72. msg_id = None
  73. entry = {
  74. "i": i, "ts": ts, "src": src, "dst": dst,
  75. "len": len(bytes.fromhex(payload_hex)) // 2 if payload_hex else 0,
  76. "msg_id_guess": msg_id,
  77. "hex": payload_hex,
  78. }
  79. out.append(entry)
  80. except Exception as e:
  81. print(f" pkt {i} err: {e}")
  82. if i > 0 and i % 200 == 0:
  83. print(f" parsed {i} packets...")
  84. return out
  85. def main() -> int:
  86. parser = argparse.ArgumentParser()
  87. parser.add_argument("--out", default="cq2_capture.pcap")
  88. parser.add_argument("--duration", type=int, default=120, help="seconds to capture")
  89. parser.add_argument("--host", default="dhsf.xqhuyu.com")
  90. parser.add_argument("--port", type=int, default=12000)
  91. parser.add_argument("--parse", action="store_true", help="parse pcap after capture")
  92. args = parser.parse_args()
  93. out = Path(args.out)
  94. out.parent.mkdir(parents=True, exist_ok=True)
  95. rc = asyncio.run(capture(out, args.duration, args.host, args.port))
  96. if rc != 0:
  97. return rc
  98. if args.parse:
  99. pkts = parse_pcap_for_cs2001(out)
  100. print(f"\n=== {len(pkts)} TCP segments with payload ===")
  101. for p in pkts[:30]:
  102. print(f" {p['ts']} {p['src']:>22} -> {p['dst']:>22} "
  103. f"len={p['len']:>4} msg_id?={p['msg_id_guess']} "
  104. f"hex[:40]={p['hex'][:40]}")
  105. # Look for any msg_id == 2001
  106. cs2001 = [p for p in pkts if p["msg_id_guess"] == 2001]
  107. if cs2001:
  108. print(f"\n*** Found {len(cs2001)} CS 2001 frames! ***")
  109. for c in cs2001[:3]:
  110. print(f" hex: {c['hex']}")
  111. else:
  112. print("\n(no msg_id=2001 found in payload headers)")
  113. return 0
  114. if __name__ == "__main__":
  115. sys.exit(main())