VPN 网关 阶段 7:Agent ICMP echo 与能力协商
VPN 网关 阶段 7:Agent ICMP echo 与能力协商
标题
feat(agent): serve icmp echo streams behind a negotiated capability
本记录覆盖分支上阶段 7 的全部提交(计划 → 协议契约 → echo 引擎 → 锁修正 → 流适配与 ack 门禁 → Agent 配置 → 能力通告 → CLI 装配 → 选择器修复 → 签发门禁 → 文档与前端文案),而不只是最后一个提交。
目标分支
main
本分支 stacked 在 codex/vpn-phase5-admin-console 之上。阶段 4 与阶段 5 的 PR 合并进 main 后,
必须先 git rebase --onto main codex/vpn-phase5-admin-console 再更新本 PR,
否则 diff 会重复包含阶段 5 的产物(控制台 VPN 页与节点页 VPN 区块)。
关联记录
- 实施计划:VPN 网关 阶段 7:Agent ICMP echo 与能力协商 Implementation Plan
- 设计规格:内嵌 VPN 网关(WireGuard)设计(§6.1 能力协商、§6.2 ICMP echo、§12.1 稳定码、§14 测试策略、§15 阶段 7、§18 验收 8 与 9)
- 决策载体:ADR 0002: Open a public UDP ingress for an embedded VPN gateway(ICMP echo 已批准;P2P NAT traversal 与任意远程命令执行仍禁止)
- 前置阶段:阶段 5:管理台 VPN 页、阶段 4:纯逻辑包与管理 API(
refuseICMP一律拒绝) - 协议文档:代理协议模块 · ICMP echo 与
stream_icmp_echo.v1 - 配置文档:配置参考 · Stream 延迟与授权缓存、配置示例
- 用户文档:Agent 使用指南、管理员指南 · VPN 网关 peer 管理
摘要
阶段 7 让 Agent 具备“替 Server 发一个 ICMP echo 并把应答带回来”的能力,并让管理面据此做真实判定
而不是阶段 4 的一律拒绝。本阶段结束时 ping 仍然不通:没有任何进程发起 icmp-echo 流,
Server 数据面(WireGuard 端点、内存态 TUN、逐包策略链)属阶段 6。本阶段交付的是从线路契约到签发门禁的
一整条能力链,以及顺带修掉的一个既有选路缺陷。
七类产物:
- 协议契约(
internal/protocol/icmp.go134 行 +icmp_test.go150 行):StreamProtocolICMPEcho = "icmp-echo"与CapabilityStreamICMPEcho = "stream_icmp_echo.v1",ICMPEchoRequest/ICMPEchoReply及其编解码(受MaxDatagram约束、空 correlation 拒绝、 未知字段容忍),以及闭合状态枚举ok/timeout/capacity_exhausted/unreachable/unsupported/cancelled。 契约只有一处定义,Agent 在阶段 7 解码、Server 在阶段 6 编码。 - Agent echo 引擎(
internal/agent/icmp_echo.go425 行 +icmp_echo_test.go550 行): 一个非特权 ping socket 服务所有在途 echo。内核会改写 ICMP id,因此 id 不能用于关联; 引擎为每个在途 echo 分配唯一的 wire sequence,并在应答到达时逐字节比对 payload, 防止 16 位序号回绕后撞上旧关联。回给 Server 的应答携带 peer 自己的 identifier/sequence。 超时、超预算、目标不可达是应答而不是错误,因为 IP 没有错误通道。OpenEchoer把权限失败映射成ErrPingGroupRangeRequired,消息里写明需要设置的 sysctl。 - 流适配与 ack 门禁(
internal/agent/dialer.go+165 行、session.go、connection_pool.go、icmp_stream_test.go327 行):一条icmp-echo流沿用 UDP 的数据报语义,一次 Write 一个请求、 一次 Read 一个应答、之后 EOF。能力门禁默认关闭,只由 Server 的 ack 打开;target_port: 0只对icmp-echo放宽。 - 配置与装配(
internal/config/config.go+30、internal/cli/root.go+45、agent_runtime_test.go+254):agent.streams.icmp_enabled(默认false)、icmp_bind_address、icmp_timeout、icmp_max_concurrent四个键;AgentStreamCapabilities(streams, icmpReady)只在 “配置开启且引擎就绪”时通告;socket 打不开时记 Error 到 stderr、不通告能力、Agent 不退出。 - 既有缺陷修复(
internal/server/connection_selector.go):Select把流协议名当能力名比较, 导致任何通告了能力的会话都不被选中,本地选路落空后走远端路径并报relay: node disconnected。 这影响的是现网的 tp-* 与 WebSSH 选路,不只是本阶段新增的 ICMP 出口。 - 签发门禁(
internal/server/vpn_icmp_gate.go111 行 +vpn_icmp_gate_test.go143 行、vpn_peer_service.go+92):refuseICMP换成authorizeICMP,按Supported/Unsupported/Unverified三态判定;审计详情新增icmpCapability: verified|unverified。 - 文档与前端文案:协议文档新增 ICMP echo 一节;配置文档新增 4 个键、
ping_group_range一次性设置; Agent 指南新增能力条目、主机要求与“能签发但 ping 不通”的排查顺序;管理员指南写清集群限制与unverified语义;OpenAPI 只改描述文字;前端vpn.form.icmpEnabledHelp与vpn.errors.agentCapabilityMissing不再宣称“本版一律拒绝”。
零改动项(与计划一致):go.mod/go.sum(golang.org/x/net 已是主模块直接依赖)、
migrations/、SchemaVersion(保持 15)、OpenAPI 路径集合与 10 个稳定错误码集合、
web/package.json/web/package-lock.json。
用户影响
- 默认无变化:
agent.streams.icmp_enabled默认false,升级二进制不会在任何主机上打开 ping socket, 也不会通告新能力;Server 侧因此仍然拒签 ICMP peer,与阶段 4/5 行为一致。 - 选路缺陷修复对所有用户生效:
Select不再误杀通告了能力的会话。修复前,只要 Agent 通告了任何能力 (现网 Agent 恒通告stream_open_result.v1),本地选路就一定落空并回落到远端 relay 路径; 单节点部署因此报relay: node disconnected。这是 tp-*、WebSSH 与后续 VPN 出口共用的代码路径。 - 开启 ICMP 需要两步:主机一次性
sysctl -w net.ipv4.ping_group_range='0 2147483647', 以及agent.streams.icmp_enabled: true。缺任一步时 Agent 在 stderr 报一条 Error、不通告能力, 但继续服务既有隧道;Server 侧签发返回 409vpn_agent_capability_missing。 - 控制台文案更正:原先告诉用户“本版服务端会拒绝”,现在说明真正的两个前提(Agent 在线且已协商能力、 节点开关打开),并把节点开关列为 409 的一个原因。
- 仍然 ping 不通:数据面在阶段 6。对外通知不得把本阶段描述成“VPN 可用”或“支持 ping”。
API、Schema 与配置影响
API
无新增路径、无新增稳定错误码。docs/api/openapi.yaml 只改了两处描述文字:
| 位置 | 变更 |
|---|---|
VPNPeerCreateRequest.icmpEnabled |
由“本版一律拒绝”改为“需要节点开关 + 出口 Agent 已协商 stream_icmp_echo.v1;集群中无法核实时仍签发并在审计记 icmpCapability: unverified” |
VPNPeerPatchRequest.icmpEnabled |
由“本版一律拒绝”改为“置 true 会按出口 Agent 能力重新校验” |
internal/server/vpn_peer_openapi_test.go 对 10 个稳定码与路径集合的双向相等守卫继续绿灯。
Schema
无变更。migrations/ 与 SchemaVersion(15)零改动,双方言迁移测与升级文档因此不适用。
审计的 icmpCapability 是 audit_logs.details JSON blob 里的一个新键,不是列,
旧代码不写、审计消费方(控制台 JSON 详情)对未知键容忍,无需迁移。
配置
新增 4 个 Agent 键(默认值登记在 internal/config/config.go 的 defaults map,与 server.vpn.icmp_* 同名同默认):
| 键 | 默认值 | 校验 |
|---|---|---|
agent.streams.icmp_enabled |
false |
— |
agent.streams.icmp_bind_address |
0.0.0.0 |
仅 icmp_enabled=true 时校验,必须是 IP(ping socket 绑地址而非 host:port) |
agent.streams.icmp_timeout |
5s |
仅 icmp_enabled=true 时校验,必须为正 |
agent.streams.icmp_max_concurrent |
64 |
仅 icmp_enabled=true 时校验,必须为正 |
校验只在开启时执行,与 server.vpn.icmp_* 的既有范式一致:关掉 ICMP 不会因为遗留的占位取值而无法启动,
而一个未开启的引擎永远不会被打开,因此“能启动即能用”成立。环境变量等价:TUNNELMESH_AGENT_STREAMS_ICMP_*。
没有新增命令行 flag:这四个键沿用既有的 --agent.streams.* 通用键路径,与 max_concurrent_dials 等一致。
协议
新增一个流协议值与一个能力,均遵循“未知即安全拒绝”:
OPEN_STREAM.protocol = "icmp-echo":旧 Agent 走dialStreamPayload的 default 分支返回agent: unsupported stream protocol,不崩溃;旧 Server 不会发它。stream_icmp_echo.v1:进入NegotiateCapabilities的交集,Agent 通告、Server ack、Agent 校验 ack。 ack 未包含该能力时 Agent 拒绝服务icmp-echo开流,因此“通告了但对端不认”不会留下半开状态。
安全与授权影响
- 能力门禁 fail-closed:
StreamDispatcher.icmpEcho默认false,只由FrameAgentMetadataAck打开。 一个从未收到 ack 的 Agent 拒绝 echo 开流,返回unsupported_capability而不是尝试执行。 - 目标地址双重校验:
routing.IsDangerousAddress(与代理模块共用的 SSRF 规则单一来源) 加引擎自身的validateEchoTarget(地址族、未指定、组播、链路本地)。169.254.0.0/16(云 metadata) 在 Agent 侧被拒,因此一个被攻陷的控制面不能把 Agent 当成 metadata oracle。 目标是字面 IP;主机名被拒而不是被解析,因为 Server 在开流前已解析, Agent 自行解析会回答一个与请求不同的问题。私网与回环目标在 Agent 侧允许: peer 能否到达它们由 Server 的逐包策略决定(阶段 6)。 - PolicyHook 保持生效:echo 开流以
("icmp", host, 0)走既有 policy,端口固定为 0。 传 wire 上的端口值会让一个端口白名单去裁决一个只关于地址的问题。 - 进程级预算:
icmp_max_concurrent在 Agent 内做全局信号量,超限立即回capacity_exhausted而不排队。Server 看不到一个 Agent 还在替哪些 peer 服务,所以这是防止一批 VPN peer 把出口 Agent 变成反射器的最后一道防线;排队会让 Server 的 per-peer 限额变成一句空话。 - 只放开 echo:不实现 ICMPv6、不实现 traceroute、不放开其它 ICMP 类型。 引擎的读循环对非 echo-reply 数据报直接丢弃。P2P NAT traversal 与任意远程命令执行仍按 ADR 0002 禁止。
- 签发门禁不泄露 Agent 归属:
Update路径的探测移到authorizeAgent之后。 先探测会回答一个关于调用方可能无权使用的 Agent 的问题,而“409 缺能力”与“Agent 不可用” 之间的差异本身就是信息。 - 无敏感数据落地:审计只记 peer ID、agent ID、node ID 与
icmpCapability; ICMP payload、私钥、Token、DSN 不进日志、审计与指标。 - 权限失败明确报错:
ErrPingGroupRangeRequired消息里带 sysctl 名与修复命令, 不静默降级成“能力没通告但没人知道为什么”。
测试证据
红灯(每个任务实测,非推断)
| 任务 | 红灯原文首行 |
|---|---|
| T1 协议 | internal/protocol/icmp_test.go:15:14: undefined: protocol.CapabilityStreamICMPEcho |
| T2 引擎 | internal/agent/icmp_echo_test.go:129:22: undefined: ICMPPacketConn |
| 锁修正 | WARNING: DATA RACE / Write at 0x00c000308190 by goroutine 154: internal/agent.(*Echoer).failAll() icmp_echo.go:360 / --- FAIL: TestEchoerReadsItsFailureCauseUnderTheLock (0.04s) |
| T3 流适配 | internal/agent/icmp_stream_test.go:101:19: unknown field ICMPEcho in struct literal of type Dialer |
| T5 配置 | internal/config/config_test.go:218:13: streams.ICMPEnabled undefined (type config.AgentStreamConfig has no field or method ICMPEnabled) |
| T4 通告 | internal/agent/dial_executor_test.go:212:63: too many arguments in call to AgentStreamCapabilities |
| T6 装配 | internal/cli/agent_runtime_test.go:207:108: too many arguments in call to NewAgentDispatcherFactory |
| T7 选择器 | internal/server/connection_selector_test.go:105:14: undefined: streamProtocolCapability |
| T8 门禁 | internal/server/vpn_icmp_gate_test.go:35:17: undefined: probeAgentICMPEcho |
T9 是文档与前端文案,不产生新行为,因此没有红灯;其守卫是既有的 scripts/doc_claims_test.go
(文档主张)、internal/server/vpn_peer_openapi_test.go(稳定码与路径集合)与前端 i18n 键对齐测试。
新增/修改的测试
| 文件 | 测试函数 | 覆盖 |
|---|---|---|
internal/protocol/icmp_test.go |
6 | 请求/应答往返、MaxDatagram 上限、未知字段容忍、空 correlation 拒绝、非法状态拒绝、状态集合稳定 |
internal/agent/icmp_echo_test.go |
12 | 内核改写 id 后仍正确关联、并发多路复用与乱序应答、payload 不匹配的伪造应答被丢弃、超时不泄露关联、超预算立即拒绝、未匹配与非 echo 数据报、关闭使所有在途 echo 失败、ctx 取消释放关联、非法目标、不可达作为应答、ping_group_range 缺失时的错误消息、失败原因必须在锁内读取 |
internal/agent/icmp_stream_test.go |
7 | nil 引擎明确拒绝、一请求一应答后 EOF(含 PolicyHook 以 ("icmp", host, 0) 调用)、四类非法目标、单请求契约与非法数据报、能力未开启时 RESET 且不拨号、端口规则只对 ICMP 放宽、ack 独立驱动两个门禁 |
internal/agent/dial_executor_test.go |
+1 | 能力集合随“配置 × 就绪”变化(三种组合) |
internal/config/config_test.go |
+1 | 四个默认值、环境变量覆盖、三类非法值快速失败、关闭时不校验 |
internal/cli/agent_runtime_test.go |
+2 | 工厂通告能力并把引擎真的装进 dialer(端到端:开流 → 写请求 → 引擎发包 → 回应答 → DATA 帧带回 peer 自己的 identifier/sequence)、socket 被拒时不致命且配置正确传递 |
internal/server/connection_selector_test.go |
+2 | 协议 → 能力映射表、tcp 不再误杀通告了能力的会话、icmp-echo 命中唯一有能力的那条 |
internal/server/vpn_icmp_gate_test.go |
2(含 7 个子测试) | 健康会话有能力 → Supported 且不查注册中心、健康会话无能力 → Unsupported、会话已关闭 → 回落集群、无任何连接 → Unsupported、只有本节点陈旧租约 → Unsupported、注册中心失败如实上报、什么都没装配 → fail closed;适配器晚解析 session manager 与 cluster lister |
internal/server/vpn_peer_service_test.go |
+2(替换 1) | 签发七态:Supported 成功且审计 verified、Unverified 成功且审计 unverified、Unsupported 409、探针错误是内部错误而非能力判定、节点开关是上限且不询问探针、无探针 fail closed、不请求 ICMP 时不探测;Update 三态:无能力 409、unverified 成功且审计、改名不触发探测 |
internal/server/vpn_peer_api_test.go |
+1 | HTTP 层端到端:真实会话协商了能力 + 节点开关打开 → 201 且 icmpEnabled: true |
门禁执行结果
go test ./... -count=1 # 全部 ok(protocol/agent/server/config/cli/e2e 等)
go test -race ./... -timeout 30m -count=1 # 全部 ok,无 DATA RACE
go vet ./... # 无输出
gofmt -l internal scripts # 无输出
git diff --check # clean
go test ./scripts/ -count=1 # ok(文档主张守卫)
cd web && npm test -- --run # 38 files / 323 tests 全绿
cd web && npm run build # 构建成功并同步 web_dist
bash scripts/verify-web-embed.sh # web/dist 与 internal/server/web_dist 一致
python3 scripts/gen_doc_index.py # 连续两次幂等,索引无变化
git status --porcelain go.mod go.sum migrations web/package.json web/package-lock.json # 空
internal/server 41.2s、internal/agent 3.1s、internal/cli 1.3s、internal/config 0.6s、
internal/protocol 0.6s(-count=1)。分支上每个提交都单独 go vet ./... 通过(含测试文件),
因此任意提交点都可编译。
未执行的验证与原因
| 验证 | 状态 | 原因 |
|---|---|---|
| 真实 ping socket 收发 | 未执行 | 本机(macOS)与 CI 都不满足 net.ipv4.ping_group_range;icmp.ListenPacket("udp4", …) 会返回 EACCES。引擎以 ICMPPacketConn 接口注入测试,权限失败路径由 listenICMPPacket 变量替换覆盖,因此“缺 sysctl 时明确报错”是被测过的,但“真实内核改写 id 后仍能关联”只在假连接上验证过 |
端到端 ping 通 |
未执行 | 阶段 6 才有发起方:没有 TUN、没有 netstack、没有 UDP 51820 监听,没有任何进程会开 icmp-echo 流 |
test/e2e/vpn/ |
未创建 | 属阶段 8;规格 §14 要求环境不满足时 SKIP 且退出码 0 |
| Docker/Compose 验证 | 不适用 | 本阶段没有 Dockerfile、Compose 或部署产物变更 |
| 双方言迁移测与升级文档 | 不适用 | 无 Schema 变更 |
与计划的偏差(全部为收紧或事实更正,无功能缩水)
- T4 与 T5 的实施顺序对调。计划 §7 把 T4(能力通告)排在 T5(Agent 配置)之前,
但 T4 的实现要读
streams.ICMPEnabled,那是 T5 才引入的字段;计划 §10 的回滚顺序 (T5 先于 T4 回滚)恰好反映了真实依赖。实施按 T5 → T4 执行,两个提交都独立编译并通过门禁。 AgentStreamCapabilities有两个调用点,不是一个。计划 §3.3 记为“唯一调用点在internal/cli/root.go:378”,实测internal/e2e/socks5_web_page_latency_test.go:522也在调用。 漏改会让go vet ./...与go test ./internal/e2e/编译失败。已修正,并通过git commit --fixup+rebase --autosquash折叠进 T4 提交,保证 T4 单独 checkout 也可编译。- 多出一个提交
fix(agent): read the echo failure under the lock。T2(49fc462)的Send在解锁后调用failureLocked(),违反其“调用方持e.mu”的文档契约;读循环在failAll里 持锁写同一字段。缺陷在 T2 时是潜伏的——只有 T3 让 echo 从自己的 goroutine 调用Send之后,-race才报出来。修在独立提交里,因为它改的是已提交的 T2 代码,与 T3 的文件集不相交。 readBack的缓冲区对icmp-echo按MaxDatagram分配(session.go)。计划 T3 未列这一项, 但默认 32 KiB 会截断一个由单个数据报构成的应答,而截断后没有第二次 Read 可以补齐。- 门禁实现的名字与形状:计划把它叫
clusterAgentICMPProbe(一个类型)。实施拆成 纯函数probeAgentICMPEcho(ctx, sessions, connections, localNodeID, agentID)加一个clusterAgentICMPProbe适配器,因为SetVPN(runtime.go:164)在SetClusterAgentConnections(runtime.go:213)之前执行:装配时捕获 lister 会冻结一个 nil, 进程整个生命周期里所有 ICMP 签发都会 fail closed。适配器每次调用现取,纯函数则可以在没有 API、数据库和注册中心的情况下被测。 - 端口校验抽成
validStreamTargetPort(proto, port)(计划描述为内联分支)。抽出来是因为 “只有icmp-echo允许 0”这条规则需要独立可测,且Handle里的条件已经四个合取项。 - nil 探针 fail closed(计划 D7 只覆盖了集群三态)。
AgentCapabilities为 nil 是装配错误, 不是集群状态,因此判为Unsupported并给出独立消息(“本节点没有配置能力探针”)。 生产装配总会注入(SetVPN),所以这条只在嵌入方漏接线时生效——而那正是应该拒绝的时候。 Update的探测顺序移到authorizeAgent之后(计划未指定)。理由见“安全与授权影响”。- T8 对阶段 4 测试的更正范围比计划小。计划列出
vpn_peer_service_test.go与vpn_peer_api_test.go都要更正“一律 409”的断言;实测后者的 fixture 本来就把server.vpn.icmp_enabled留空(=false),因此 409 仍然成立,只是原因从“本版不支持” 变成“节点开关关闭”。断言保留、注释更正,另加一个走真实探针的 201 用例覆盖能力齐备的路径。 前者的TestVPNPeerServiceICMPRequiresAnAgentCapability被七态用例替换。
顺带发现、本阶段刻意不修的既有问题
connection_selector.go的能力误比较(已修,T7)。修前Select(agentID, "tcp")在任何 通告了能力的会话上都落空。这是现网缺陷,不是本阶段引入的;修它是阶段 6 的前置条件, 因为 VPN 出口开流要走同一条选路。- 集群级能力可见性缺失(技术债,推迟到阶段 8 评估)。
registry.NodeOwner没有能力字段,agent_connection_leases没有能力列,agents.capabilities是用户声明的标签、 不是运行时协商结果,挪用它会破坏单一数据源。因此跨节点的判定只能是Unverified。 要做成精确判定需二选一:agent_connection_leases增一列(Schema v16,MINOR,需 expand/contract 与双方言增量脚本);- relay 增一个能力查询 RPC,照
ClusterAgentConnectionCloseService(internal/server/agent_connection_api.go:82-140)的dialRelayNode+ gRPC 范式。 两者都不属于阶段 7 的最小闭环。当前取舍是Unverified放行并在审计标注, 理由见“安全与授权影响”与管理员指南:随机 409 比“签发成功、运行时按icmp_unsupported拒绝并计数”更难排查。
docs/protocol/websocket.md:7的时点描述已过期:它把 flow control 与 UDP association 列为“后续协议增强项”,而两者早已落地。本阶段不改它——时点记录零改写, 且更正它属于协议文档的整体复核,不该夹在一个 ICMP 变更里。vpn_peer_service_test.go的newVPNServiceFixture每次都建新库,因此状态化用例必须逐条 重建 fixture。这不是缺陷,但让七态用例比看起来更长;抽象它属于测试基建重构,不在本阶段范围。
发布步骤
- 先升级 Server,再升级 Agent。顺序反过来也安全(旧 Server 不发
icmp-echo, 新 Agent 的引擎闲置),但先升 Server 能让能力协商在 Agent 上线的那一刻就完成。 - Server 侧无配置变更即可上线:
server.vpn.icmp_enabled默认true是上限, 没有 Agent 通告能力时签发仍返回 409,行为与阶段 5 一致。 - Agent 侧默认无变化:
agent.streams.icmp_enabled默认false。要启用 ICMP 出口的部署按顺序执行:# 1) 主机一次性放开非特权 ping socket(可收窄到 Agent 运行 gid) sysctl -w net.ipv4.ping_group_range='0 2147483647' echo 'net.ipv4.ping_group_range = 0 2147483647' > /etc/sysctl.d/99-tunnelmesh-icmp.conf sysctl --system # 2) 打开 Agent 配置 # agent.streams.icmp_enabled: true # 3) 校验后重启 tunnelmesh-agent check-config启动后确认 stderr 没有
agent icmp echo is unavailable, 并在管理台确认签发 ICMP peer 不再返回 409。 - 不要对外宣布
ping可用:数据面在阶段 6,本阶段结束后仍然 ping 不通。 - 灰度顺序:先在一台 Agent 上开启,观察
agent icmp echo is unavailable与签发审计里的icmpCapability;确认稳定后再铺开。
回滚步骤
- 不回滚代码即可止损:把
agent.streams.icmp_enabled改回false并重启 Agent。 ping socket 关闭、能力不再通告、Server 侧门禁自动退回 409。5 分钟内可完成, 不涉及数据库、防火墙或证书动作。 - 代码回滚顺序:T9 → T8 → T7 → T6 → T4 → T5 → T3 → 锁修正 → T2 → T1。
T1 必须最后回滚(
protocol常量与编解码被 T2/T3/T7/T8 全部引用),单独回滚 T1 会编译失败。 - 能力是协商出来的,因此两端可以独立回滚:只回滚 Agent → 旧 Agent 不通告
stream_icmp_echo.v1,Server 门禁自动判Unsupported→ 409,无需同时回滚 Server; 只回滚 Server → 旧 Server 不发icmp-echo,新 Agent 的引擎闲置。 - 回滚 T7 需要单独评估:它是既有缺陷修复,回滚会让
Select(agentID, "tcp")重新误杀 通告了能力的会话,即 tp-* 与 WebSSH 的选路回到缺陷状态。回滚前必须确认没有其他路径依赖它。 - 数据无需迁移:本阶段没有 Schema 变更。已签发的 ICMP peer 行保持
icmp_enabled=1, 回滚后只是不能再新增或改回true。 - 审计的新键可容忍:
icmpCapability回滚后旧代码不再写;控制台按 JSON 详情原样渲染, 对未知键与缺失键都容忍。
Reviewer 关注点
icmpEchoStream为什么自建 context(internal/agent/dialer.go)。DialExecutor.worker在e.dial(ctx, …)返回后立刻cancel(),而 echo 按设计要比拨号活得久。 借用传入的 ctx 会让每个 echo 在发出前就被取消。Close()是唯一取消点。- 关联为什么用 wire sequence 而不是 ICMP id(
internal/agent/icmp_echo.go)。 非特权 ping socket 下内核改写 id;一个进程只有一个 socket 却要服务多条并发 echo。 payload 逐字节比对是防序号回绕撞上旧关联的第二道锁。 - 超时/超预算/不可达为什么是应答而不是错误。IP 没有错误通道;把它们变成错误会让
peer 的
ping挂着而不是收到一个答复。状态是闭合枚举,阶段 6 直接映射error_class。 - 门禁默认关闭是否覆盖所有路径(
internal/agent/session.go的Handle)。 拒绝发生在d.mu.Lock()内、重复流检查之前,读的是锁内的d.icmpEcho;rejectStreamMode按 strict 标志分别发OPEN_RESULT或RESET。 Unverified放行是否可接受(internal/server/vpn_icmp_gate.go)。 这是本阶段最大的判断,理由与替代方案都写在 D7 与管理员指南里; 审计的icmpCapability是它的可观测补偿。若 Reviewer 认为应当拒绝, 改动点只有authorizeICMP的一个 case,但请同时接受“签发随请求落点随机失败”的后果。- T7 的行为变化面。修复后本地会话重新参与选路,远端 relay 回落的触发条件变严。
internal/e2e全绿说明既有链路没有依赖旧行为,但这是本 PR 影响面最广的一处, 建议单独复核connection_selector_test.go的新增两用例。 readBack缓冲区。icmp-echo按MaxDatagram(64 KiB)分配,UDP 仍按MaxPayload(1 MiB), 其余协议仍按 32 KiB。三档而不是两档,是因为一个 ICMP 应答由单个数据报构成, 截断后没有第二次 Read 可以补齐。- 探针的晚解析。
clusterAgentICMPProbe每次调用现取a.agentSessions与a.clusterConnections。若将来SetVPN与SetClusterAgentConnections的调用顺序被调整,TestAPIAgentICMPProbeResolvesClusterStateLate会失败,这是有意的守卫。
集成状态
- 分支:
codex/vpn-phase7-agent-icmp(11 个提交,stacked 在已推送的codex/vpn-phase5-admin-console之上) - 变更规模:34 个文件,+3393 / −59
- 门禁:
go test ./... -count=1、go test -race ./... -timeout 30m -count=1、go vet ./...、gofmt -l internal scripts、git diff --check、go test ./scripts/ -count=1、npm test -- --run(38/323)、npm run build、scripts/verify-web-embed.sh、scripts/gen_doc_index.py两次幂等、docs 相对链接死链核验(none)——全部通过 - 零改动守卫:
go.mod、go.sum、migrations/、SchemaVersion、OpenAPI 路径集合、 10 个稳定错误码集合、web/package.json、web/package-lock.json - 时点记录:阶段 4/5 的计划与 PR 记录只链接、未修改;对它们的事实更正只写在本文件
- 后续阶段:阶段 6(Server 数据面)消费本阶段的
streamProtocolCapability选路、VPNAgentCapabilityProbe接口与protocol.ICMPEcho*编解码; 阶段 8(可观测性与部署)承接集群能力可见性技术债、Prometheus 指标、test/e2e/vpn/与部署文档