Files
crossdesk/docs/p2p-punching-implementation-plan.md
T

750 lines
59 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CrossDesk 双端 UDP 打洞增强实现方案
状态:P0 门槛已通过;P1–P4 默认关闭原型、受控运行与跨平台构建已实现;真实异网 C 组已有两次增强直连通过双端日志及抓包核验,正式 P5 对照仍待执行。按用户要求,画面、声音、控制和切换质量不纳入本次验收。日期:2026-09-14。第 15 节保留 P0 历史记录,最新进展与边界见第 16 节。
源码基线:CrossDesk `73e3bf709d7c054df7bd75cfafceda0721c6cda1`;MiniRTC 子模块 `f169a696cb4e6cb59037ec02e85532e30d3c1262`;libnice `0.1.24` 加仓库已有补丁。
本文规定第一版的范围、接口职责、协议、资源预算、开发顺序和验收条件。第 1–14 节以设计基线为基础,第 6 节同步当前实验参数;调整理由、实际接口及完成情况以第 15–16 节和所链接的执行报告为准。实现和受控测试不代表真实 NAT 成功率已得到验证。
## 1. 目标与取证依据
目标是在普通 ICE 检查未建立直连、TURN 已经可以传输的情况下,通过“固定端点一侧扩大目的端口探索+另一侧使用多个本地 UDP 端点”寻找新的可达组合,再接入已有 ICE 检查及中继转直连流程。
2026-09-13 两次连接提供了以下依据:
| 项目 | CrossDesk 16:06 会话 | UU 21:30 会话 |
| --- | --- | --- |
| Mac 的直接探测 | 同一个本地端口,向 11 个公网端口发出 324 个 Binding Request,含重传 | 同一个本地端口,向 231 个离散目的端口发送短探测包 |
| Windows 的直接探测 | 主 ICE 端口向 Mac 公网候选持续检查,双方未捕获到直接入站 | 两个常规端点检查失败后,49 个不同本地源端口轮流向 Mac 固定公网端点发短包 |
| 成功组合 | 没有观察到 | Windows 第 46 个短包端点与 Mac 建立双向通信,随后 STUN 成功并持续传输 |
| 成功端点是否继续传输 | 不适用 | Windows :52257,经 NAT 为 :11968,后续沿用该端点 |
Mac 先前发往 :11968 的短包没有出现在 Windows 捕获中;成功开始于 Windows 自己发出的短包到达 Mac。无需把两个固定载荷短包解释成请求与回显,才能说明双方端口探索的作用。
证据边界:
- 231、49 是成功附近实际执行的数量,不是已知的 UU 配置上限。
- 抓包证明多个源端口参与,没有证明所有历史 socket 句柄同时存活,也没有恢复 UU 的端口生成算法。
- NAT 内部建表和过滤过程仍是推断;本方案不宣称复刻 UU 私有实现。
- 两次应用运行时间、socket 和服务端不同,不能从这两个样本计算成功率提升幅度。
- 详细原始取证位于本次任务的本地分析产物;实现与验收必须能够独立于 `/tmp` 文件运行,不把抓包或会话密钥提交到仓库。
## 2. 第一版范围和关键决策
### 2.1 纳入范围
- 原生 MiniRTC ↔ 原生 MiniRTC;首先验证 Windows x64 与 macOS arm64,Linux 用于编译和可控网络测试。
- IPv4 UDP,单个 ICE component,选择一个明确的物理接口和原始 host socket。
- 非强制中继、非 reliable ICE;已有选中路径包含 relay,且 DTLS 握手和远端指纹验证已经成功。
- 本轮一侧具有同一 socket 对多个 STUN 目标的稳定公网映射观测,另一侧具有目的相关映射观测。
- 每个 ICE generation 至多执行一轮增强;失败继续使用原连接。
### 2.2 暂不纳入
- 浏览器/libdatachannel 对端、iOS 启用、IPv6 探索、两侧都目的相关或两侧信息不足时的通用算法。
- TURN 尚未建立时的首次连接加速、`TurnDisabled` 下的增强协商、未启用 SRTP/DTLS 的会话。
- UPnP/PCP/NAT-PMP 的新实现、网关配置修改、跨多个接口同时建立端口池。
- 根据 UU 单个样本自动学习端口分布,或把端口枚举成全部普通 ICE 候选。
- 新增直连失效后无缝回切 TURN 的完整恢复算法。第一版遵循当前恢复行为,验证失败恢复;不得把“仍保留 TURN allocation”描述为已经具备自动回切能力。
### 2.3 架构决策
1. **先验证 socket 接入,再实现大范围探测。** 新端点在中继 READY 后必须能参与 ICE 并成为媒体端点,这是 P0 的前置验收项。
2. **SDP 只协商能力,运行时协调复用已选中路径。** 第一版通过现有 relay 数据路径传递带认证的内部控制包,不新增 WSS 消息类型,也不要求修改 CrossDesk Server。
3. **新 socket 由 libnice 管理。** 不在 GUI、业务数据通道或独立线程中建立脱离 ICE 生命周期的临时 socket 池。
4. **探索和 ICE 检查分开调度。** 数百个目的端口属于有限的探索集合;只有少量命中端点进入普通检查表。
5. **保留普通 ICE 与已有 30 秒升级窗口。** 增强操作不能重置该窗口、重启 DTLS,或延迟正常的中继可用状态。
6. **默认关闭。** 编译扩展存在、运行配置开启、双方声明支持、网络和连接状态满足条件时才启动。
这会使第一版主要改善“已通过中继连接,但希望随后升为 P2P”的场景。将来需要在没有中继时运行同类算法,应另行设计可验证的信令协调通道。
## 3. 现有代码与拟增加模块
| 位置 | 当前职责 / 后续变更 |
| --- | --- |
| [ice_utils.h](/Users/dijunkun/crossdesk/deps/submodules/minirtc/src/ice/ice_utils.h) | 已有精确匹配 SDP 能力行与 ufrag 解析;新增独立 v1 能力,不改变旧能力含义 |
| [nat_traversal.h](/Users/dijunkun/crossdesk/deps/submodules/minirtc/src/ice/nat_traversal.h) | 已有映射观测和附近端口预测;提取带 socket/generation 的快照,新增角色选择和有界采样逻辑 |
| [ice_agent.h/.cpp](/Users/dijunkun/crossdesk/deps/submodules/minirtc/src/ice/ice_agent.cpp) | 持有 NiceAgent、GMainContext 和 DTLS;接入协调器、验证后的 exporter、内部控制包分流和停止流程 |
| [ice_transport.cpp](/Users/dijunkun/crossdesk/deps/submodules/minirtc/src/transport/ice_transport.cpp) | 已有选路日志、trickle 和媒体入口;防止内部端点误发普通候选,新增结果统计,保持媒体状态连续 |
| [MiniRTC xmake.lua](/Users/dijunkun/crossdesk/deps/submodules/minirtc/xmake.lua) | 注册可选的纯逻辑测试和 ICE 集成测试目标,接入编译开关 |
| [libnice 配方](/Users/dijunkun/crossdesk/deps/submodules/minirtc/thirdparty/libnice/xmake.lua) | 注册新补丁、哈希及包身份版本,增加扩展符号检查,确保双端重新构建依赖 |
| [relay upgrade 补丁](/Users/dijunkun/crossdesk/deps/submodules/minirtc/thirdparty/libnice/patches/relay_upgrade_0.1.24.patch) | 已有保留直连检查、定期重试及再次提名;扩展接入点须遵守已有截止时间和优先级条件 |
拟新增到 MiniRTC 的模块,路径以子模块根目录为基准:
```text
src/ice/punch_config.h 配置、硬上限、实验模式
src/ice/punch_protocol.h/.cpp 编解码、认证、去重、generation 校验
src/ice/punch_scheduler.h/.cpp 端口采样、预算、时序;可注入时钟和随机源
src/ice/punch_coordinator.h/.cpp 每个 IceAgent 的增强状态机
src/ice/punch_budget.h/.cpp 进程级 socket/发包配额
thirdparty/libnice/patches/punch_pool_0.1.24.patch
tests/ice/punch_* 纯逻辑和集成测试
tests/ice/lab/ 可控 NAT 场景与抓包核验脚本
```
MiniRTC 和 CrossDesk 为不同 Git 仓库。后续先在子模块形成可验证的提交,再更新主仓库的子模块指针;不得只修改本机 Xmake 下载缓存中的 libnice 源码。
## 4. 能力、身份和角色协商
### 4.1 能力开关
拟新增精确 SDP 行:
```text
a=x-minirtc-udp-punch:1
```
只有本地构建包含对应 libnice 扩展、运行配置开启、原生 UDP 路径允许时才通告。双方同时支持该能力和现有 relay-upgrade 能力才启用。旧的 `a=x-minirtc-p2p-enhancement:1` 继续表示原有增强能力,不隐式包含端口池协议。
SDP 的能力行不是命令;资源数量由本地配置和认证后的协商共同限制。未知版本或异常重复能力声明只停用此增强,不使普通 ICE 失败。
现有 `IceTransport::SetRemoteSdp` 会提取媒体段,并显式补回旧增强能力行;新增能力也必须经过该路径保留。需要测试能力位于 session、视频或其他媒体段,以及 fingerprint 提取前后的情况,避免完整 SDP 中支持、传入 IceAgent 后却丢失。
### 4.2 连接和轮次身份
- 使用有序的 offer/answer ufrag 对标识 ICE generation;记录本地单调递增 generation 序号,防止异步旧回调误用新连接。
- 线上 `generation_id` 为长度前缀编码的 offer/answer ufrag 对的 SHA-256 前 16 字节;实际认证密钥另由本次已验证的 DTLS 会话导出。
- 原始 offer 发送方担任协调方,分配随机 128-bit `round_id`。协调角色不等于动态 ICE controlling 角色。
- 所有控制包、探测包、候选提升请求和回调必须携带 generation/round 范围。重连、ICE restart、接口改变时旧范围作废。
### 4.3 网络角色
命名:`anchor` 表示复用固定 socket 的一侧;`pool` 表示使用额外本地端点的一侧。角色根据观测选择,不与 Windows/macOS、主控/被控绑定。
有效映射快照至少包含:同一原始 host socket 的不透明 ID、本地地址、候选 foundation、component、generation、目标 STUN 地址、公网结果、接收的单调时间、观测序号。
- `stable-observed`:当前 generation 的同一 socket 至少对两个不同公网服务器 IP 得到同一公网 IP:port,观测仍在有效期内。
- `dependent-observed`:同一 socket 对不同目标出现不同映射;不能把多个本地源端口的结果混在一起分类。
- `unknown`:不足、过期、来源不一致,或公网 IP 本身变化。
第一版只在恰好一侧 `stable-observed`、另一侧 `dependent-observed` 时开始。前者为 anchor。其余组合记录跳过原因,保留常规 ICE;实验工具可显式固定角色验证接入,但不得在生产中把固定角色伪装成自动分类。
快照默认有效期 15 秒。角色选择只表示实验条件,不把有限的 STUN 观测升级为对 NAT 过滤类型或未来分配行为的保证。anchor 的公网端点必须与被保留的那个真实 socket 对应;不能使用无关联的临时 STUN socket 的结果。
## 5. 控制通道与报文契约
### 5.1 通道和密钥
控制包通过 NiceAgent 当前选中的 relay 路径发送,在 `IceAgent::OnNiceRecv` 中、DTLS/RTP/SRTP 判断之前识别和消费;不送入桌面业务协议,也不占用媒体 SSRC。
直接探测包在被借用的 anchor socket 或 libnice 管理的 pool socket 上发送,使用相同的会话范围,但使用独立的密钥域。原始 socket 收包钩子只接收直接路径的探测种类;TURN 解封装后的逻辑来源不能产生“直连命中”事件。
密钥方案:
1. 新增 `dtls_peer_verified` 状态,仅在握手完成且远端 fingerprint 比较成功后设置。当前 `dtls_handshake_done_` 在 fingerprint 判断之前赋值,不能单独作为导出增强密钥的条件。
2. 在 DTLS 所属上下文内调用 `SSL_export_keying_material`,使用独立标签 `EXPORTER-MiniRTC-UDP-Punch-v1`,context 使用上节有序 ufrag 编码,`use_context=1`。
3. 导出 128 字节,依次拆为四个 32-byte 密钥:offer→answer 控制、answer→offer 控制、offer→answer 探测、answer→offer 探测。不得复用 SRTP 密钥或把密钥写入 SDP/日志。
4. 报文采用 HMAC-SHA-256 前 16 字节认证完整头和载荷,常量时间比较。控制内容只含连接元数据,不承载媒体或密钥;它是带认证的内部数据报,不宣称因走 relay 就自动被 DTLS 加密。
### 5.2 固定头
采用显式序列化,整数为网络字节序,不直接发送 C/C++ struct。
| 字段 | 长度 | 约束 |
| --- | --- | --- |
| magic | 4 | `f3 4d 50 31`,避免与常见 STUN、DTLS、RTP 首字节范围混淆 |
| version | 1 | 1 |
| kind | 1 | 见下表,控制与探测种类严格分离 |
| payload_length | 2 | 总数据报必须恰好为 `44 + payload_length + 16` 字节 |
| generation_id | 16 | 与当前连接一致 |
| round_id | 16 | HELLO 阶段为全零;其他消息为已接受轮次 |
| sequence | 4 | 按方向和密钥域递增,重传保持原序号与内容 |
| payload | 可变 | v1 控制采用 UTF-8 JSON;直接探测采用固定二进制载荷 |
| tag | 16 | HMAC 截断值 |
控制包总长上限 1,200 字节,直接 PROBE/ACK 总长不超过 128 字节。先校验长度、版本、范围、认证,再解析业务字段;不支持分片重组。未知类型、越界数字、重复 JSON 字段、尾随内容均拒绝。
控制 JSON 使用受限、版本化的字段集;HMAC 对线上原始字节计算,接收端不重新序列化后验签。v1 禁止未知字段,后续增加字段需升级协议契约或显式定义可选字段。
### 5.3 消息和顺序
| kind | 作用与必须字段 |
| --- | --- |
| `HELLO (0x01)` | 本地观测快照、候选锚点、运行预算、剩余升级时间;快照只发送分类所需的少量样本 |
| `PREPARE (0x02)` | 协调方发送 round、角色、双方端点、配置摘要和本轮时长;不能授权任意第三方 IP |
| `READY (0x03)` | 接收方已保留原始 socket 或分配所需 pool socket,返回实际预算与 PREPARE 摘要 |
| `START (0x04)` | 协调方确认开始;双方按本地 monotonic clock 运行,不传递绝对系统时钟作为启动依据 |
| `STARTED (0x05)` | 确认 START 已接受;重复 START 不重置计时器 |
| `RESULT (0x06)` | 本端已选中直连,包含结果和地址摘要;该消息本身不能改变对端 selected pair |
| `CANCEL (0x07)` | 超时、状态变化、资源不足或主动停止;对端同样有本地截止时间 |
| `PROBE (0x20)` | 直接包;载荷为网络字节序 uint64 socket_id,随后为 12-byte probe_nonce,共 20 字节 |
| `PROBE_ACK (0x21)` | 直接包;载荷原样返回 PROBE 的 socket_id 和 nonce,由接收它的同一个 socket 发回 |
协调方通过已有能力与 HELLO 判定条件后发 PREPARE;双方准备完成后执行 START/STARTED。若协商出的实际预算改变,更新 PREPARE 并重新 READY,不在一端静默扩大或缩减配置。
控制状态消息按 200/400/800 毫秒退避,最多重发 3 次;使用本轮总截止时间约束。每种消息响应幂等,重复 READY/START 不重复分配 socket,也不重启轮次。乱序消息只能在对应状态允许的范围内缓存或拒绝,不能越级启动。
每方向、每密钥域保留 256 项序号重放窗口,允许正常乱序;已处理的控制请求可重新发送缓存响应,不能重新执行副作用。探测 ACK 必须匹配本轮尚有效的 nonce 和本地 socket_id。未认证包不回复。
上述直接 socket_id 是本地注册表的不透明标识,不是 OS 文件描述符;不得由远端指定本地 FD。
## 6. 探测算法与预算
### 6.1 anchor 的行为
1. 保留与稳定公网观测对应的实际 host socket,沿用当前物理接口绑定。
2. 在已确认的对端公网 IP 上,从 1024–65535 无放回抽取目的端口,默认 1024 个。先用可注入的 CSPRNG 生成随机集合,再将选中的离散端口按数值递增调度;测试使用固定种子。只对抽样结果排序,避免退化为从最低端口开始的连续扫描。
3. 现有普通候选和邻近预测继续由原 ICE 调度;新探索集合避免重复消耗这些目标的探测预算。低于 1024 的探测以及非均匀采样留待后续实验,不声称第一版覆盖 UU 的全部端口范围。
4. 每个新目标默认发一次 PROBE。收到 pool 的认证 PROBE 时,从同一 socket 向该包实际来源端点回 PROBE_ACK;该源 IP 必须仍属于本轮批准的对端公网 IP。
5. 原始来包地址用于路径发现;不能用包内宣称的地址替代源地址,也不能把 TURN 逻辑地址当成直接源地址。
### 6.2 pool 的行为
1. 在同一选定物理接口上分配默认 128 个不同 UDP 本地端口;绑定地址和接口遵循原 ICE socket 的平台规则。本地端口仍由系统分配,不要求其数值递增,也不据此推断公网映射顺序。
2. 新 socket 不为每个端点申请 TURN allocation、启动独立 DTLS 或开展完整多 STUN 采集,只向 anchor 已协商的公网端点探索。
3. 以 round-robin 调度 PROBE,最多 3 轮;相邻轮至少间隔 1 秒。全部端点保持可接收至本轮停止或被接入 ICE,不能发完就关闭。
4. 收到 anchor 的有效 PROBE 或匹配 ACK 后,对该具体 socket 请求提升并启动普通 ICE 检查;收到 PROBE 时先从同一 socket 回复 ACK。
5. 不根据本地源端口序列推算公网端口。探测过程中没有第三方 STUN 返回也不妨碍命中后的标准 ICE 验证。
两侧时间窗需要重叠,但不要求包在同一毫秒发出。允许丢失第一包、控制消息重传和两端系统时钟有偏差。
### 6.3 当前实验配置
以下是按用户要求扩大数量并改为离散递增发送后的实验参数,不是从 UU 反推的最佳值。调整理由见第 16.3 节;历史测试仍保留其原始参数。所有值通过经过校验的不可变 `PunchConfig` 进入每个 IceAgent。
| 参数 | 默认实验值 | v1 硬上限 / 规则 |
| --- | --- | --- |
| enabled | false | 必须双方开启 |
| trigger_delay_ms | 1500 | 从首次选中 relay 计时,并等待 DTLS 指纹已验证 |
| mapping_sample_max_age_ms | 15000 | 不跨 generation 或接口变化复用 |
| pool_size | 128 | 每会话最多 128;禁止远端扩容越过本地上限 |
| target_port_count | 1024 | 最多 1024,唯一端口数 |
| probe_pps | 128 | 本端全部新增 PROBE/ACK 合计最多 200 包/秒 |
| burst_packets | 8 | token bucket;事件循环滞后时不能瞬间补发整个积压队列 |
| pool_probe_rounds | 3 | v1 最多 3;重发也计入总预算 |
| round_duration_ms | 10000 | 探索阶段时长,最多 15000,并受原 relay upgrade 截止时间限制 |
| ice_finish_reserve_ms | 3000 | 到原升级截止前停止新探索,留给已命中 ICE 检查 |
| probe_packet_budget | 1024 | 本端 PROBE/ACK 累计上限;发送失败也记录尝试计数 |
| control_packet_budget | 48 | 本端全部控制消息与重传总量 |
| promoted_endpoint_limit | 4 | 最多 8;只有命中端点进入普通 ICE 检查 |
| rounds_per_generation | 1 | 重复能力/控制消息不创建新轮次 |
| process_active_round_limit | 2 | 超额会话跳过增强,正常媒体不排队 |
| process_extra_socket_limit | 256 | 进程内所有 IceAgent 共享配额 |
| process_probe_pps | 200 | 进程级公平调度,优先及时 ACK,仍受总预算约束 |
定义两个截止时间:`T_probe = min(本轮开始 + 探索时长, 原升级截止 - ICE 保留时间)`;`T_finish = min(T_probe + ICE 保留时间, 原升级截止)`。到 T_probe 停止新探索、释放未命中端点,已提升且正在检查的端点进入 DRAINING,继续标准 ICE 至 T_finish。到 T_finish 才取消仍未完成的增强检查;已选中的端点继续归正常 ICE 管理。协商、分配、重传均不延长原有 30 秒窗口。双方各自检查本地剩余时间;不足时拒绝开始。
控制包与新增 PROBE/ACK 单独计数;提升后的普通 STUN 检查遵循 libnice 原节拍,但需要记录它们带来的额外事务和重传。媒体包不计入探测预算。
首次实现用内部配置和明确命名的实验环境变量加载配置,仅读取一次,不增加公开 `Params` 布局或 GUI 开关;例如 `MINIRTC_UDP_PUNCH=off|wide-only|pool-wide`。无效值禁用增强并打印原因。端口池大小等调参入口统一由同一校验函数处理。
`wide-only` 保留同样的协议、角色和锚点选择,但 pool 侧只用一个端点;`pool-wide` 使用配置的端点池。基线 `off` 仍保留已有附近端口预测。不能让实验分组顺带改变其他 ICE、TURN 或重试配置。
单端点模式也经过新的协调和端点接入流程,因此 A/B 差异不能全部归因于采样范围;B/C 对比用于隔离多个本地端点带来的增量作用。
## 7. libnice 接入与命中后的完整闭环
### 7.1 后端职责
新增能力全部在 NiceAgent 的 GMainContext 中执行。C++ 只持有带 generation 的不透明句柄,不直接持有可悬空的 `NiceSocket*`。
拟提供的内部接口契约,确切 C 签名在 P0 验证后冻结:
| 拟新增接口 | 语义 |
| --- | --- |
| `punch_get_capabilities()` | 扩展版本及可用能力;不能仅因头文件存在就通告支持 |
| `punch_acquire_anchor(base_candidate)` | 借用并保留真实 anchor socket,返回句柄与实际本地地址 |
| `punch_create_pool(base_candidate, count)` | component 拥有新 socket,逐项报告分配成功/失败;只做直接探索 |
| `punch_send(handle, remote, bytes)` | 从指定 socket 发出原始直接 UDP,不走当前 selected relay |
| `punch_packet` 事件 | 句柄、原始外层 src/dst、字节、generation;只在直接 socket 接收路径产生 |
| `punch_promote_and_check(handle, anchor_candidate)` | 原子地把该 pool socket 注册为可检查的本地 host 候选,向已有 anchor 远端候选触发普通 STUN |
| `punch_stop_discovery(round)` | 到探索截止时停止自定义探测,释放未命中端点;保留已提升并在验证的端点 |
| `punch_cancel(round)` | 撤销探索 source、借用引用和未被 ICE 接管的 socket;幂等 |
| `punch_get_upgrade_deadline()` | 返回已有升级截止的 monotonic 时间,避免 C++ 层另起不一致的 30 秒计时 |
不要用 `nice_agent_set_port_range()` 代替端口池;该接口只约束 host 候选的端口分配范围。不要调用强制 selected-candidate API 绕过 ICE 验证。
接入点要覆盖:component 的 socket source 管理、UDP 接收分流、候选与 socket 的关联、候选对入队、READY 后重新唤起检查计时器、候选裁剪和销毁。新增 socket 使用现有 UDP 后端的创建过程,保持 Windows/Apple 物理接口绑定行为。
### 7.2 直接接收分流
- anchor 原始 socket:在 libnice 将未知源的非 STUN 数据丢弃之前,识别仅在已协商轮次生效的 PROBE/ACK,传给协调器验证。
- pool 探索 socket:只处理本轮的直接 PROBE/ACK;普通业务数据不向上交付。提升后允许该 socket 进入正常 libnice STUN/媒体处理。
- 接收钩子必须区分原始 UDP、TURN 服务器来源及 TURN 解封装后来源。嵌套的同 magic 载荷不得走直接命中分支。
- 在原来的 NiceSocket 事件循环中读取,禁止另一个 recvfrom 线程竞争同一 socket 的 STUN 或媒体包。
- 回调不能在 libnice 内部持有不可重入锁时同步反向调用后端;必要时保留有界字节副本和句柄,投递到同一 context 的后续任务。
### 7.3 标准 ICE 验证路径
第一版固定由 pool 端首先对新 socket 启动普通检查,避免将每个猜测端口伪装成已发现的远端候选:
1. pool 从某个 socket 收到有效 PROBE 或匹配 ACK,确认该端点有直接入站证据。
2. 后端保持该实际 socket 不关闭,将其提升为正常本地 host 候选,并与已有 anchor 公网候选建立检查对。
3. pool 从该 socket 发出正常 Binding Request,使用当前 generation 的 ICE 凭据、真实 controlling/controlled 角色和既有 priority 规则。
4. anchor 在原始 socket 上收到请求,经 libnice 认证后按标准逻辑学习新的 peer-reflexive 远端地址、响应并触发反向检查。
5. 双向检查形成 valid pair,controlling 一侧通过现有 relay-upgrade 路径执行 nomination/renomination。
6. 收到真实 selected-pair 变化,双方才各自将状态记为 `DIRECT_SELECTED`。RESULT、短包 ACK 或发现 prflx 均不能代替这个条件。
直接 probe 认证和 ICE 认证是两个不同步骤。成功 probe 不标记候选 valid,不伪造 STUN 成功事务,也不提前对外报告“学到 prflx”。
必须覆盖 pool 为 controlling 和 pool 为 controlled 两种情况。新增 pair 的优先级由现有候选计算规则产生,并验证满足已有“优先级高于所选 relay 才升级”的条件。
### 7.4 候选数量与 end-of-candidates
探索 socket 在命中之前不参加全量候选配对,不为端点与目标端口的全部组合建立检查对象;控制提升数量,逐个接入正常检查。
内部增强端点不通过普通 `new_candidate` 信令发送。为这些端点明确添加内部来源标记,并在现有 `new-candidate-full` 回调处处理,防止在已发送 end-of-candidates 后又误发一批普通候选。既有 host/srflx/relay 的正常收集和结束标记不受影响。
这属于双方协商的 MiniRTC 扩展;不能假定标准浏览器或未打补丁的 libnice 在 READY 后接受相同的动态行为。
### 7.5 媒体连续性和所有权
- selected relay 在新的 pair 真正选中前持续传输;不调用 ICE restart,不创建第二套 DTLS/SRTP 会话,不重置 SSRC、编码器和传输控制器。
- 探索中:component 持有 pool socket;协调器持有不透明句柄。
- 提升后:所有权转入正常候选/检查生命周期,协调器只保留观察标记。
- 选中后:候选及 component 保持 socket 存活直到连接关闭;取消探测不能关闭它。
- 未选中的已提升 socket 若仍被进行中的 STUN/checklist 引用,先在 context 中安全移除相应工作,再释放。不能按一个“关闭整个池”的循环直接 close。
- 借用的 anchor socket 和原 relay socket 始终由原 ICE 生命周期管理,探测清理只释放额外引用。
进程级额外 socket 配额随实际 socket 生命周期持有。提升或选中只转移配额所有者,不能提前返还配额后让进程实际句柄数越过上限。
## 8. 状态机、取消和故障行为
```text
DISABLED / INELIGIBLE
|
v
WAIT_RELAY_AND_VERIFIED_DTLS
|
v
EXCHANGE_HELLO -> PREPARING -> READY -> PROBING
| |
| v
| ICE_VALIDATING -> DIRECT_SELECTED
| |
v v
DRAINING -> DIRECT_SELECTED
|
v
STOPPED
```
`ICE_VALIDATING` 表示至少一个端点已提升;在数量与时间预算内可继续其余探索。单个命中检查失败不能立刻报告整个实验成功或关闭仍在工作的 relay。
到 T_probe 或探索预算耗尽时停止新探索;存在待验证端点则进入 DRAINING,否则结束。DRAINING 只继续已经登记的 ICE 工作,不再创建新端点或扩大端口集合;最多持续到 T_finish。
任何活动状态均可因下列事件进入 STOPPED:
- 对端不支持、模式不符、观测不足、预算不足、控制协商超时。
- 普通 ICE 提前选中了直连;立即停止本增强,区分成功来源为 `ordinary-ice`。
- 原升级窗口到期、会话断开、ICE generation 改变、被借用 socket/接口失效。
- 本轮 socket 分配不足、队列过载、认证上下文消失、协议冲突。
无效外部包只丢弃和限频计数,不能让任意错误报文取消有效会话。已认证对端主动 CANCEL 可以停止本轮,但不关闭媒体连接。
关闭顺序必须是:禁止新调度并标记取消 → 作废 generation/round 任务 → 移除 GSource/回调 → 释放探索资源 → 安全处理未选中提升端点 → 执行现有 NiceAgent 异步关闭 → 清理 exporter 密钥和 DTLS。工作不能阻塞或反向等待自己的 nice_thread。
构建默认开关关闭时应没有额外 socket、计时器、控制包或能力通告。运行关闭后不能靠收到对端消息隐式开启。
## 9. 日志与测量口径
INFO 记录阶段和结论;逐包记录放入可选调试日志。所有记录包含 connection/generation/round 标识,不打印 ICE 密码、exporter 或完整认证 tag。
| 事件 | 必须字段 |
| --- | --- |
| eligibility | 模式、双方能力、角色依据、样本 socket/年龄、跳过原因 |
| prepare/start | 协商实际预算、本地截止时间、anchor 本地/公网端点、批准的对端 IP |
| probe-tx/rx | 本地 socket_id、实际地址、原始外层源/目的、nonce、direct/control/relay 标记 |
| promote | 命中的具体 socket、触发原因、候选 foundation、候选对标识 |
| ICE check | TID、pair、角色、状态、外层地址、TURN peer(若有) |
| selected | 完整本地/远端候选类型和地址、成功来源、是否沿用命中 socket |
| stop | 原因、持续时间、唯一目标数、socket 峰值、发送尝试/成功/失败/重传、清理结果 |
成功率至少分别报告:
- 全部连接尝试中的直连建立比例。
- 满足增强条件并实际启动的连接中的增强成功比例。
- 普通 ICE 自行成功、增强成功、继续 relay、连接失败的数量。
分母包含失败/超时,不把“跳过增强”混入增强失败,也不能只汇总成功样本。记录 P50/P95 建立耗时、额外包数、峰值句柄和 CPU。切换期间的媒体间隙为可选观测,缺测记 null,不作为本次验收门槛。每条结论都能追到 session ID、二进制版本和抓包。
## 10. 分阶段实施与验收
以下顺序是依赖关系。P0 未通过时先解决后端接入问题,不能通过增加扫描范围掩盖“已命中却无法接入 ICE”。
### P0:验证 READY 后新增 socket 与媒体切换
交付:最小 libnice 扩展及双端集成测试,在受控网络中创建一个额外 pool socket,获得直接入站后提升、普通 ICE 验证、选中并传输。
验收:
- 从真实 selected relay、READY 和已验证 DTLS 状态开始,端点在此后创建。
- Windows/macOS 均验证创建、物理接口绑定、接收、提升及结束清理;两个 ICE 角色都覆盖。
- 捕获证明命中的本地端口和后续 STUN/媒体端口一致。
- 验证候选不会因 READY、检查上限、同 foundation/base 或 end-of-candidates 流程被错误抑制。
- 选路变化不重启 DTLS;未命中和主动取消保留原 relay。
- 冻结后端接口、所有权表和必要补丁范围,记录实际代码接入点。
P0 工具只能使用实验提供的明确对端地址,不含广范围探测。
### P1:协议、密钥与配置
交付:能力行、验证后 exporter、报文编解码、重放窗口、HELLO/PREPARE/READY/START 协商和默认关闭配置。
验收:新旧版本互连正常;关闭时无新增行为;无效 tag、旧 generation、重复 START、丢失 READY、时钟偏差不导致越级启动或重复分配。控制包不会进入 DTLS/SRTP/业务包解析。
### P2:角色、端口采样和预算
交付:映射快照、角色判断、无放回端口采样、token bucket、pool 轮发策略、进程级配额。
验收:假时钟可以复现相同发送计划;延迟唤醒不超突发上限;观测过期/多 socket 混淆被拒绝;同进程多连接不突破共享限制;短 ACK 不绕过总预算。
### P3:组合探测与提升
交付:anchor、pool、直接报文接收钩子、命中后普通 ICE 检查、selected-pair 归因。
验收:完整实现第 7 节闭环;只提升命中端点;来自 TURN 内层的相同短包不产生直接命中;成功、超时和普通 ICE 抢先成功均正确清理。
### P4:生命周期、兼容与构建
交付:断开/重连/接口变化处理,libnice 配方和包身份更新,新符号检测,Windows/macOS/Linux 编译验证。
验收:连续创建/停止至少 100 轮无残留端口池或 source,关闭竞态检查通过;旧依赖缓存不能错误通告新能力;iOS/WebRTC 路径未启用此模式且正常构建。
### P5:真实网络对照与启用决定
交付:A/B/C 配置、自动汇总脚本、双端抓包复核和实验结论。
- A:现有策略,增强关闭。
- B:相同新协议与相同的自动角色判定规则,一侧单端点,另一侧扩大目的端口;角色必须由有效观测支持。
- C:与 B 相同,但使用端口池。
按交错顺序重复 A/B/C,第一轮每组至少 30 次;保持网络、版本、STUN/TURN 和其余配置相同,记录每次启动间隔和真实端点。观察到一次成功不等于方案验收完成。
先验收机制和无回归,再评价收益:每个增强成功样本必须能证明直接命中 → 标准 STUN → nomination → 直接媒体;默认关闭路径无行为变化;资源全部在上限内。报告成功比例的不确定性,不从小样本承诺固定提升幅度。
验收范围调整(2026-09-14,用户明确要求):不验收画面、声音、控制和切换质量,
不再将这些项目列为阻塞项,也不将免验记为测试通过。仍需证明同一命中 socket
完成标准 ICE 提名并持续承载应用数据,保留 DTLS、兼容性及资源生命周期检查。
在真实网络证明净收益且兼容/退出检查通过之前,保持默认关闭;通过后再单独形成默认启用及参数调整变更。
## 11. 测试设计
### 11.1 纯逻辑与协议测试
新增拟定目标 `minirtc_punch_unit_test`,覆盖:
- 跨 generation、跨 socket 的样本隔离和角色对称性。
- 端口采样范围、唯一性、可复现性、耗尽与数量边界。
- 速率/包数/时长/进程配额,事件循环暂停后的行为。
- 报文截断、长度溢出、错误域密钥、错 tag、旧轮次、nonce/socket 不匹配、乱序与重放。
- READY/START/CANCEL 重复、断开在任意状态发生、部分分配失败。
- 默认关闭、新旧能力组合、强制 relay、reliable、无 DTLS 验证的跳过条件。
### 11.2 libnice 集成测试
新增拟定目标 `minirtc_punch_integration_test`。当前 MiniRTC 没有已注册的可直接运行的 ICE 测试目标,旧 `tests/peerconnection` 示例不是可用基线。实现时必须注册目标并写出准确的构建/执行命令。
集成测试验证真实 NiceAgent、真实 socket、STUN 认证、triggered check、选路、媒体收发及销毁。模拟器内的单元测试不能替代 P0。
### 11.3 可控网络场景
搭建受控 UDP/NAT 测试拓扑,或具有真实 socket 和明确翻译规则的测试代理。不要默认 Linux 普通 MASQUERADE 就等于本次目的相关映射;必须先通过多目标 STUN 验证场景行为。
必测:
1. LAN/普通可直连网络:普通 ICE 成功,不启动或及时停止增强。
2. 一侧稳定映射且有端口相关过滤,另一侧目的相关映射:未知新映射参与匹配。
3. 丢弃 anchor 最早的探测,让 pool 后来的包首先到达:复现 UU 的关键可达顺序。
4. 没有目标端口交集:到期后保持 relay,释放探索资源。
5. 模拟 UDP 丢失、重复、乱序,控制包延迟,以及两端墙上时钟相差至少 500 ms。
6. 只有 TURN 内层收到同内容数据:不得记录为直接命中。
7. 两侧都目的相关、观测不足、IPv6-only:v1 跳过,原功能继续。
8. pool 任一端点命中后断开、提名中取消、接口变化、旧任务到达新 generation。
9. 进程中超过两个连接请求增强:资源限制与普通媒体均正常。
### 11.4 真实会话验收
双方记录二进制/提交版本、实际配置、网卡、抓包起止与内核丢包计数;按 nonce、STUN TID、外层地址和成功后的 socket 对齐。NAT 可能重写端口,不能仅按发端源端口搜索对端抓包。
“直连成功”的严格条件:候选对两端均不含 relay,标准检查和提名完成,双端网卡观察到相匹配的外层 UDP,并持续承载应用数据。日志中的 prflx 或一个短包 ACK 不算成功。
## 12. 已识别的实现风险与解决位置
| 风险 | 解决阶段 |
| --- | --- |
| READY 后新增 socket 无法进入检查或被裁剪 | P0 验证并冻结必要 libnice 改动 |
| 为全部端口组合创建 ICE 对象导致检查表膨胀 | P2/P3 将探索与少量提升分离 |
| 临时 socket 成功后被清理,媒体仍发旧端口 | P0/P3 明确所有权转移并核对原始端口 |
| 把 TURN 解封装逻辑来源当成直接来包 | P3 在原始 socket 层分类,P5 双端抓包验收 |
| 现有 DTLS done 标志先于指纹验证 | P1 增加明确的 peer-verified 状态与导出门槛 |
| 自定义控制包落入 SRTP/业务解析 | P1/P3 按版本和种类分流,协议测试与媒体回归 |
| libnice/GLib 回调锁内重入、退出时悬空引用 | P0/P4 同 context 调度、句柄注册表与幂等停止 |
| 两端升级窗口不同或旧消息延长新轮次 | P1/P2 本地 monotonic 截止、generation/round 隔离 |
| 头文件与旧 libnice 静态库不匹配 | P4 包身份、补丁哈希、编译/链接及运行能力检测 |
| 把一次成功当作普遍成功率提升 | P5 分组重复实验,保留全部失败和跳过样本 |
## 13. 后续执行入口
后续实现从 **P0** 开始,先读取本方案和当时仓库状态,确认源码基线是否变化。每完成一个阶段,在对应实现提交说明中记录:改动范围、验收证据、未解决问题和本方案是否需要修订。
如果 P0 证明某个后端接口或 selected-pair 路径不可按本文实现,应先根据实际 libnice 代码修订这一处设计并给出替代路径,再继续后续阶段;不要把未完成的直接验证改成强制选路或延长超时。
本方案当前的执行目标是一个默认关闭、可复现实验、完整管理 socket 生命周期的原型。通过 P5 后,才讨论默认启用、扩大网络适用范围,以及首次连接阶段的增强。
## 14. 技术依据
- [RFC 8445:ICE 的认证、peer-reflexive 候选、触发检查和提名](https://www.rfc-editor.org/rfc/rfc8445.html):命中后的验证复用 ICE;本文 READY 后探索为双方协商的额外能力。
- [libnice NiceAgent 文档](https://libnice.freedesktop.org/libnice/NiceAgent.html):候选收集、端口范围、接收回调和检查参数;新增 pool API 不属于现有公共 API。
- [OpenSSL 3.3:SSL_export_keying_material](https://docs.openssl.org/3.3/man3/SSL_export_keying_material/):独立标签及 context 导出会话密钥;本方案新增 peer-verified 前置条件。
- [NAT traversal:多 socket 与目的端口探索](https://tailscale.com/blog/how-nat-traversal-works#the-benefits-of-birthdays):两个集合相交的通用思路,用于解释实验方向,不作为 UU 私有实现或成功率证明。
## 15. 本轮 P0 实施记录(2026-09-13)
MiniRTC 实施提交:`7f0e666216891c9b79bd4a8f75b63741fe1fabf2`。
本机最终检查为 22/22 通过,包括 en1 实际 IPv4 与内核接口绑定读回;这不是跨 NAT
成功率。详情见 [P0 测试记录](../deps/submodules/minirtc/tests/ice/lab/p0-validation-2026-09-13.md)
与 [构建、双机执行和抓包步骤](../deps/submodules/minirtc/tests/ice/lab/p0-experiments.md)。
本节是执行记录,不改变 P0→P5 的依赖门槛。**本轮没有完成可启用的“双端
UDP 打洞增强”产品功能。** P0 的本机机制已可运行;Windows、双端物理网卡抓包
和实际 CrossDesk 媒体尚待验收,因此不启动 P1–P5,不通告生产 v1 能力,也不进行
512 目的端口/64 本地端点的广范围探索。
交付位于 MiniRTC 子模块,不能仅依赖开发机缓存:
- `thirdparty/libnice/patches/punch_pool_0.1.24.patch`:在前三个现有补丁后应用,
新增 `nice/punch.h` 与单端点实验后端,补丁 SHA、包身份版本与符号链接检查已进配方。
- `tests/ice/punch_integration_test.cpp`:真实 NiceAgent、TURN、DTLS、SRTP 与 UDP
转发门控,pool 的两个 ICE 角色均可执行;在代码中检查内核实际 selected socket。
- `tests/ice/punch_peer_test.cpp`:两个独立进程/两台 LAN 主机的 P0 实验入口,
私有共享目录提供测试信令,便于 Windows/macOS 后续配合;不是生产控制协议。
- `tests/ice/lab/run_p0.py`:临时 coturn、STUN 就绪检查、场景矩阵、二进制 SHA、
结构化结果与清理;`tests/ice/lab/p0-experiments.md` 给出准确构建、双机操作和抓包清单。
- `src/ice/ice_agent.*`:明确 `dtls_peer_verified_`,统一两个握手完成入口的指纹
验证;导出 SRTP 前要求验证成功;有界保留早于本端 READY 的 DTLS flight。
### 15.1 经源码和实验确认的调整
1. `priv_conn_check_add_for_candidate_pair_matched()` 原来会将 READY 降为 CONNECTED。
对已打开 P0 端点且处于原升级窗口内的连接保留 READY,正常 STUN 排队、优先级、
valid pair 和 nomination 不变。取消增强检查时,尚存的 selected relay 不因
checklist 中提名对象已裁剪而被错误宣告 FAILED。
2. 直接接收分流必须排除两类中继:本端 TURN 解封装,以及**对端 allocation 向
本端 host 发出的普通外层 UDP**。后者在 pool 为 controlling 的负向测试中被发现,
因而在原始接收钩子中额外排除已知远端 relay candidate 地址。
3. 初始实验复现了既有 DTLS 次序问题:ClientHello 到达时本端仍为 CONNECTED,
原代码丢包且对端没有后续包推动握手。现在最多暂存 4 个、每个不超过 2048 字节的
早到 DTLS 数据报,仍由原 READY 回调启动同一个 DTLS 会话;没有重新握手、修改
ICE/DTLS 超时或延长升级窗口。BIO 方法初始化也改为线程安全的一次性初始化。
4. libnice 的 SDP 解析器按 LF 切分,直接复制 ufrag/password;实验初版擅自将
libnice 输出改成 CRLF,造成密码尾部多出 CR 与真实 STUN integrity 失败。
夹具已保留原 LF 格式。此修正仅在测试信令中,不改写生产信令行为。
### 15.2 当前后端契约及边界
公开名称刻意标为 `NICE_PUNCH_P0_ABI`,不是 `udp-punch:1` 的运行能力证明。
| 接口 | P0 实际语义 |
| --- | --- |
| `nice_agent_punch_get_abi/live_sockets` | 实验 ABI、进程额外 socket 存量 |
| `nice_agent_punch_get_deadline` | 读取现有有效 relay upgrade 截止,无新窗口 |
| `nice_agent_punch_open` | 指定真实 IPv4 host base,在 READY relay 上借用或创建一个端点 |
| `nice_agent_punch_send` | 指定 generation/不透明 handle,向明确地址发送 80 字节实验探测 |
| `punch-packet` | 锁外信号,带范围、实际外层地址与有界字节副本;不表示 ICE valid |
| `nice_agent_punch_promote` | 保持 socket,加入一个普通 host/已知 anchor remote 检查对 |
| `nice_agent_punch_stop(..., FALSE)` | 停止探索,关闭未提升端点,继续已提升检查 |
| `nice_agent_punch_stop(..., TRUE)` | 先剪除未选中检查再关 socket;已选中端点保持传输 |
原始 anchor、relay 的所有权始终属于原 ICE;新 socket 属于 component 的
SocketSource;提升不关闭或重建 socket。额外 socket 配额在实际 SocketSource
释放之后返还,选中后仍占配额直到关闭。句柄不暴露 FD,不在 libnice 锁内反向调用。
P0 仅允许每 component 一个实验端点,进程硬限制 256 个额外 socket。它没有实现
P1 的生产认证上下文、P2 速率公平预算、P3 广范围组合探测或 P4 完整重连协调器。
实验 HMAC 密钥仅来自可信夹具/私有测试信令,不声称已经完成 DTLS exporter。
`MINIRTC_UDP_PUNCH=wide-only|pool-wide` 目前没有被实现或读取;产品仍关闭。
### 15.3 验收和下一步
当前可运行的构建与实验步骤见子模块
`tests/ice/lab/p0-experiments.md`;可公开的测试摘要保存在同目录的结果记录中。
成功意味着本机真实中继 → 验证过的 DTLS → 新 socket 的认证入站 → 正常 STUN/
nomination → 同一 socket 的双向 SRTP,不能将编译、短包 ACK 或一次局域网成功
当作 P0 总验收或 P5 的真实网络收益。
P0 总门槛仍需 Windows x64 编译/执行、Windows/macOS 双端抓包、两个 ICE 角色、
实际桌面媒体连续性及退出检查。完成这些后再冻结生产后端接口并进入 P1,沿用本文
其余协议、预算和 P1–P5 验收要求。此处没有通过降低验收要求提前启动广范围探索。
2026-09-13 后续进展:用户报告同步后的 Windows 构建成功,尚未取得 Windows
网络测试日志或二进制哈希;不能将构建成功记为 P0 运行验收。两端实际不在同一
局域网,当前双机夹具要求明确且互相可达的 UDP 地址,因此不能直接使用两端
私网 IP 运行该夹具。`run_p0.py` 新增外部实验 TURN 模式,使 Windows 可以先
独立完成本机双 ICE 角色、SRTP 和资源清理矩阵,无需本机安装 coturn。外部 TURN
不等于双机直连拓扑,此调整不改变 P0 门槛、原升级窗口或后续阶段依赖。
### 15.4 两台 Mac 局域网实测
用户提供同局域网的第二台 arm64 Mac 后,已在 `192.168.31.125/en1` 与
`192.168.31.102/en0` 执行真实双机 P0;双方系统均为 macOS 26.6.2。
`run_p0_ssh.py` 仅用 SSH 传递实验 JSON 信令,探测、STUN 和 SRTP 仍通过真实
LAN UDP socket 与原受控门控转发。没有改用 SSH 隧道承载被测传输。
四种 ICE/端口池角色组合的媒体断言均通过。首轮严格双端逐包核对为 3/4 通过,
另一组有一个 SRTP 包只出现在发送端抓包中,原始失败记录已保留,未改判为成功。
该组以新端口、新 run ID 复测后通过,两端均取得零内核丢包统计。四种组合现均有
探测 HMAC → 认证 STUN/提名 → 命中 socket 上双向 SRTP 的双机报文证据。
双机夹具增加开池前至少 10 个中继 SRTP、停止探测后至少 10 个直连 SRTP、
selected socket 的物理接口绑定读回;全部仍在原升级窗口内。仓库新增
`verify_p0_capture.py`,核对实际 pcapng、ICE MESSAGE-INTEGRITY、提名 TID 和
门控两侧的相同报文,私有密钥和原始抓包不入库。
完整次数、哈希、例外、复现和清理记录见
[双 Mac P0 实测记录](../deps/submodules/minirtc/tests/ice/lab/p0-lan-validation-2026-09-13.md)。
本轮完成 Mac 双机机制与抓包验证,**没有替代 Windows 运行、跨平台及实际应用
媒体验收**。P0 总验收仍未完成,P1–P5 继续保持未启动,生产功能默认关闭。
### 15.5 Windows 局域网验收准备(2026-09-14)
用户另提供同局域网 Windows:WLAN `192.168.31.220/24`,仓库
`D:\SourceCode\crossdesk`,账户 `desktop-ri1qfbe\kunkundi`。用户输出确认
Python 3.13.5 可用,MiniRTC 为 `7f0e666`,两个 P0 测试 EXE 文件存在。
这些信息不代表测试已运行;升级后的双机媒体断言需要同步并重新构建 peer 目标。
实验驱动增加 `--remote-shell powershell`,通过编码命令传入 Windows 的字面路径,
仍仅使用 SSH 管道传递私有信令。抓包核验器允许显式指定两端物理和本地回环接口名,
以适配 Npcap 的命名;未将虚拟网卡或回环流量当作物理网卡交付证据。
README 补充 Windows SSH 驱动、两接口抓包和精确接口名核验步骤。
本机已检查 Python 语法、带空格/单引号/中文路径的命令编码、POSIX 参数兼容,
并用原 Mac 抓包回归默认和显式接口选择;改写接口名后的同一份 Mac 报文也能核验,
错误物理接口或 anchor 回环接口会被拒绝。这只是工具回归,**未执行 Windows
PowerShell 启动、Windows 网络矩阵或 Npcap 验证**。当前等待 Windows SSH 接入,
后续使用本机临时 TURN 开展单机矩阵和四种跨平台角色组合,保持原 P0 门槛。
### 15.6 Windows/macOS P0 验收完成(2026-09-14)
Windows 真实单机矩阵 20/20 通过,包含两个 ICE 角色、100 次生命周期、取消、
未命中、错误指纹和原截止时间。跨平台四角色均已取得命中端口上的认证 STUN、
提名及双向 SRTP 的严格双端抓包证据。完整结果、二进制和抓包 SHA、失败样本及
边界见 [Windows/macOS P0 记录](../deps/submodules/minirtc/tests/ice/lab/p0-windows-validation-2026-09-14.md)。
本轮没有把全部尝试改判为通过:初次双机抓包为 3/4,修正夹具收尾后为 4/5。
最终普通 Mac pool/controlling 一组有中段一个 SRTP 包只出现在 Mac 抓包;同角色
的单向通知延迟 1 秒组严格通过,因此四种角色均已有最终版本证据。另一次 Windows
开池前 TURN 媒体基线失败,针对两种角色复测均通过,原失败仍保留且原因未确立。
Windows 抓包统计包含 16 个 flushed,pcap/dumpcap/接口丢包为 0;未声称总丢包为 0。
经源码和最小实验确认两处夹具修正:Windows `IP_UNICAST_IF` GET 已为主机序,
去掉重复转换;旧夹具等待对端完成消息时仍发媒体,另一端可先销毁 gate,单向
延迟实验复现最后 13 包未转发。新增停止发送、交换最终序号、双端认证接收确认后
再销毁的屏障;没有修改标准 ICE、原窗口、DTLS 或降低媒体门槛。
完整 CrossDesk 两端编译成功,Mac 使用 `xmake r crossdesk` 提供 Slint 运行环境。
用户对双向画面、声音、鼠标键盘和三次重连请求回复已完成,未报告异常;此项作为
用户报告的普通应用回归,不能声称独立测量了 pool 切换时的实际编解码连续性。
命中 socket 的切换与认证媒体由 P0 真实 SRTP 夹具验证。
P0 后端接口、所有权与取消语义按 15.2 冻结,允许进入 P1。P0 仍是每 component
一个实验端点;P3 扩展数量时必须继续遵守同一 socket、标准检查和选中资源所有权。
生产能力、广范围探索和默认启用尚未交付,P5 真实未知 NAT 的收益仍需后续对照。
## 16. P1–P4 实现与 P5 实验入口(2026-09-14)
本节更新当前状态;第 15 节“未开始 P1–P5”等文字是对应时间点的历史记录。
在 P0 跨平台门槛通过后依次完成 P1/P2 基础、P3 多端点接入和 P4 生命周期/构建,
没有在 P0 前放大探索范围。源码及实验工具由主仓库当前 gitlink 固定。完整证据见
[P1–P4 验证记录](../deps/submodules/minirtc/tests/ice/lab/punch-runtime-validation-2026-09-14.md),
准确命令见 [v1 实验步骤](../deps/submodules/minirtc/tests/ice/lab/punch-v1-experiments.md)。
| 阶段 | 当前交付及边界 |
| --- | --- |
| P0 | 已通过原门槛,保持单端点 ABI 1 与原始失败记录 |
| P1 | 配置/能力、指纹验证后 exporter、认证协议与协商完成;默认关闭,新旧/未知能力纯逻辑及实际生成能力检查通过 |
| P2 | 同源观测、角色、CSPRNG 采样、本地与进程预算完成;首次实现时 Mac/Windows/Linux 各 2,952 个单元断言通过,后续清理回归见验证记录 |
| P3 | 独立 ABI 2、64/128 端点、同 socket 提升/标准检查/提名/媒体闭环实现;三个平台受控 runtime 各 12/12 |
| P4 | owner context 清理、DRAINING、取消、重连身份和接口/base 检查实现;百次 source/agent 生命周期、Mac/Windows 完整应用、Linux ICE smoke、iOS 静态库已验证。另有新版 Mac→旧 Windows 的非 SRTP 与 DTLS/SRTP 普通中继互通及清理日志;后者本地增强资格满足、旧端不支持,正确不启动增强。物理网卡真实切换和其余旧版连接组合仍待验证 |
| P5 | 90 次交错清单与核验工具已准备;完整窗口 A/off 3 次均中继,READY 重查修复后 B/wide-only 3 次均启动但未命中,C/pool-wide 3 次为 2 次增强直连、1 次中继。两个成功会话已补齐 Windows 日志并由仓库 wire 脚本核验 2/2 通过。正式随机 A/B/C、运行版本和捕获统计仍需补齐,不作收益或默认开启决定;画面/声音/控制/切换质量按用户要求免验 |
### 16.1 对实际源码的必要调整
- 保留已验收的 `punch_pool_0.1.24.patch`,追加 `punch_v1_0.1.24.patch`。
`nice_agent_punch_get_v1_abi/open_v1` 与 P0 API 区分;v1 每池最多 128、进程 256,
提升最多 8。补丁 SHA 与包身份版本进入 MiniRTC 配方,不依赖本机缓存改动。
- `PunchRuntime` 由 IceAgent 持有,在同一 GMainContext 使用真实句柄,直接收包
与 TURN 控制分流,认证命中后由 pool 提升。实际 selected pair 才报告结果,
各自的原始截止不会因 HELLO/重传/START 被延长。
- SDP 指纹最小实验发现原解析吞并 fingerprint 之后所有行并截断 SDP,导致新增
候选/能力位于其后时 DTLS 校验失败。改为逐行解析、保留其余 SDP、拒绝冲突指纹,
并加入真实握手与位置测试;没有通过禁用指纹校验绕过失败。
- 通用 DEBUG 日志在现有项目中编译关闭,因此添加独立、默认关闭的
`MINIRTC_UDP_PUNCH_TRACE=1`,只记录有界探测元数据。应用原有 SDP 日志包含 ICE
密码,原始实验文件必须保持私有;新代码不记录 exporter 或完整 tag。
- 首次启用后的真实应用试跑发现 eligibility 日志读取未赋值的旧 ufrag 指针,
在接收 SDP 时崩溃;最小实验复现后,eligibility/skip 与 exporter 统一从当前
libnice stream 读取 generation 凭据。补充双角色真实 SDP 接收/重建及自动 skip
的 TURN/SRTP 回归,详情见验证记录。此修复不改变协议或绕过探测前置条件。
- Mac off 试跑发现传输层复制指纹行后缺少 LF,追加原能力时产生 CR CR LF,
无 v1 能力的 SDP 被严格校验拒绝。完整 IceTransport 实验复现后在拼接处补齐
LF;三种运行模式各 36 个组合及错误指纹拒绝通过,证书校验和默认关闭不变。
- B 组一次启动遇到 READY → CONNECTED → READY 的正常候选重查。后端截止时间
查询只在 READY 有效,旧入口把暂时返回 0 当作最终跳过。保存首次 READY 时
后端提供的真实截止时间;在该时间内等待 CONNECTED 恢复,随后重新让后端验证
是否可探测。不在 CONNECTED 开池,不从恢复时间重新计算窗口;新增真实 TURN
的 `auto-recheck` 夹具,覆盖两个角色、原截止不变和单次 DTLS/认证中继媒体。
- 第 10 节 B 组原“固定角色”明确为**固定自动判定规则**,与第 4 节生产禁用强制
角色保持一致。受控夹具仍可固定角色;真实实验记录观测/角色变化及跳过,不把
它们隐藏为成功或擅自在生产中绕过适用条件。
### 16.2 验收边界与下一步
已验证 DRAINING 后真实 STUN 重传仍能提名并传输、认证 CANCEL 保留 relay、
TURN 内层同形载荷不会产生直接命中,以及选中 socket 的实际存活和配额保持。
Windows 64 端点百次压力测试在单一窗口到期后拒绝分配;工具通过多个新连接分批,
最终两角色合计 20/20,各累计 100 次分配/停止/端口重绑定,没有扩大 ICE 窗口。
分批首轮 15/20 暴露开池前基线只按发送次数判断的问题;改为原截止前等待双方
各实际收到至少 10 个认证 SRTP 包后才开池。所有初次失败和修正理由保存在报告中,
不追溯认定所有历史媒体缺包原因相同。
真实异网 C 组已有两个成功会话完成双端日志、命中 handle、标准提名和物理抓包
关联;Windows 的 handle 38/44 分别对应命中本地端口 54058/51630,随后持续直连。
成功停止探测后额外 socket 计数各为 1,第三次未命中超时后归零;两端每次均只有
一次 DTLS 握手。完整技术证据及限制见所链接的验证记录。
14:18–14:22 的后续故障实验另记录五个 generation:一轮增强直连、四轮中继。
增强直连在约 10.3 秒入站 UDP 中断后沿用命中 socket 恢复,两轮中继在约 29.9/
17.2 秒中断后恢复;每次均单次 DTLS,并完成连接清理及后续新 generation 建连。
包含 DRAINING 未命中时取消、探测超时及 HELLO 超时;该轮增强成功的仓库 wire
核验 1/1 通过。用户补充确认 clumsy 丢包率为 100%;过滤范围、方向、启停时间及
真实网卡切换尚未确认,不将观察到的间隙当作精确配置时长或物理接口变化通过,
也不并入正式 A/B/C 分母,详见验证记录。
UU 与 CrossDesk 的端口发现及通道复用差异,按
[双端对照测试](p2p-uu-comparison-test-plan.md) 先做 8 次诊断连接。进程重启与保留
进程重连分开记录,不把短暂静默当作 NAT 映射清空,也不混入正式 P5 成功率。
用户在 2026-09-14 明确取消画面、声音、控制和切换质量验收;这些项目免验,
不再要求人工回归或质量指标,不追溯标记为已测试。后续用同一版本、同一信令与
STUN/TURN 配置交错执行 A/B/C,核对有效观测、双端物理报文、标准提名、同一命中
socket、同一 DTLS/SRTP 会话和持续应用数据。至少每组 30 次,保留全部失败/缺测,
并完成真实接口变化、重连及连接兼容性检查。在真实收益结论完成前保持默认关闭。
### 16.3 离散递增与数量调整(2026-09-14,U01/X01 之后)
按用户要求,将 anchor 的随机无重复目标集合改为按端口数值递增发送,默认目标
512 → 1024、pool 64 → 128、每端 PROBE/ACK 速率 100 → 128 PPS。先完成随机
抽样再排序,保留原来的全范围抽样分布及候选排除;pool 本地端口继续由系统分配。
源码确认 runtime 每 10 ms 最多处理 8 个包,因此 128 PPS 不受“一次回调一个包”
限制。单轮无 ACK、无争用的理想调度约 8 秒可完成 1024 个目标;100 PPS 在原
10 秒窗口内不能保证覆盖这些目标,因而配套调整速率,没有延长探索或 ICE 截止。
保留 1024 次 PROBE/ACK 合计预算、8 包突发、200 PPS 进程限速和最多 256 个额外
socket。命中、ACK、发送失败、同进程会话争用、原窗口剩余时间不足都可能减少
实际新目标数量,1024 是计划上限,不承诺每次发完。双方使用 HELLO 逐项取较小
预算,旧参数对端仍收敛到 64/512/100;没有增强能力的旧端保持普通 ICE/中继。
本次不改变后端 ABI、仓库 libnice 补丁、选路、命中 socket 接入和 DTLS 生命周期。
这是一组新的实验配置,历史 U01/X01 结论及参数不改写。需要双方重新构建并重启
才能使用完整新参数;旧 P5 manifest 保留显式参数,新批次应重新生成 manifest。
数量增加及发送顺序本身不构成真实 NAT 成功率提高的证明。
本机 Mac 完整构建、5,430 个单元断言、Python 工具 10/10、三种模式兼容性及
各 36 个传输层 SDP 组合通过;128 端点 runtime 8/8、DRAINING 2/2 通过。
这些是本机回环受控网络结果,Windows 新配置及真实异网尚未运行;详情见
[本次回归记录](../deps/submodules/minirtc/tests/ice/lab/punch-runtime-validation-2026-09-14.md#离散递增及扩大默认数量2026-09-14u01x01-之后)。