瀏覽代碼

更新并验证relay子节点可用性

Gogs 2 周之前
父節點
當前提交
5f691597ff
共有 6 個文件被更改,包括 132 次插入3 次删除
  1. 68 0
      .codex/AGENTS.md
  2. 3 1
      README.md
  3. 49 0
      cli.py
  4. 1 0
      config.json
  5. 2 0
      config.py
  6. 9 2
      socks_edge.py

+ 68 - 0
.codex/AGENTS.md

@@ -0,0 +1,68 @@
+# mynetspeeder context guide
+
+## 结论先行
+
+- 这是一个以 `config.json` + `config.py` 为中心的网络加速项目。
+- 日常修改时,优先读 `config.py`、相关入口文件、对应脚本;不要默认通读 `README.md`。
+- 当前推荐运行模式是:`TCP 透明接管 + UDP SOCKS`。
+- 当 `config.json` 中 `socks_port > 0` 时,默认语义应理解为:`UDP 不走透明接管,优先走 SOCKS5 UDP ASSOCIATE`。
+
+## 最小上下文读取顺序
+
+按任务类型只加载必要文件,避免把大段说明一次性塞进上下文。
+
+### 配置相关
+
+1. `config.py`
+2. `config.json`
+3. `README.md` 中“配置文件 / 启动方式”相关小节
+
+### TCP/UDP 转发相关
+
+1. `transparent_edge.py`
+2. `socks_edge.py`
+3. `relay_client.py`
+4. `relay_server.py`
+5. 必要时再看 `protocol.py`
+
+### 启动脚本相关
+
+1. `scripts/start-transparent.sh`
+2. `scripts/stop-transparent.sh`
+3. `scripts/start-relay.sh`
+
+### CLI/命令入口相关
+
+1. `cli.py`
+2. `__main__.py`
+3. `scheduler.py`
+
+## 上下文压缩规则
+
+- 先摘要后展开:先记录“目标、影响文件、风险点”,再决定是否读取大文件。
+- `README.md` 只按标题定点读取,不要一次性全读。
+- 日志、抓包、长报错默认只保留:报错类型、关键堆栈、端口、协议、目标地址、触发命令。
+- 对配置讨论,优先引用字段名,不复述整份 JSON。
+- 对网络路径讨论,固定区分三类:`direct`、`relay`、`socks`,避免语义混杂。
+
+## 上下文退化防护
+
+- 若同时出现“UDP 透明”和 `socks_port > 0`,以脚本实际行为为准,先判定为 `UDP SOCKS`。
+- 若文档描述与代码冲突,以 `config.py` 和脚本实现为高优先级事实源。
+- 若任务只改局部模块,不要把其他模式说明带入当前判断。
+- 若已经发现错误假设,不在原上下文上叠加修正,直接丢弃旧假设并重述当前结论。
+
+## 稳定事实
+
+- 配置加载入口:`config.py`
+- 默认 SOCKS 监听:`127.0.0.1`
+- `socks_port = 0` 表示关闭 SOCKS UDP 入口
+- `udp_always_broadcast` 默认开启
+- `udp_direct_redundancy` / `direct_redundancy` 是 direct 路径冗余主开关
+
+## 修改前检查清单
+
+- 这是配置问题、转发问题、脚本问题,还是文档问题?
+- 这次任务是否真的需要读取 `README.md` 全文?
+- 是否把“透明 UDP”和“UDP SOCKS”混成了同一种路径?
+- 是否只需最小改动就能完成任务?

+ 3 - 1
README.md

@@ -88,6 +88,7 @@ flowchart LR
   "probe_interval": 15,
   "udp_redundancy": 1,
   "udp_direct_redundancy": 2,
+  "udp_disable_direct": false,
   "udp_always_broadcast": true,
   "udp_copy_interval_ms": 8,
   "socks_host": "127.0.0.1",
@@ -170,6 +171,7 @@ flowchart LR
 - `UDP/QUIC` 更适合显式 SOCKS:应用主动把目标地址和 UDP 数据交给 `socks`,路径更直接,状态更清晰,避免透明重定向场景里对 `SO_ORIGINAL_DST`、conntrack、自环规避、NAT 回包等额外依赖
 - `UDP` 对时延抖动和状态保持更敏感,显式入口通常比透明重定向更稳定,尤其在 QUIC、游戏、实时音视频这类长流场景
 - 当前项目里 `socks` UDP 并不是“单路径代理”,它内部同样会做 `direct UDP + relay UDP` 并行竞速,所以不会损失 UDP 加速能力
