cli.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. from __future__ import annotations
  2. import argparse
  3. import asyncio
  4. import json
  5. import re
  6. from collections import defaultdict
  7. from pathlib import Path
  8. from . import __version__
  9. from .config_tcp import TcpConfig
  10. from .config_udp import UdpConfig
  11. from .relay_server_tcp import TcpRelayServer
  12. from .relay_server_udp import UdpRelayServer
  13. from .relay_client_tcp import TcpRelayManager
  14. from .edge_udp import UdpEdge
  15. from .edge_tcp import TcpEdge
  16. TCP_WIN_RE = re.compile(
  17. r"tcp win session=(?P<session>\d+) target=(?P<host>[^:]+):(?P<port>\d+) winner=(?P<winner>\S+) .*?direct=(?P<direct>\d+) .*?relay=(?P<relay>\d+)"
  18. )
  19. UDP_WIN_RE = re.compile(
  20. r"udp flow=(?P<flow>\d+) winner=(?P<winner>\S+) target=(?P<host>[^:]+):(?P<port>\d+)"
  21. )
  22. UDP_SOCKS_SUMMARY_RE = re.compile(
  23. r"udp summary bind=(?P<bind>\S+) flows=(?P<flows>\d+) winners=(?P<winners>\d+) "
  24. r"winner_breakdown=direct=(?P<direct>\d+),relay=(?P<relay>\d+) "
  25. r"sample=(?P<sample>.*?) candidates=(?P<candidates>\[.*?\]) "
  26. r"sent=(?P<sent>\d+) recv=(?P<recv>\d+) dup=(?P<dup>\d+) "
  27. r"direct_paths=(?P<direct_paths>\d+) relay_paths=(?P<relay_paths>\d+) relay_errors=(?P<relay_errors>.*)"
  28. )
  29. def normalize_winner(name: str) -> str:
  30. return "direct" if name.startswith("direct") else name
  31. def parse_summary_log(log_path: Path) -> dict[str, object]:
  32. tcp_total = 0
  33. tcp_direct = 0
  34. tcp_relay = 0
  35. tcp_winners: dict[str, int] = defaultdict(int)
  36. tcp_targets: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
  37. udp_total = 0
  38. udp_direct = 0
  39. udp_relay = 0
  40. udp_winners: dict[str, int] = defaultdict(int)
  41. udp_targets: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
  42. udp_snapshot: dict[str, object] | None = None
  43. for line in log_path.read_text(errors="replace").splitlines():
  44. tcp_match = TCP_WIN_RE.search(line)
  45. if tcp_match:
  46. tcp_total += 1
  47. winner = normalize_winner(tcp_match.group("winner"))
  48. host = tcp_match.group("host")
  49. port = tcp_match.group("port")
  50. key = f"{host}:{port}"
  51. tcp_winners[winner] += 1
  52. tcp_targets[key][winner] += 1
  53. if winner == "direct":
  54. tcp_direct += 1
  55. else:
  56. tcp_relay += 1
  57. continue
  58. udp_match = UDP_WIN_RE.search(line)
  59. if udp_match:
  60. udp_total += 1
  61. winner = normalize_winner(udp_match.group("winner"))
  62. host = udp_match.group("host")
  63. port = udp_match.group("port")
  64. key = f"{host}:{port}"
  65. udp_winners[winner] += 1
  66. udp_targets[key][winner] += 1
  67. if winner == "direct":
  68. udp_direct += 1
  69. else:
  70. udp_relay += 1
  71. continue
  72. udp_snapshot_match = UDP_SOCKS_SUMMARY_RE.search(line)
  73. if udp_snapshot_match:
  74. udp_snapshot = {
  75. "bind": udp_snapshot_match.group("bind"),
  76. "flows": int(udp_snapshot_match.group("flows")),
  77. "winners": int(udp_snapshot_match.group("winners")),
  78. "direct": int(udp_snapshot_match.group("direct")),
  79. "relay": int(udp_snapshot_match.group("relay")),
  80. "sample": udp_snapshot_match.group("sample"),
  81. "candidates": udp_snapshot_match.group("candidates"),
  82. "sent": int(udp_snapshot_match.group("sent")),
  83. "recv": int(udp_snapshot_match.group("recv")),
  84. "dup": int(udp_snapshot_match.group("dup")),
  85. "direct_paths": int(udp_snapshot_match.group("direct_paths")),
  86. "relay_paths": int(udp_snapshot_match.group("relay_paths")),
  87. "relay_errors": udp_snapshot_match.group("relay_errors"),
  88. }
  89. return {
  90. "path": log_path,
  91. "tcp_total": tcp_total,
  92. "tcp_direct": tcp_direct,
  93. "tcp_relay": tcp_relay,
  94. "tcp_winners": tcp_winners,
  95. "tcp_targets": tcp_targets,
  96. "udp_total": udp_total,
  97. "udp_direct": udp_direct,
  98. "udp_relay": udp_relay,
  99. "udp_winners": udp_winners,
  100. "udp_targets": udp_targets,
  101. "udp_snapshot": udp_snapshot,
  102. }
  103. def build_parser() -> argparse.ArgumentParser:
  104. parser = argparse.ArgumentParser(prog="mynetspeeder")
  105. parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
  106. sub = parser.add_subparsers(dest="command", required=True)
  107. relay_tcp = sub.add_parser("relay-tcp", help="在子节点 VPS 上启动 TCP relay")
  108. relay_tcp.add_argument("--listen-host", default="0.0.0.0")
  109. relay_tcp.add_argument("--listen-port", type=int, default=9009)
  110. relay_tcp.add_argument("--token", required=True)
  111. relay_tcp.set_defaults(handler=handle_relay_tcp)
  112. relay_udp = sub.add_parser("relay-udp", help="在子节点 VPS 上启动 UDP relay")
  113. relay_udp.add_argument("--listen-host", default="0.0.0.0")
  114. relay_udp.add_argument("--listen-port", type=int, default=9010)
  115. relay_udp.add_argument("--token", required=True)
  116. relay_udp.set_defaults(handler=handle_relay_udp)
  117. edge_tcp = sub.add_parser("edge-tcp", help="在当前主 VPS 上启动 TCP 透明加速")
  118. edge_tcp.add_argument("--listen-host", default="127.0.0.1")
  119. edge_tcp.add_argument("--listen-port", type=int, default=19080)
  120. edge_tcp.add_argument("--config", required=True)
  121. edge_tcp.add_argument("--kernel", choices=("auto", "20", "24"), default="auto")
  122. edge_tcp.set_defaults(handler=handle_edge_tcp)
  123. edge_udp = sub.add_parser("edge-udp", help="在当前主 VPS 上启动 UDP SOCKS5 加速")
  124. edge_udp.add_argument("--listen-host", default="127.0.0.1")
  125. edge_udp.add_argument("--listen-port", type=int, default=19180)
  126. edge_udp.add_argument("--config", required=True)
  127. edge_udp.set_defaults(handler=handle_edge_udp)
  128. probe = sub.add_parser("probe", help="查看子节点探测与在线状态")
  129. probe.add_argument("--config", required=True)
  130. probe.add_argument("--once", action="store_true")
  131. probe.set_defaults(handler=handle_probe)
  132. summary = sub.add_parser("summary", help="汇总当前架构日志里的胜率")
  133. summary.add_argument("--log-file", default="/var/log/mynetspeeder-tcp-edge.log")
  134. summary.add_argument("--top", type=int, default=10)
  135. summary.add_argument("--json", action="store_true")
  136. summary.set_defaults(handler=handle_summary)
  137. return parser
  138. def handle_relay_tcp(args: argparse.Namespace) -> int:
  139. asyncio.run(TcpRelayServer(args.token).start(args.listen_host, args.listen_port))
  140. return 0
  141. def handle_relay_udp(args: argparse.Namespace) -> int:
  142. asyncio.run(UdpRelayServer(args.token).start(args.listen_host, args.listen_port))
  143. return 0
  144. def handle_edge_tcp(args: argparse.Namespace) -> int:
  145. asyncio.run(TcpEdge(args.listen_host, args.listen_port, TcpConfig.load(args.config), kernel_mode=args.kernel).start())
  146. return 0
  147. def handle_edge_udp(args: argparse.Namespace) -> int:
  148. asyncio.run(UdpEdge(args.listen_host, args.listen_port, UdpConfig.load(args.config)).start())
  149. return 0
  150. def handle_probe(args: argparse.Namespace) -> int:
  151. async def run_probe() -> None:
  152. manager = TcpRelayManager(TcpConfig.load(args.config))
  153. await manager.start()
  154. await asyncio.sleep(2)
  155. print(json.dumps(manager.snapshot(), ensure_ascii=False, indent=2))
  156. if not args.once:
  157. while True:
  158. await asyncio.sleep(5)
  159. print(json.dumps(manager.snapshot(), ensure_ascii=False, indent=2))
  160. asyncio.run(run_probe())
  161. return 0
  162. def handle_summary(args: argparse.Namespace) -> int:
  163. log_path = Path(args.log_file)
  164. if not log_path.exists():
  165. raise SystemExit(f"log file not found: {log_path}")
  166. parsed = parse_summary_log(log_path)
  167. tcp_total = parsed["tcp_total"]
  168. tcp_direct = parsed["tcp_direct"]
  169. tcp_relay = parsed["tcp_relay"]
  170. tcp_winners = parsed["tcp_winners"]
  171. tcp_targets = parsed["tcp_targets"]
  172. udp_total = parsed["udp_total"]
  173. udp_direct = parsed["udp_direct"]
  174. udp_relay = parsed["udp_relay"]
  175. udp_winners = parsed["udp_winners"]
  176. udp_targets = parsed["udp_targets"]
  177. udp_snapshot = parsed["udp_snapshot"]
  178. if udp_total == 0:
  179. sibling_logs = [
  180. log_path.with_name("mynetspeeder-udp-socks.log"),
  181. log_path.with_name("mynetspeeder-relay-tcp.log"),
  182. log_path.with_name("mynetspeeder-relay-udp.log"),
  183. log_path.with_name("mynetspeeder-socks.log"),
  184. log_path.with_name("mynetspeeder-relay.log"),
  185. log_path.with_name("mynetspeeder-edge.log"),
  186. ]
  187. for sibling in sibling_logs:
  188. if not sibling.exists():
  189. continue
  190. sibling_parsed = parse_summary_log(sibling)
  191. if sibling_parsed["udp_total"] == 0 and sibling_parsed["udp_snapshot"] is None:
  192. continue
  193. udp_total = sibling_parsed["udp_total"]
  194. udp_direct = sibling_parsed["udp_direct"]
  195. udp_relay = sibling_parsed["udp_relay"]
  196. udp_winners = sibling_parsed["udp_winners"]
  197. udp_targets = sibling_parsed["udp_targets"]
  198. udp_snapshot = sibling_parsed["udp_snapshot"]
  199. break
  200. tcp_ordered_targets = sorted(
  201. tcp_targets.items(),
  202. key=lambda item: sum(item[1].values()),
  203. reverse=True,
  204. )[: max(args.top, 0)]
  205. udp_ordered_targets = sorted(
  206. udp_targets.items(),
  207. key=lambda item: sum(item[1].values()),
  208. reverse=True,
  209. )[: max(args.top, 0)]
  210. result = {
  211. "log_file": str(log_path),
  212. "tcp": {
  213. "total": tcp_total,
  214. "direct": tcp_direct,
  215. "relay": tcp_relay,
  216. "winners": dict(sorted(tcp_winners.items(), key=lambda item: (-item[1], item[0]))),
  217. "targets": [
  218. {
  219. "target": target,
  220. "wins": dict(sorted(counts.items(), key=lambda item: (-item[1], item[0]))),
  221. }
  222. for target, counts in tcp_ordered_targets
  223. ],
  224. },
  225. "udp": {
  226. "total": udp_total,
  227. "direct": udp_direct,
  228. "relay": udp_relay,
  229. "winners": dict(sorted(udp_winners.items(), key=lambda item: (-item[1], item[0]))),
  230. "targets": [
  231. {
  232. "target": target,
  233. "wins": dict(sorted(counts.items(), key=lambda item: (-item[1], item[0]))),
  234. }
  235. for target, counts in udp_ordered_targets
  236. ],
  237. "snapshot": udp_snapshot,
  238. },
  239. }
  240. if udp_total == 0 and udp_snapshot is not None:
  241. result["udp"]["total"] = udp_snapshot["winners"]
  242. result["udp"]["direct"] = udp_snapshot["direct"]
  243. result["udp"]["relay"] = udp_snapshot["relay"]
  244. if not result["udp"]["winners"] and udp_snapshot is not None:
  245. snapshot_winners: dict[str, int] = {}
  246. if int(udp_snapshot["direct"]) > 0:
  247. snapshot_winners["direct"] = int(udp_snapshot["direct"])
  248. if int(udp_snapshot["relay"]) > 0:
  249. snapshot_winners["relay"] = int(udp_snapshot["relay"])
  250. result["udp"]["winners"] = snapshot_winners
  251. if not result["udp"]["targets"] and udp_snapshot is not None:
  252. snapshot_target_wins: dict[str, int] = {}
  253. if int(udp_snapshot["direct"]) > 0:
  254. snapshot_target_wins["direct"] = int(udp_snapshot["direct"])
  255. if int(udp_snapshot["relay"]) > 0:
  256. snapshot_target_wins["relay"] = int(udp_snapshot["relay"])
  257. result["udp"]["targets"] = [
  258. {
  259. "target": f"snapshot@{udp_snapshot['bind']}",
  260. "wins": snapshot_target_wins,
  261. }
  262. ]
  263. if args.json:
  264. print(json.dumps(result, ensure_ascii=False, indent=2))
  265. return 0
  266. print(f"log: {log_path}")
  267. for protocol in ("tcp", "udp"):
  268. section = result[protocol]
  269. print(f"{protocol}: total={section['total']} direct={section['direct']} relay={section['relay']}")
  270. if protocol == "udp" and section.get("snapshot"):
  271. snapshot = section["snapshot"]
  272. print(
  273. f" snapshot: bind={snapshot['bind']} flows={snapshot['flows']} winners={snapshot['winners']} "
  274. f"direct_paths={snapshot['direct_paths']} relay_paths={snapshot['relay_paths']} dup={snapshot['dup']} "
  275. f"candidates={snapshot['candidates']}"
  276. )
  277. print("winners:")
  278. for name, count in section["winners"].items():
  279. print(f" {name}: {count}")
  280. print("targets:")
  281. for item in section["targets"]:
  282. wins = ", ".join(f"{name}={count}" for name, count in item["wins"].items())
  283. print(f" {item['target']}: {wins}")
  284. return 0
  285. def main() -> int:
  286. parser = build_parser()
  287. args = parser.parse_args()
  288. return args.handler(args)