cli.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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 import Config
  10. from .relay_server import RelayServer
  11. from .relay_client import RelayManager
  12. from .socks_edge import SocksEdge
  13. from .transparent_edge import TransparentEdge
  14. TCP_WIN_RE = re.compile(
  15. 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+)"
  16. )
  17. UDP_WIN_RE = re.compile(
  18. r"udp flow=(?P<flow>\d+) winner=(?P<winner>\S+) target=(?P<host>[^:]+):(?P<port>\d+)"
  19. )
  20. SOCKS_UDP_SUMMARY_RE = re.compile(
  21. r"udp summary .*?winner_detail=(?P<winner_detail>.*?) (?:target_detail=(?P<target_detail>.*?) )?packets_sent="
  22. )
  23. def normalize_winner(name: str) -> str:
  24. return "direct" if name.startswith("direct") else name
  25. def build_parser() -> argparse.ArgumentParser:
  26. parser = argparse.ArgumentParser(prog="mynetspeeder")
  27. parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
  28. sub = parser.add_subparsers(dest="command", required=True)
  29. relay = sub.add_parser("relay", help="在子节点 VPS 上启动 relay")
  30. relay.add_argument("--listen-host", default="0.0.0.0")
  31. relay.add_argument("--listen-port", type=int, default=9009)
  32. relay.add_argument("--token", required=True)
  33. relay.set_defaults(handler=handle_relay)
  34. edge = sub.add_parser("edge", help="在当前主 VPS 上启动透明 direct 出站加速")
  35. edge.add_argument("--listen-host", default="127.0.0.1")
  36. edge.add_argument("--listen-port", type=int, default=19080)
  37. edge.add_argument("--config", required=True)
  38. edge.add_argument("--enable-udp", action="store_true")
  39. edge.add_argument("--kernel", choices=("auto", "20", "24"), default="auto")
  40. edge.set_defaults(handler=handle_edge)
  41. socks = sub.add_parser("socks", help="在当前主 VPS 上启动显式 SOCKS5 出站加速")
  42. socks.add_argument("--listen-host", default="127.0.0.1")
  43. socks.add_argument("--listen-port", type=int, default=19180)
  44. socks.add_argument("--config", required=True)
  45. socks.set_defaults(handler=handle_socks)
  46. probe = sub.add_parser("probe", help="查看子节点探测与在线状态")
  47. probe.add_argument("--config", required=True)
  48. probe.add_argument("--once", action="store_true")
  49. probe.set_defaults(handler=handle_probe)
  50. summary = sub.add_parser("summary", help="汇总透明模式日志里的胜率")
  51. summary.add_argument("--log-file", default="/var/log/mynetspeeder-edge.log")
  52. summary.add_argument("--socks-log-file", default="/var/log/mynetspeeder-socks.log")
  53. summary.add_argument("--top", type=int, default=10)
  54. summary.add_argument("--json", action="store_true")
  55. summary.set_defaults(handler=handle_summary)
  56. return parser
  57. def handle_relay(args: argparse.Namespace) -> int:
  58. asyncio.run(RelayServer(args.token).start(args.listen_host, args.listen_port))
  59. return 0
  60. def handle_edge(args: argparse.Namespace) -> int:
  61. asyncio.run(TransparentEdge(args.listen_host, args.listen_port, Config.load(args.config), enable_udp=args.enable_udp, kernel_mode=args.kernel).start())
  62. return 0
  63. def handle_socks(args: argparse.Namespace) -> int:
  64. asyncio.run(SocksEdge(args.listen_host, args.listen_port, Config.load(args.config)).start())
  65. return 0
  66. def handle_probe(args: argparse.Namespace) -> int:
  67. async def run_probe() -> None:
  68. manager = RelayManager(Config.load(args.config))
  69. await manager.start()
  70. await asyncio.sleep(2)
  71. print(json.dumps(manager.snapshot(), ensure_ascii=False, indent=2))
  72. if not args.once:
  73. while True:
  74. await asyncio.sleep(5)
  75. print(json.dumps(manager.snapshot(), ensure_ascii=False, indent=2))
  76. asyncio.run(run_probe())
  77. return 0
  78. def handle_summary(args: argparse.Namespace) -> int:
  79. log_path = Path(args.log_file)
  80. if not log_path.exists():
  81. raise SystemExit(f"log file not found: {log_path}")
  82. socks_log_path = Path(args.socks_log_file)
  83. tcp_total = 0
  84. tcp_direct = 0
  85. tcp_relay = 0
  86. tcp_winners: dict[str, int] = defaultdict(int)
  87. tcp_targets: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
  88. udp_total = 0
  89. udp_direct = 0
  90. udp_relay = 0
  91. udp_winners: dict[str, int] = defaultdict(int)
  92. udp_targets: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
  93. for line in log_path.read_text(errors="replace").splitlines():
  94. tcp_match = TCP_WIN_RE.search(line)
  95. if tcp_match:
  96. tcp_total += 1
  97. winner = normalize_winner(tcp_match.group("winner"))
  98. host = tcp_match.group("host")
  99. port = tcp_match.group("port")
  100. key = f"{host}:{port}"
  101. tcp_winners[winner] += 1
  102. tcp_targets[key][winner] += 1
  103. if winner == "direct":
  104. tcp_direct += 1
  105. else:
  106. tcp_relay += 1
  107. continue
  108. udp_match = UDP_WIN_RE.search(line)
  109. if udp_match:
  110. udp_total += 1
  111. winner = normalize_winner(udp_match.group("winner"))
  112. host = udp_match.group("host")
  113. port = udp_match.group("port")
  114. key = f"{host}:{port}"
  115. udp_winners[winner] += 1
  116. udp_targets[key][winner] += 1
  117. if winner == "direct":
  118. udp_direct += 1
  119. else:
  120. udp_relay += 1
  121. socks_udp_wins: dict[int, tuple[str, str | None]] = {}
  122. if socks_log_path.exists():
  123. for line in socks_log_path.read_text(errors="replace").splitlines():
  124. summary_match = SOCKS_UDP_SUMMARY_RE.search(line)
  125. if not summary_match:
  126. continue
  127. winners_raw = summary_match.group("winner_detail").strip()
  128. targets_raw = (summary_match.group("target_detail") or "").strip()
  129. if winners_raw == "none":
  130. continue
  131. target_map: dict[int, str] = {}
  132. if targets_raw and targets_raw != "none":
  133. for item in targets_raw.split(", "):
  134. parts = item.split(":")
  135. if len(parts) < 3:
  136. continue
  137. try:
  138. flow_id = int(parts[0])
  139. except ValueError:
  140. continue
  141. target_map[flow_id] = f"{':'.join(parts[1:-1])}:{parts[-1]}"
  142. for item in winners_raw.split(", "):
  143. flow_parts = item.split(":", 1)
  144. if len(flow_parts) != 2:
  145. continue
  146. try:
  147. flow_id = int(flow_parts[0])
  148. except ValueError:
  149. continue
  150. winner = normalize_winner(flow_parts[1])
  151. socks_udp_wins[flow_id] = (winner, target_map.get(flow_id))
  152. for winner, target in socks_udp_wins.values():
  153. udp_total += 1
  154. udp_winners[winner] += 1
  155. if winner == "direct":
  156. udp_direct += 1
  157. else:
  158. udp_relay += 1
  159. if target:
  160. udp_targets[target][winner] += 1
  161. tcp_ordered_targets = sorted(
  162. tcp_targets.items(),
  163. key=lambda item: sum(item[1].values()),
  164. reverse=True,
  165. )[: max(args.top, 0)]
  166. udp_ordered_targets = sorted(
  167. udp_targets.items(),
  168. key=lambda item: sum(item[1].values()),
  169. reverse=True,
  170. )[: max(args.top, 0)]
  171. result = {
  172. "log_file": str(log_path),
  173. "tcp": {
  174. "total": tcp_total,
  175. "direct": tcp_direct,
  176. "relay": tcp_relay,
  177. "winners": dict(sorted(tcp_winners.items(), key=lambda item: (-item[1], item[0]))),
  178. "targets": [
  179. {
  180. "target": target,
  181. "wins": dict(sorted(counts.items(), key=lambda item: (-item[1], item[0]))),
  182. }
  183. for target, counts in tcp_ordered_targets
  184. ],
  185. },
  186. "udp": {
  187. "total": udp_total,
  188. "direct": udp_direct,
  189. "relay": udp_relay,
  190. "winners": dict(sorted(udp_winners.items(), key=lambda item: (-item[1], item[0]))),
  191. "targets": [
  192. {
  193. "target": target,
  194. "wins": dict(sorted(counts.items(), key=lambda item: (-item[1], item[0]))),
  195. }
  196. for target, counts in udp_ordered_targets
  197. ],
  198. },
  199. }
  200. if args.json:
  201. print(json.dumps(result, ensure_ascii=False, indent=2))
  202. return 0
  203. print(f"log: {log_path}")
  204. if socks_log_path.exists():
  205. print(f"socks_log: {socks_log_path}")
  206. for protocol in ("tcp", "udp"):
  207. section = result[protocol]
  208. print(f"{protocol}: total={section['total']} direct={section['direct']} relay={section['relay']}")
  209. print("winners:")
  210. for name, count in section["winners"].items():
  211. print(f" {name}: {count}")
  212. print("targets:")
  213. for item in section["targets"]:
  214. wins = ", ".join(f"{name}={count}" for name, count in item["wins"].items())
  215. print(f" {item['target']}: {wins}")
  216. return 0
  217. def main() -> int:
  218. parser = build_parser()
  219. args = parser.parse_args()
  220. return args.handler(args)