cli.py 11 KB

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