纠正一个传出去的错误结论:不是「运营商封了 UDP」

我在给部署脚本的指示里写过:

    实测这条线路上运营商对 UDP 有限制 ——
    12 个 Cloudflare edge IP:TCP 12/12 通、UDP 0/12 通

它照这个把注释写进了 relay/deploy.sh。

**但这个测量是无效的。** relay-stability 后来自己纠正了:
那个 UDP 探测包是手搓的简化包,格式不对,
Cloudflare 本来就不该回应它 —— 「0/12 无回应」什么都证明不了。

用 cloudflared --loglevel debug 跑临时实例,真相是:

    UDP Connectivity  region1/region2  PASS  QUIC connection successful
    SUMMARY: Environment is healthy. cloudflared will use 'quic'
    Registered tunnel connection        (握手 1 秒)

**UDP 完全通。**

真因是:QUIC 建连要 6+ 个包往返,而 TCP 的 SYN 只有 3 个包、
内核还会按 1s/2s/4s 激进重传。约 5.4%/包 丢包下:

    (1 - 0.054)^6 ≈ 72%     <- 和实测的 72% 对得上

所以 QUIC 的弱点在「连不上」,不在「连上之后不好」;
一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。

结论的**用途**没变(还是要两条协议一起跑),但留着错误的根因注释会:
  * 误导后人以为 UDP 被封 -> 可能做出「干脆别用 QUIC」的错误决定
  * 掩盖真正可优化的方向(QUIC 握手对丢包敏感是链路物理属性,
    只能靠 HTTP/2 兜底来绕,不是「运营商限制」)

改动:relay/deploy.sh 两处、_build/test_deploy_sh.sh 一处。

教训记一笔:**手搓协议包去探测可达性非常容易得出错误结论** ——
那个包格式不对,对方根本没义务回应。要么用官方工具的自检
(cloudflared 自己会打 UDP Connectivity PASS/FAIL),
要么别下结论。
This commit is contained in:
xmz7891
2026-10-07 18:22:19 +08:00
parent cc5891e6d0
commit e611b07fb1
8 changed files with 353 additions and 67 deletions
-57
View File
@@ -1,57 +0,0 @@
修局域网/中转对比基准:它自己有两个 bug,害产品背了黑锅
这个基准一直是红的(中转那条路报「连接阶段失败: timed out」),
而且报告里写着"这是中转链路的产品 bug"。**查下来不是产品的问题。**
== bug 1:两条路用了同一个配对码 ==
基准为了排除互相干扰,给两条路**各起一台全新被控端**(设计是对的),
但两台都用了写死的 code="246813"。
中转有「一个配对码只属于一台设备」的规则:
第 1 台(跑局域网)注册成功
-> stop_agent 杀掉进程,但**中转还以为它在线**(last_seen 没过期)
第 2 台(跑中转)用同一个码注册 -> **被拒**
中转日志:配对码 246813 已被 ... 占用,拒绝 ...
而 start_agent 只检查"有没有设备在线" —— 第 1 台僵尸还挂着,
于是**以为新的一台登记成功了**
-> 没有任何进程在回包 -> 超时
对照实验(新增 _build/verify_code_reuse.py):
场景 A 同一个码 -> **不通**
场景 B 各用不同码 -> 通
修法:两条路各用不同码;start_agent 的判据从"有没有在线设备"
改成"有没有**配对码正好是这一台要用的码**的在线设备"。
== bug 2:attach 用的还是旧码(我第一版补丁也漏了这处)==
relay_connect() 里 `client.attach(code)` 用的还是局域网那个码 ——
attach 到一台已经死掉、但中转以为在线的设备上,pair 自然没人回。
修成 `attach(relay_code)`。
== 顺带更新一段过时注释 ==
原文写着「当前版本经中转只稳定支撑前 1~2 次交互……这是中转链路的
产品 bug」,并据此把中转的轮数调小、超时调短。
那个 bug **已经修好了**(根因:一条 HTTP/1.1 连接被长轮询和发命令
两个线程同时用,撞状态机抛 CannotSendRequest("Idle"),
异常冒到 _client_loop 外面把 client-relay 线程打死)。
注释不更新会误导后人以为产品还有这个问题。
== 修好之后的实测(本地中转,30 次/路)==
局域网 握手 16ms 成功 30/30 (100%) 中位 0ms P90 0ms 最大 1ms
中转 握手 46ms 成功 30/30 (100%) 中位 15ms P90 29ms 最大 32ms
这是本地中转,量的是**协议本身的开销**(长轮询往返 vs 直连 TCP);
真实跨网络那条腿的 1.0~1.5 秒来自隧道和 4G,是另一回事。
结论:局域网确实是"毫秒级、不耗流量"的那条根本路径 ——
中位 0ms vs 15ms,而且不走任何外网。
踩坑记录:这两处都是"看起来像产品 bug、实际是测试自己的 bug"。
先怀疑测量工具,比先怀疑产品更省时间 ——
上一次(管理页缓存头)也是我拿 curl -I 测 GET 白查了半天。
+141
View File
@@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
r'''纠正一个我自己传出去的**错误结论**。
== 错在哪 ==
I(Lead) 在给部署脚本智能体的消息里写过:
「实测这条线路上运营商对 UDP 有限制 —— 对 12 个 Cloudflare edge IP
探测,TCP:7844 12/12 可达,UDP:7844 0/12 有回应」
它照这个说法把注释写进了 relay/deploy.sh。
**但这个测量是无效的。** relay-stability 后来自己纠正了:
那个 UDP 探测包是我用 Python 手搓的简化包,**格式不对,
Cloudflare 本来就不该回应它** —— "0/12 无回应"什么都证明不了。
用 `cloudflared --loglevel debug` 跑临时实例,真相是:
INF | UDP Connectivity region1.v2.argotunnel.com PASS QUIC connection successful
INF | UDP Connectivity region2.v2.argotunnel.com PASS QUIC connection successful
INF | SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol
INF Registered tunnel connection (握手 1 秒)
**UDP 完全通,握手成功时只要 1 秒。**
== 真因 ==
QUIC 握手需要 6+ 个包的往返;
TCP 的 SYN 只有 3 个包,而且内核会按 1s/2s/4s 激进重传。
在约 5.4%/包 的丢包下:
成功率 ≈ (1 - p)^6 = (1 - 0.054)^6 ≈ 72% <- 和实测的 72% 对得上
所以 QUIC 的弱点是「**连不上**」,不是「连上之后不好」;
一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。
== 为什么要改 ==
结论错了但**结论的用途是对的**(所以要两条协议一起跑),
可是留着一句错误的根因注释会:
* 误导后人以为 UDP 被封 -> 可能做出"干脆别用 QUIC"的错误决定
* 掩盖真正可优化的方向(QUIC 握手对丢包敏感,这是链路的物理属性,
只能靠 HTTP/2 兜底来绕,不是"运营商限制")
'''
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
NEW_REASON = """为什么两条一起跑 —— QUIC 握手对丢包敏感:
它要 6+ 个包往返才建得起来,而 TCP 的 SYN 只有
3 个包、内核还会按 1s/2s/4s 激进重传。
在约 5% 丢包下 (1-p)^6 只有 ~72% 能建成 ——
所以刻意让 HTTP/2 占几条连接兜住那 28%。
(UDP 本身是通的:cloudflared 自检 PASS,
握手成功时只要 1 秒。不是"运营商封了 UDP"。)"""
NEW_BLOCK = """# 根因也查清了:**QUIC 握手对丢包敏感**。
#
# QUIC 注册 525 失败 197 掉线 48 拨号成功率 72%
# HTTP/2 注册 13 失败 0 掉线 7 拨号成功率 100%
#
# QUIC 建连要 6+ 个包的往返,TCP 的 SYN 只有 3 个包、内核还会
# 按 1s/2s/4s 激进重传。约 5.4%/包的丢包下:
#
# (1 - 0.054)^6 ≈ 72% <- 和实测的 72% 对得上
#
# 注意:**这不是"运营商封了 UDP"**。曾经这么以为过,但那个判断
# 来自一个格式不对的手搓 UDP 探测包(Cloudflare 本来就不该回应它)。
# 用 cloudflared --loglevel debug 实测:
#
# INF | UDP Connectivity region1.v2.argotunnel.com PASS
# INF | UDP Connectivity region2.v2.argotunnel.com PASS
# INF | SUMMARY: Environment is healthy ...
# INF Registered tunnel connection (握手 1 秒)
#
# UDP 是通的。QUIC 的弱点在"连不上",不在"连上之后不好"——
# 一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。
# 所以对策是「多 QUIC + 少量 HTTP/2 兜底」,而不是放弃 QUIC。"""
def patch(path: Path, old: str, new: str, label: str) -> bool:
if not path.is_file():
print(" [跳过] %s 不存在" % path.name)
return True
text = path.read_text(encoding="utf-8")
if old not in text:
if "握手对丢包敏感" in text:
print(" • %s 已经是新说法" % label)
return True
print(" ✗ %s 没找到锚点" % label)
return False
if text.count(old) != 1:
print(" ! %s 锚点出现 %d 次" % (label, text.count(old)))
return False
path.write_text(text.replace(old, new, 1), encoding="utf-8", newline="\n")
print(" ✓ %s" % label)
return True
def main() -> int:
ok = True
ok &= patch(
ROOT / "relay" / "deploy.sh",
"""为什么两条一起跑 —— 运营商对 UDP 有限制
(12 个 edge IP:TCP 12/12 通、UDP 0/12 通),""",
NEW_REASON, "deploy.sh --help 里的说法")
ok &= patch(
ROOT / "relay" / "deploy.sh",
"""# 根因也查清了:**运营商对 UDP 有限制**。
#
# 对 12 个 Cloudflare edge IP 探测:
# TCP:7844 12/12 可达(169~252ms)
# UDP:7844 0/12 有回应
#
# 也就是 UDP 被链路限制,QUIC 先天不利,所以让它当主力、
# HTTP/2 兜底。""",
NEW_BLOCK, "deploy.sh 的根因注释")
ok &= patch(
ROOT / "_build" / "test_deploy_sh.sh",
"""# 运营商限 UDP(12 个 edge IP:TCP 12/12 通、UDP 0/12 通)""",
"""# QUIC 握手要 6+ 个包往返,TCP SYN 只要 3 个且激进重传,
# 约 5% 丢包下 (1-p)^6 ≈ 72%(不是"运营商封 UDP",那是错的测量)""",
"test_deploy_sh.sh 的说明")
print()
if not ok:
print(" 有锚点没匹配")
return 1
print(" 纠正完成")
return 0
if __name__ == "__main__":
sys.exit(main())
+20
View File
@@ -0,0 +1,20 @@
[PASS] 基准脚本存在
---- 一、两条路用不同的配对码 ----
[PASS] 定义了局域网用的 code
[PASS] 定义了中转用的 relay_code
[PASS] 两个码不一样(否则第 2 台会被一码一机规则拒掉)
---- 二、attach 要用 relay_code ----
[PASS] 能定位到 relay_connect
[PASS] relay_connect 里用的是 relay_code
[PASS] relay_connect 里没有裸 attach(code)
---- 三、start_agent 的判据要认码 ----
[PASS] start_agent 接受码参数
[PASS] 没有「只看有没有在线设备」的松判据
[PASS] 判据里比对了配对码
---- 四、真跑一遍短基准(两条路都要通)----
[PASS] 基准跑完并输出了 BENCH_RESULT
[PASS] 局域网那条路通了
[PASS] 中转那条路通了
========================================================
通过 13 项,失败 0 项
+21
View File
@@ -152,3 +152,24 @@ time code ms
2026-10-07 18:14:59 200 1089
2026-10-07 18:15:15 200 987
2026-10-07 18:15:31 200 999
2026-10-07 18:15:48 200 1426
2026-10-07 18:16:04 200 1222
2026-10-07 18:16:20 200 1397
2026-10-07 18:16:37 200 1127
2026-10-07 18:16:53 200 1010
2026-10-07 18:17:09 200 1140
2026-10-07 18:17:25 200 1032
2026-10-07 18:17:41 200 1367
2026-10-07 18:17:57 200 1111
2026-10-07 18:18:13 200 1285
2026-10-07 18:18:30 200 1169
2026-10-07 18:18:46 200 1380
2026-10-07 18:19:02 200 1017
2026-10-07 18:19:18 200 1121
2026-10-07 18:19:34 200 1184
2026-10-07 18:19:51 200 1386
2026-10-07 18:20:07 200 1351
2026-10-07 18:20:23 200 1381
2026-10-07 18:20:40 200 1432
2026-10-07 18:20:56 200 1468
2026-10-07 18:21:13 200 1462
1 time code ms
152 2026-10-07 18:14:59 200 1089
153 2026-10-07 18:15:15 200 987
154 2026-10-07 18:15:31 200 999
155 2026-10-07 18:15:48 200 1426
156 2026-10-07 18:16:04 200 1222
157 2026-10-07 18:16:20 200 1397
158 2026-10-07 18:16:37 200 1127
159 2026-10-07 18:16:53 200 1010
160 2026-10-07 18:17:09 200 1140
161 2026-10-07 18:17:25 200 1032
162 2026-10-07 18:17:41 200 1367
163 2026-10-07 18:17:57 200 1111
164 2026-10-07 18:18:13 200 1285
165 2026-10-07 18:18:30 200 1169
166 2026-10-07 18:18:46 200 1380
167 2026-10-07 18:19:02 200 1017
168 2026-10-07 18:19:18 200 1121
169 2026-10-07 18:19:34 200 1184
170 2026-10-07 18:19:51 200 1386
171 2026-10-07 18:20:07 200 1351
172 2026-10-07 18:20:23 200 1381
173 2026-10-07 18:20:40 200 1432
174 2026-10-07 18:20:56 200 1468
175 2026-10-07 18:21:13 200 1462
+2 -1
View File
@@ -519,7 +519,8 @@ echo "── 7c. 默认:两个隧道单元都要启用(协议级故障隔离
stub_ok "7c"
# 真机实测(40 次/组):quic 8 + h2 2 拿到 100% 可用率 + 最低 P90/P99。
# 单独跑一个协议都不如两个一起跑:
# 运营商限 UDP(12 个 edge IP:TCP 12/12 通、UDP 0/12 通)
# QUIC 握手要 6+ 个包往返,TCP SYN 只要 3 个且激进重传,
# 约 5% 丢包下 (1-p)^6 ≈ 72%(不是"运营商封 UDP",那是错的测量)
# -> quic 拨号成功率只有约 72%,需要 http2 兜住失败的那部分。
: > "$STUB_LOG"
printf 'lanremote-relay\n' > "$STUB_LOG.state"