fix(uot-relay): 数据报级错误不再杀隧道 + 地址族健壮性

实测复现:目标为关闭的 UDP 端口时,连接式 socket 收到 ICMP port unreachable
→ ConnectionRefusedError → 被兜底 except 接住并 stop.set() → 整条 UoT 隧道被关
(客户端见 'UoT TCP 流已关闭',多隧道同秒集体死亡)。

修复:
- 一律用未连接 UDP socket(不接收 ICMP 错误)
- send/recv 的数据报级错误只丢包继续;EAGAIN 等可写重试(1s 上限)
- 按地址族惰性建 socket(v4/v6 混用)、IPv6 recvfrom 4 元组归一化
- connect 模式预建 socket(修掉主循环空池即关连接的回归)

五节点已部署并验证:狂发 200 包到关闭端口/无路由目标后隧道仍存活;
UoT DNS 端到端 JP/US/AQ/HK2 通过,RU 为该节点出口 UDP 限制(实测 4/10)
导致的偶发超时,非 relay 问题。
This commit is contained in:
RRRosmontis
2026-09-19 16:41:19 +08:00
parent 41204df78a
commit bdd9d3498d
2 changed files with 133 additions and 25 deletions
+15
View File
@@ -328,6 +328,21 @@ magic 地址 → `127.0.0.1:15201`,其余 CONNECT 走 `$connect_addr` 默认
4. 模块**拒绝 CONNECT 端口 0**(`port < 1` 返回 400)→ 客户端连 magic 地址时
端口必须非 0(用 443 即可,relay 不读 CONNECT 端口,目标取自 UoT 帧头)。
**健壮性要求(2026-09 实测修复,别踩)**:
- relay 必须用**未连接**的 UDP socket(`sendto`/`recvfrom`)。用连接式 socket 时,
目标侧 ICMP(port unreachable 等)会变成 `ConnectionRefusedError` 在 send/recv 上抛出,
若被兜底 `except` 接住并 `stop.set()`,**一条数据报出错就杀掉整条 UoT 隧道**
(客户端表现为 "UoT TCP 流已关闭"、多条隧道同秒集体死亡)。
- 数据报级错误(`BlockingIOError`/EAGAIN、`ENETUNREACH`、`EHOSTUNREACH`、`EMSGSIZE`)
**只丢当前包**,EAGAIN 先等可写再重试;只有 TCP EOF、协议错误、空闲超时才关隧道。
- 按地址族惰性建 socket,non-connect 模式可混用 IPv4/IPv6 目标;IPv6 `recvfrom`
返回 4 元组需归一化。
- connect 模式要**预建** socket(否则主循环首轮无可 select 对象,会立刻关连接)。
**节点出口差异(实测)**:RU 节点到境外 DNS(8.8.8.8:53)UDP 成功率仅约 4/10
(俄 ISP 限制),表现为 UoT DNS 偶发超时——属该节点出口特性,非 relay 故障。
此类节点的 UDP 业务建议换用当地可达目标,或走其它节点。
**客户端协议**:认证与 TCP CONNECT 相同(TLS + Basic Auth),然后
`CONNECT sp.v2.udp-over-tcp.arpa:443 HTTP/1.1`,200 之后收发 UoT v2 帧:
请求头 `| isConnect u8 | ATYP u8 | address | port u16be |`,