+- 若要只测试 relay UDP,可在 `config.json` 中设置 `"udp_disable_direct": true`;此时 `socks` 汇总日志会显示 `direct_paths=0`,`candidates` 中只保留 relay 节点
 - 透明 UDP 的主要优势是“应用零改动”,不是“性能一定更高”;在可控环境下,它更多是兼容方案而不是首选方案
 
 建议按场景选择:
@@ -230,7 +232,7 @@ python3 -m mynetspeeder probe --config /home/mynetspeeder/config.json --once
 汇总日志胜率:
 
 ```bash
-python3 -m mynetspeeder summary --log-file /var/log/mynetspeeder-edge.log
+python3 -m mynetspeeder summary --log-file /var/log/mynetspeeder-edge.log --socks-log-file /var/log/mynetspeeder-socks.log
 ```
 
 ## 安装子节点

+ 49 - 0
cli.py

@@ -21,6 +21,9 @@ TCP_WIN_RE = re.compile(
 UDP_WIN_RE = re.compile(
     r"udp flow=(?P<flow>\d+) winner=(?P<winner>\S+) target=(?P<host>[^:]+):(?P<port>\d+)"
 )
+SOCKS_UDP_SUMMARY_RE = re.compile(
+    r"udp summary .*?winner_detail=(?P<winner_detail>.*?) (?:target_detail=(?P<target_detail>.*?) )?packets_sent="
+)
 
 
 def normalize_winner(name: str) -> str:
@@ -59,6 +62,7 @@ def build_parser() -> argparse.ArgumentParser:
 
     summary = sub.add_parser("summary", help="汇总透明模式日志里的胜率")
     summary.add_argument("--log-file", default="/var/log/mynetspeeder-edge.log")
+    summary.add_argument("--socks-log-file", default="/var/log/mynetspeeder-socks.log")
     summary.add_argument("--top", type=int, default=10)
     summary.add_argument("--json", action="store_true")
     summary.set_defaults(handler=handle_summary)
@@ -98,6 +102,7 @@ def handle_summary(args: argparse.Namespace) -> int:
     log_path = Path(args.log_file)
     if not log_path.exists():
         raise SystemExit(f"log file not found: {log_path}")
+    socks_log_path = Path(args.socks_log_file)
 
     tcp_total = 0
     tcp_direct = 0
@@ -141,6 +146,48 @@ def handle_summary(args: argparse.Namespace) -> int:
             else:
                 udp_relay += 1
 
+    socks_udp_wins: dict[int, tuple[str, str | None]] = {}
+    if socks_log_path.exists():
+        for line in socks_log_path.read_text(errors="replace").splitlines():
+            summary_match = SOCKS_UDP_SUMMARY_RE.search(line)
+            if not summary_match:
+                continue
+            winners_raw = summary_match.group("winner_detail").strip()
+            targets_raw = (summary_match.group("target_detail") or "").strip()
+            if winners_raw == "none":
+                continue
+            target_map: dict[int, str] = {}
+            if targets_raw and targets_raw != "none":
+                for item in targets_raw.split(", "):
+                    parts = item.split(":")
+                    if len(parts) < 3:
+                        continue
+                    try:
+                        flow_id = int(parts[0])
+                    except ValueError:
+                        continue
+                    target_map[flow_id] = f"{':'.join(parts[1:-1])}:{parts[-1]}"
+            for item in winners_raw.split(", "):
+                flow_parts = item.split(":", 1)
+                if len(flow_parts) != 2:
+                    continue
+                try:
+                    flow_id = int(flow_parts[0])
+                except ValueError:
+                    continue
+                winner = normalize_winner(flow_parts[1])
+                socks_udp_wins[flow_id] = (winner, target_map.get(flow_id))
+
+    for winner, target in socks_udp_wins.values():
+        udp_total += 1
+        udp_winners[winner] += 1
+        if winner == "direct":
+            udp_direct += 1
+        else:
+            udp_relay += 1
+        if target:
+            udp_targets[target][winner] += 1
+
     tcp_ordered_targets = sorted(
         tcp_targets.items(),
         key=lambda item: sum(item[1].values()),
@@ -187,6 +234,8 @@ def handle_summary(args: argparse.Namespace) -> int:
         return 0
 
     print(f"log: {log_path}")
+    if socks_log_path.exists():
+        print(f"socks_log: {socks_log_path}")
     for protocol in ("tcp", "udp"):
         section = result[protocol]
         print(f"{protocol}: total={section['total']} direct={section['direct']} relay={section['relay']}")

+ 1 - 0
config.json

@@ -15,6 +15,7 @@
   "udp_direct_redundancy": 2,
   "udp_direct_redundancy_v4": 2,
   "udp_direct_redundancy_v6": 2,
+  "udp_disable_direct": false,
   "udp_always_broadcast": true,
   "udp_copy_interval_ms": 8,
   "socks_host": "127.0.0.1",

+ 2 - 0
config.py

@@ -45,6 +45,7 @@ class Config:
     udp_direct_redundancy: int = 2
     udp_direct_redundancy_v4: int | None = None
     udp_direct_redundancy_v6: int | None = None
+    udp_disable_direct: bool = False
     udp_always_broadcast: bool = True
     udp_copy_interval_ms: int = 8
     socks_host: str = "127.0.0.1"
@@ -80,6 +81,7 @@ class Config:
             udp_direct_redundancy=max(1, raw.get("udp_direct_redundancy", 2)),
             udp_direct_redundancy_v4=raw.get("udp_direct_redundancy_v4"),
             udp_direct_redundancy_v6=raw.get("udp_direct_redundancy_v6"),
+            udp_disable_direct=raw.get("udp_disable_direct", False),
             udp_always_broadcast=raw.get("udp_always_broadcast", True),
             udp_copy_interval_ms=max(0, raw.get("udp_copy_interval_ms", 8)),
             socks_host=raw.get("socks_host", "127.0.0.1"),

+ 9 - 2
socks_edge.py

@@ -390,6 +390,9 @@ class UdpAssociateServer(asyncio.DatagramProtocol):
         direct_paths = sum(len(flow.direct_sockets) for flow in self.client_flows.values())
         relay_candidates = sum(len(flow.link_streams) for flow in self.client_flows.values())
         winner_detail = ", ".join(f"{flow.flow_id}:{flow.winner_name}" for flow in self.client_flows.values() if flow.winner_name) or "none"
+        target_detail = ", ".join(
+            f"{flow.flow_id}:{flow.target_host}:{flow.target_port}" for flow in self.client_flows.values()
+        ) or "none"
         candidate_detail_parts: list[str] = []
         relay_errors: list[str] = []
         for flow in self.client_flows.values():
@@ -416,12 +419,12 @@ class UdpAssociateServer(asyncio.DatagramProtocol):
         if self.client_addr:
             print(
                 f"[edge] udp summary bind={self.client_addr[0]}:{self.client_addr[1]} active_flows={active_flows} "
-                f"winner_flows={winners} winner_detail={winner_detail} packets_sent={packets_sent} packets_received={packets_received} dup={duplicates} "
+                f"winner_flows={winners} winner_detail={winner_detail} target_detail={target_detail} packets_sent={packets_sent} packets_received={packets_received} dup={duplicates} "
                 f"direct_paths={direct_paths} relay_paths={relay_candidates} candidates={candidate_detail} relay_errors={relay_error_detail}"
             )
         else:
             print(
-                f"[edge] udp summary bind=unbound active_flows={active_flows} winner_flows={winners} winner_detail={winner_detail} packets_sent={packets_sent} packets_received={packets_received} dup={duplicates} "
+                f"[edge] udp summary bind=unbound active_flows={active_flows} winner_flows={winners} winner_detail={winner_detail} target_detail={target_detail} packets_sent={packets_sent} packets_received={packets_received} dup={duplicates} "
                 f"direct_paths={direct_paths} relay_paths={relay_candidates} candidates={candidate_detail} relay_errors={relay_error_detail}"
             )
 
@@ -529,6 +532,8 @@ class SocksEdge:
         return ordered
 
     def _udp_direct_redundancy_for_target(self, target_host: str) -> int:
+        if self.config.udp_disable_direct:
+            return 0
         base = self.config.udp_direct_redundancy
         if ":" in target_host and self.config.udp_direct_redundancy_v6 is not None:
             base = self.config.udp_direct_redundancy_v6
@@ -538,6 +543,8 @@ class SocksEdge:
 
     async def _ensure_udp_direct_paths(self, flow: UdpFlowState, udp_server: UdpAssociateServer) -> None:
         target_count = self._udp_direct_redundancy_for_target(flow.target_host)
+        if target_count <= 0:
+            return
         for index in range(target_count):
             name = f"direct-{index + 1}" if target_count > 1 else "direct"
             if name in flow.direct_sockets or name in flow.direct_failures: