cli.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  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. def normalize_winner(name: str) -> str:
  21. return "direct" if name.startswith("direct") else name
  22. def build_parser() -> argparse.ArgumentParser:
  23. parser = argparse.ArgumentParser(prog="mynetspeeder")
  24. parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
  25. sub = parser.add_subparsers(dest="command", required=True)
  26. relay = sub.add_parser("relay", help="在子节点 VPS 上启动 relay")
  27. relay.add_argument("--listen-host", default="0.0.0.0")
  28. relay.add_argument("--listen-port", type=int, default=9009)
  29. relay.add_argument("--token", required=True)
  30. relay.set_defaults(handler=handle_relay)
  31. edge = sub.add_parser("edge", help="在当前主 VPS 上启动透明 direct 出站加速")
  32. edge.add_argument("--listen-host", default="127.0.0.1")
  33. edge.add_argument("--listen-port", type=int, default=19080)
  34. edge.add_argument("--config", required=True)
  35. edge.add_argument("--enable-udp", action="store_true")
  36. edge.add_argument("--kernel", choices=("auto", "20", "24"), default="auto")
  37. edge.set_defaults(handler=handle_edge)
  38. socks = sub.add_parser("socks", help="在当前主 VPS 上启动显式 SOCKS5 出站加速")
  39. socks.add_argument("--listen-host", default="127.0.0.1")
  40. socks.add_argument("--listen-port", type=int, default=19180)
  41. socks.add_argument("--config", required=True)
  42. socks.set_defaults(handler=handle_socks)
  43. probe = sub.add_parser("probe", help="查看子节点探测与在线状态")
  44. probe.add_argument("--config", required=True)
  45. probe.add_argument("--once", action="store_true")
  46. probe.set_defaults(handler=handle_probe)
  47. summary = sub.add_parser("summary", help="汇总透明模式日志里的胜率")
  48. summary.add_argument("--log-file", default="/var/log/mynetspeeder-edge.log")
  49. summary.add_argument("--top", type=int, default=10)
  50. summary.add_argument("--json", action="store_true")
  51. summary.set_defaults(handler=handle_summary)
  52. return parser
  53. def handle_relay(args: argparse.Namespace) -> int:
  54. asyncio.run(RelayServer(args.token).start(args.listen_host, args.listen_port))
  55. return 0
  56. def handle_edge(args: argparse.Namespace) -> int:
  57. asyncio.run(TransparentEdge(args.listen_host, args.listen_port, Config.load(args.config), enable_udp=args.enable_udp, kernel_mode=args.kernel).start())
  58. return 0
  59. def handle_socks(args: argparse.Namespace) -> int:
  60. asyncio.run(SocksEdge(args.listen_host, args.listen_port, Config.load(args.config)).start())
  61. return 0
  62. def handle_probe(args: argparse.Namespace) -> int:
  63. async def run_probe() -> None:
  64. manager = RelayManager(Config.load(args.config))
  65. await manager.start()
  66. await asyncio.sleep(2)
  67. print(json.dumps(manager.snapshot(), ensure_ascii=False, indent=2))
  68. if not args.once:
  69. while True:
  70. await asyncio.sleep(5)
  71. print(json.dumps(manager.snapshot(), ensure_ascii=False, indent=2))
  72. asyncio.run(run_probe())
  73. return 0
  74. def handle_summary(args: argparse.Namespace) -> int:
  75. log_path = Path(args.log_file)
  76. if not log_path.exists():
  77. raise SystemExit(f"log file not found: {log_path}")
  78. tcp_total = 0
  79. tcp_direct = 0
  80. tcp_relay = 0
  81. tcp_winners: dict[str, int] = defaultdict(int)
  82. tcp_targets: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
  83. udp_total = 0
  84. udp_direct = 0
  85. udp_relay = 0
  86. udp_winners: dict[str, int] = defaultdict(int)
  87. udp_targets: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
  88. for line in log_path.read_text(errors="replace").splitlines():
  89. tcp_match = TCP_WIN_RE.search(line)
  90. if tcp_match:
  91. tcp_total += 1
  92. winner = normalize_winner(tcp_match.group("winner"))
  93. host = tcp_match.group("host")
  94. port = tcp_match.group("port")
  95. key = f"{host}:{port}"
  96. tcp_winners[winner] += 1
  97. tcp_targets[key][winner] += 1
  98. if winner == "direct":
  99. tcp_direct += 1
  100. else:
  101. tcp_relay += 1
  102. continue
  103. udp_match = UDP_WIN_RE.search(line)
  104. if udp_match:
  105. udp_total += 1
  106. winner = normalize_winner(udp_match.group("winner"))
  107. host = udp_match.group("host")
  108. port = udp_match.group("port")
  109. key = f"{host}:{port}"
  110. udp_winners[winner] += 1
  111. udp_targets[key][winner] += 1
  112. if winner == "direct":
  113. udp_direct += 1
  114. else:
  115. udp_relay += 1
  116. tcp_ordered_targets = sorted(
  117. tcp_targets.items(),
  118. key=lambda item: sum(item[1].values()),
  119. reverse=True,
  120. )[: max(args.top, 0)]
  121. udp_ordered_targets = sorted(
  122. udp_targets.items(),
  123. key=lambda item: sum(item[1].values()),
  124. reverse=True,
  125. )[: max(args.top, 0)]
  126. result = {
  127. "log_file": str(log_path),
  128. "tcp": {
  129. "total": tcp_total,
  130. "direct": tcp_direct,
  131. "relay": tcp_relay,
  132. "winners": dict(sorted(tcp_winners.items(), key=lambda item: (-item[1], item[0]))),
  133. "targets": [
  134. {
  135. "target": target,
  136. "wins": dict(sorted(counts.items(), key=lambda item: (-item[1], item[0]))),
  137. }
  138. for target, counts in tcp_ordered_targets
  139. ],
  140. },
  141. "udp": {
  142. "total": udp_total,
  143. "direct": udp_direct,
  144. "relay": udp_relay,
  145. "winners": dict(sorted(udp_winners.items(), key=lambda item: (-item[1], item[0]))),
  146. "targets": [
  147. {
  148. "target": target,
  149. "wins": dict(sorted(counts.items(), key=lambda item: (-item[1], item[0]))),
  150. }
  151. for target, counts in udp_ordered_targets
  152. ],
  153. },
  154. }
  155. if args.json:
  156. print(json.dumps(result, ensure_ascii=False, indent=2))
  157. return 0
  158. print(f"log: {log_path}")
  159. for protocol in ("tcp", "udp"):
  160. section = result[protocol]
  161. print(f"{protocol}: total={section['total']} direct={section['direct']} relay={section['relay']}")
  162. print("winners:")
  163. for name, count in section["winners"].items():
  164. print(f" {name}: {count}")
  165. print("targets:")
  166. for item in section["targets"]:
  167. wins = ", ".join(f"{name}={count}" for name, count in item["wins"].items())
  168. print(f" {item['target']}: {wins}")
  169. return 0
  170. def main() -> int:
  171. parser = build_parser()
  172. args = parser.parse_args()
  173. return args.handler(args)