# CrossDesk 双端 UDP 打洞增强实现方案 状态:P0 门槛已通过,P1–P4 原型、受控运行与跨平台构建已实现。按用户最新要求,原生桌面默认且仅提供 pool-wide,CrossDesk 本机 SRTP 固定开启;无指纹旧端保留原有兼容回退。真实异网已有增强直连证据,默认调整不代表正式收益验收通过。按用户要求,画面、声音、控制和切换质量不纳入本次验收。日期: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. **默认 pool-wide。** 原生桌面包含扩展、无公开模式选择;双方声明支持、加密认证、网络和连接状态满足条件时才启动。未满足条件仍使用原 ICE/中继。iOS/WebRTC 不启用。 这会使第一版主要改善“已通过中继连接,但希望随后升为 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,也不重启轮次。乱序消息只能在对应状态允许的范围内缓存或拒绝,不能越级启动。 HELLO 的重发额度耗尽只停止主动发送,在原升级截止内继续接收认证控制消息, 允许仍在正常 ICE 重查中的对端随后开始协商。此等待不分配端口池、不刷新快照 或重放窗口、不延长任何截止;PREPARE/READY/START 的重发耗尽仍结束协商。 取消、过期的同 generation 不能被迟到 HELLO 恢复。原因及复现见 16.4。 每方向、每密钥域保留 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 原节拍,但需要记录它们带来的额外事务和重传。媒体包不计入探测预算。 当前原生桌面配置仅读取一次,固定使用 pool-wide,不增加公开 `Params` 布局或 GUI 模式开关。原 `MINIRTC_UDP_PUNCH` 模式变量不再读取,公开编译开关已移除。端口池大小等诊断调参入口仍由同一校验函数处理;无效预算禁用增强并打印原因,不绕过资源上限。 `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、兼容性及资源生命周期检查。 原计划将收益验收作为默认启用门槛;用户现明确要求默认且仅用 pool-wide,因此调整默认策略。网络适用范围、认证、原截止和资源预算不变;历史 A/B 记录保留,新程序不再提供 A/B 模式,详见 16.5。 ## 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 代码修订这一处设计并给出替代路径,再继续后续阶段;不要把未完成的直接验证改成强制选路或延长超时。 本方案原始执行目标是默认关闭的实验原型;当前默认策略按用户要求改为 pool-wide,见 16.5。扩大网络适用范围及首次连接阶段增强不属于这次默认调整。 ## 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-之后)。 ### 16.4 HELLO 启动错位与映射地址碰撞修复(2026-09-14) 15:50 批次日志暴露两个边界:一端的合法 ICE 重查使 HELLO 晚于另一端约 2.2 秒 的重发结束时间;另一个会话已提升 pool socket,却在 Windows 被归为 ordinary-ice 并关闭全部额外 socket。不能把这些现象直接算成探测端口不足或纯日志误判。 源码与本机最小网络实验确认:旧协商器把 HELLO 重发耗尽当作整体超时;libnice 在处理真实 STUN 响应的 XOR-MAPPED-ADDRESS 时,只比较 UDP 传输类型和映射地址, 可能复用另一 socket 的候选,从而将命中检查关联到旧 socket。受控复现中两种 角色都选错并失去直连传输,和历史日志中的不一致有相同机制;未对历史进程的 内存状态作追溯证明。 修复保留原 HELLO 发送次数,耗尽后被动接收至原截止,由迟到对端的认证 HELLO 推进原协议;实际采样新鲜度和剩余时间仍在准备阶段重新检查。libnice 新补丁 `punch_socket_identity_0.1.24.patch` 要求普通 UDP 的反射候选同时匹配实际 socket, 并让提升候选的反射别名保留内部候选标记。TCP/TURN 的原兼容性判定不变。 依赖配方校验补丁 SHA,`punch_v1_revision` 从 2 升至 3,使旧缓存失效;ABI 仍为 2。 选路日志增加实际 GSocket 的 bound 和对应 handle,不能以公网四元组代替 socket。 新增 late-hello 与 mapped-collision 回归,先在旧实现上验证失败,再检验真实 TURN → 标准 ICE → 同一命中 socket 上持续 SRTP。默认开关、128/1024/128 PPS 实验参数、30 秒升级截止及 DTLS 生命周期不变。结果和 Windows 操作见 [修复验证记录](../deps/submodules/minirtc/tests/ice/lab/punch-runtime-validation-2026-09-14.md#hello-启动错位与映射地址碰撞修复2026-09-14)。 ### 16.5 默认 pool-wide 与本机 SRTP 固定开启(2026-09-14) 按用户要求,原生桌面不再提供 off/wide-only/pool-wide 三选一入口,也移除公开 构建开关。读取进程配置时直接选择 pool-wide;遗留模式环境变量不再生效,诊断 TRACE 与有界预算调参仍保留。内部 Off 是不支持平台/无效参数状态,WideOnly 仅保留 v1 协议及 P0/单端点夹具用途,不是产品选项。 CrossDesk 移除 Slint 和旧 ImGui 设置中的 SRTP 控件、对应 setter 与可变 UI 状态。 INI 中旧 false/缺失/无效 SRTP 值迁移为 true,其他配置保留;新建连接始终请求 SRTP。用户对旧版兼容提出疑问,因此保留无 DTLS 指纹旧端的原明文回退:这是 默认请求加密,不是拒绝所有明文旧端。该回退不能启动增强打洞。原来只根据本地 设置显示的加密盾牌被移除,避免在旧端回退时误报已加密。 没有改变候选/认证门槛、128 个端点、1024 个目标、128 PPS、30 秒总窗口, 没有强制选路或重启 DTLS。旧端没有增强能力时继续普通 ICE/中继;iOS 和 WebRTC 不启用此增强。默认启用是用户选择,不能从中推导所有 NAT 都能直连。 实验工具新计划只生成 C/pool-wide,拒绝启动旧 A/B 条目;历史结果仍可读取汇总。 同机自动入口、旧端能力与无指纹兼容、SRTP 配置迁移及真实 TURN/DTLS/SRTP 验证详见 [本次记录](../deps/submodules/minirtc/tests/ice/lab/punch-runtime-validation-2026-09-14.md#默认-pool-wide-与本机-srtp-固定开启2026-09-14)。