Compare commits

...
20 changed files with 1186 additions and 92 deletions
+7 -4
View File
@@ -51,7 +51,7 @@ CrossDesk 支持 Windows、macOS、Linux 之间的远程访问,也可从浏览
| 实时画面与声音 | H.264 / AV1、30 / 60 fps 设置、硬件编解码选项、远端声音播放;实际能力取决于设备与构建配置 |
| 多设备与多显示器 | 近期连接、设备别名、会话标签与远端显示器切换 |
| 输入与协作 | 键鼠输入、远端光标同步、组合键、文本剪贴板同步、文件传输 |
| 网络与部署 | P2P 直连、TURN 中继、SRTP 加密选项、自托管信令与中继服务 |
| 网络与部署 | P2P 直连与默认增强打洞、TURN 中继、默认 SRTP、自托管信令与中继服务 |
| Windows 锁屏控制 | 通过 CrossDesk Service 支持锁屏、登录界面及安全桌面的输入转发 |
各客户端的入口和能力有所不同;桌面会话操作见下文,iOS 的具体功能见 [开发说明](apps/ios/README.md)。
@@ -93,7 +93,7 @@ sudo apt install "./crossdesk-linux-amd64-<version>.deb"
### 从浏览器连接
1. 保持被控电脑上的 CrossDesk 运行,并确认已连接服务器;在被控端“设置”中保持 **启用 SRTP** 开启。
1. 保持被控电脑上的 CrossDesk 运行,并确认已连接服务器;新版本默认请求 SRTP,无需在设置中开启。
2. 打开 [Web 客户端](https://web.crossdesk.cn/),输入 **远程设备 ID** 和 **密码**,点击“连接”。
3. 连接后使用页面提供的显示器、鼠标模式和键盘控件操作远端;手机和平板也可通过浏览器访问。
@@ -125,16 +125,19 @@ sudo apt install "./crossdesk-linux-amd64-<version>.deb"
点击主窗口右上角 **☰ → 设置**,修改后点击“确认”保存。建议在建立连接前调整;会话进行中部分选项不可修改。设置列表可向下滚动,查看自托管、启动方式与文件保存路径。
<p align="center">
<img src="docs/images/desktop-settings-zh.png" width="720" alt="当前设置界面:画质、帧率、画面偏好、编解码、中继与 SRTP" />
<img src="docs/images/desktop-settings-zh.png" width="720" alt="设置界面参考:画质、帧率、画面偏好、编解码与中继;旧图中的 SRTP 开关已移除" />
</p>
SRTP 已默认开启,参考图中的旧 SRTP 开关不再提供。原生桌面默认使用 pool-wide 增强打洞;只有双方支持且认证、映射条件满足时才启动。无 DTLS 指纹旧端仍可兼容连接,但可能回退到未加密 RTP,此类连接不启用增强打洞。
| 设置 | 说明 |
| --- | --- |
| 画面质量 / 帧率 | 选择低、中、高画质与 30 / 60 fps;实际帧率取决于网络和设备 |
| 画面偏好 | 在帧率优先、画质优先、平衡之间选择 |
| 视频编码格式 | H.264 / AV1;硬件编解码是否可用取决于平台、硬件与构建选项 |
| 启用中继服务 | 允许借助 TURN 中继建立连接;P2P 连接失败时先检查此项 |
| 启用 SRTP | 媒体传输加密选项,两端配置需兼容 |
| SRTP | 默认请求媒体加密,无本地关闭开关;无指纹旧端保留兼容回退 |
| 自托管配置 | 设置服务器地址、信令端口与中继端口,并勾选启用 |
| 开机自启 / 启用守护进程 | 调整启动与进程守护行为,按界面提示重启后生效 |
| 文件保存路径 | 选择接收文件的目录 |
+7 -4
View File
@@ -51,7 +51,7 @@ See the [screenshot notes](docs/images/README.md) for image sources and refresh
| Live video and audio | H.264 / AV1, 30 / 60 fps settings, hardware codec options, and remote audio; availability depends on hardware and build configuration |
| Devices and displays | Recent connections, device aliases, session tabs, and remote display switching |
| Input and sharing | Keyboard/mouse input, remote cursor synchronization, shortcuts, text clipboard synchronization, and file transfer |
| Networking and deployment | Direct P2P connections, TURN relay, an SRTP encryption option, and self-hosted signaling/relay services |
| Networking and deployment | Direct P2P with hole punching enabled by default, TURN relay, SRTP by default, and self-hosted signaling/relay services |
| Windows protected desktops | CrossDesk Service forwards input on lock screens, sign-in screens, and secure desktops |
Controls and capabilities vary by client. Desktop controls are described below; see the [iOS guide](apps/ios/README.md) for native mobile features.
@@ -93,7 +93,7 @@ The eye button shows or hides the local password; the pencil button changes it.
### Connect from a browser
1. Keep CrossDesk running and connected to the server on the host computer, with **Enable SRTP** enabled in its Settings.
1. Keep CrossDesk running and connected to the server on the host computer; current versions request SRTP by default, with no Settings switch.
2. Open the [Web Client](https://web.crossdesk.cn/), enter the **Remote Device ID** and **Password**, and click **Connect**.
3. Use the page's display, mouse-mode, and keyboard controls during the session. Phone and tablet browsers can also connect.
@@ -125,16 +125,19 @@ Desktop clients synchronize text clipboard contents. Configure the receiving dir
Open **☰ → Settings** in the top-right corner and click **OK** to save. Configure the session before connecting; some settings are disabled during an active session. Scroll down for self-hosting, startup behavior, and the file save path.
<p align="center">
<img src="docs/images/desktop-settings-en.png" width="720" alt="Current settings for quality, frame rate, video preference, codecs, relay, and SRTP" />
<img src="docs/images/desktop-settings-en.png" width="720" alt="Settings reference for quality, frame rate, video preference, codecs, and relay; the old SRTP switch has been removed" />
</p>
SRTP is enabled by default; the SRTP switch in this reference image is no longer available. Native desktop builds use pool-wide punching when both peers support it and authentication and mapping conditions are met. Legacy peers without DTLS fingerprints remain compatible through a possible unencrypted RTP fallback, which cannot use enhanced punching.
| Setting | Purpose |
| --- | --- |
| Video Quality / Video Capture Frame Rate | Low, medium, or high quality and 30 / 60 fps; actual performance depends on the connection and device |
| Video Preference | Choose frame-rate priority, quality priority, or balanced adaptation |
| Video Encode Format | H.264 / AV1; hardware codec availability depends on the platform, device, and build options |
| Enable TURN Service | Allow relay-assisted connections; check this setting if P2P fails |
| SRTP | Media encryption option; configurations on both ends must be compatible |
| SRTP | Requests media encryption by default, with no local off switch; retains fallback for legacy peers without fingerprints |
| Self-Hosted Config | Set the host, signaling port, and TURN port, then enable the checkbox |
| Auto Start / Enable Daemon | Configure startup and process supervision; restart as indicated by the UI |
| File Save Path | Choose where received files are saved |
@@ -103,7 +103,12 @@ int ConfigCenter::Load() {
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
persist_config_migration = true;
}
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
// Migrate the former user switch. Keep true in the file for older builds,
// while the current application always requests encrypted native media.
if (!ini_.GetBoolValue(section_, "enable_srtp", false)) {
ini_.SetBoolValue(section_, "enable_srtp", true);
persist_config_migration = true;
}
enable_self_hosted_ =
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
@@ -165,7 +170,7 @@ int ConfigCenter::Save() {
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
ini_.SetBoolValue(section_, "enable_turn",
turn_mode_ != TURN_MODE::DISABLED);
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
ini_.SetBoolValue(section_, "enable_srtp", true);
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
// only save when self hosted
@@ -301,16 +306,6 @@ int ConfigCenter::SetTurn(bool enable_turn) {
return SetTurnMode(turn_mode_);
}
int ConfigCenter::SetSrtp(bool enable_srtp) {
enable_srtp_ = enable_srtp;
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetServerHost(const std::string& signal_server_host) {
signal_server_host_ = signal_server_host;
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
@@ -472,7 +467,7 @@ bool ConfigCenter::IsEnableTurn() const {
return turn_mode_ != TURN_MODE::DISABLED;
}
bool ConfigCenter::IsEnableSrtp() const { return enable_srtp_; }
bool ConfigCenter::IsEnableSrtp() const { return true; }
std::string ConfigCenter::GetSignalServerHost() const {
return signal_server_host_;
@@ -47,7 +47,6 @@ class ConfigCenter {
int SetHardwareVideoCodec(bool hardware_video_codec);
int SetTurnMode(TURN_MODE turn_mode);
int SetTurn(bool enable_turn);
int SetSrtp(bool enable_srtp);
int SetServerHost(const std::string& signal_server_host);
int SetServerPort(int signal_server_port);
int SetSelfHosted(bool enable_self_hosted);
@@ -99,7 +98,6 @@ class ConfigCenter {
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
bool hardware_video_codec_ = false;
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
bool enable_srtp_ = false;
std::string signal_server_host_ = "";
std::string signal_server_host_default_ = "api.crossdesk.cn";
int signal_server_port_ = 0;
@@ -1230,7 +1230,6 @@ void GuiApplication::ResetSettingsUi() {
ui_->main->set_hardware_codec_enabled(hardware_codec_available &&
enable_hardware_video_codec_);
ui_->main->set_turn_enabled(enable_turn_);
ui_->main->set_srtp_enabled(enable_srtp_);
ui_->main->set_self_hosted_enabled(enable_self_hosted_);
ui_->main->set_autostart_enabled(enable_autostart_);
ui_->main->set_daemon_enabled(enable_daemon_);
@@ -2419,7 +2418,6 @@ void GuiApplication::SyncStreamWindow() {
return;
}
(*ui_->stream)->set_window_maximized((*ui_->stream)->window().is_maximized());
(*ui_->stream)->set_srtp_enabled(enable_srtp_);
#if defined(__APPLE__)
if (ui_->stream_live_resize_configuration_attempts > 0) {
if (ConfigureStreamWindowLiveResize(
@@ -2987,7 +2985,6 @@ void GuiApplication::SaveSettingsFromUi() {
enable_hardware_video_codec_ = ConfigCenter::IsHardwareVideoCodecAvailable() &&
main->get_hardware_codec_enabled();
enable_turn_ = main->get_turn_enabled();
enable_srtp_ = main->get_srtp_enabled();
enable_self_hosted_ = main->get_self_hosted_enabled();
enable_autostart_ = main->get_autostart_enabled();
enable_daemon_ = main->get_daemon_enabled();
@@ -3010,7 +3007,6 @@ void GuiApplication::SaveSettingsFromUi() {
video_encode_format_button_value_));
config_center_->SetHardwareVideoCodec(enable_hardware_video_codec_);
config_center_->SetTurn(enable_turn_);
config_center_->SetSrtp(enable_srtp_);
config_center_->SetSelfHosted(enable_self_hosted_);
config_center_->SetAutostart(enable_autostart_);
config_center_->SetDaemon(enable_daemon_);
@@ -3056,7 +3052,6 @@ void GuiApplication::SaveSettingsFromUi() {
video_encode_format_button_value_last_ = video_encode_format_button_value_;
enable_hardware_video_codec_last_ = enable_hardware_video_codec_;
enable_turn_last_ = enable_turn_;
enable_srtp_last_ = enable_srtp_;
enable_self_hosted_last_ = enable_self_hosted_;
enable_autostart_last_ = enable_autostart_;
enable_daemon_last_ = enable_daemon_;
@@ -53,8 +53,6 @@
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_AUTOSTART_PADDING_CN 171 * dpi_scale_
@@ -57,8 +57,6 @@
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218
#define ENABLE_AUTOSTART_PADDING_CN 171
@@ -149,7 +149,6 @@ struct TranslationRow {
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
X(enable_turn, u8"启用中继服务:", \
"Enable TURN Service:", u8"Включить TURN-сервис:") \
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
u8"Конфигурация self-hosted") \
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
@@ -308,7 +308,6 @@ int SettingsManager::Load() {
owner_.enable_hardware_video_codec_ =
owner_.config_center_->IsHardwareVideoCodec();
owner_.enable_turn_ = owner_.config_center_->IsEnableTurn();
owner_.enable_srtp_ = owner_.config_center_->IsEnableSrtp();
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart();
owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon();
@@ -335,7 +334,6 @@ int SettingsManager::Load() {
owner_.enable_hardware_video_codec_last_ =
owner_.enable_hardware_video_codec_;
owner_.enable_turn_last_ = owner_.enable_turn_;
owner_.enable_srtp_last_ = owner_.enable_srtp_;
owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_;
owner_.enable_autostart_last_ = owner_.enable_autostart_;
@@ -136,7 +136,6 @@ struct UserSettingsState {
int video_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = true;
bool enable_turn_ = true;
bool enable_srtp_ = false;
char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099";
bool enable_self_hosted_ = false;
@@ -147,7 +146,6 @@ struct UserSettingsState {
int video_encode_format_button_value_last_ = 0;
bool enable_hardware_video_codec_last_ = false;
bool enable_turn_last_ = true;
bool enable_srtp_last_ = false;
bool enable_self_hosted_last_ = false;
bool enable_autostart_ = false;
bool enable_autostart_last_ = false;
@@ -48,7 +48,6 @@ export global UiStrings {
in-out property <string> codec: "Video codec";
in-out property <string> hardware-codec: "Hardware video codec";
in-out property <string> turn-relay: "TURN relay";
in-out property <string> srtp: "SRTP encryption";
in-out property <string> self-hosted: "Self-hosted server";
in-out property <string> autostart: "Start with system";
in-out property <string> daemon: "Run in background";
@@ -247,7 +246,6 @@ export component MainWindow inherits Window {
in-out property <int> codec-index: 0;
in-out property <bool> hardware-codec-enabled: true;
in-out property <bool> turn-enabled: true;
in-out property <bool> srtp-enabled: false;
in-out property <bool> self-hosted-enabled: false;
in-out property <bool> autostart-enabled: false;
in-out property <bool> daemon-enabled: false;
@@ -1488,11 +1486,6 @@ export component MainWindow inherits Window {
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.privacy-on-connect; color: root.privacy-setting-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
+1 -14
View File
@@ -160,7 +160,6 @@ export component StreamWindow inherits Window {
in property <bool> remote-cursor-active: false;
in property <int> remote-cursor-shape: 0;
in property <bool> audio-enabled: true;
in property <bool> srtp-enabled: false;
in property <bool> fullscreen-enabled: false;
in property <bool> stats-visible: false;
in property <[NetworkStatsRow]> stats-rows;
@@ -506,20 +505,8 @@ export component StreamWindow inherits Window {
width: parent.width - 27px;
height: parent.height;
background: transparent;
if root.srtp-enabled: Text {
x: 0px;
width: 12px;
height: parent.height;
text: FontAwesomeIcons.shield-halved;
color: #30343b;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: ImGuiFontStyle.base;
horizontal-alignment: center;
vertical-alignment: center;
}
Text {
x: root.srtp-enabled ? 16px : 0px;
x: 0px;
width: parent.width - self.x;
height: parent.height;
text: tab.title;
@@ -69,7 +69,6 @@ inline int ApplyMainWindowStrings(
strings.set_hardware_codec(
Text(localization::enable_hardware_video_codec[language]));
strings.set_turn_relay(Text(localization::enable_turn[language]));
strings.set_srtp(Text(localization::enable_srtp[language]));
strings.set_self_hosted(
Text(localization::self_hosted_server_config[language]));
strings.set_autostart(Text(localization::enable_autostart[language]));
@@ -268,25 +268,6 @@ int GuiApplication::SettingWindow() {
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::enable_srtp[localization_language_index_].c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
}
ImGui::Checkbox("##enable_srtp", &enable_srtp_);
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 1);
@@ -569,14 +550,6 @@ int GuiApplication::SettingWindow() {
}
enable_turn_last_ = enable_turn_;
// SRTP
if (enable_srtp_) {
config_center_->SetSrtp(true);
} else {
config_center_->SetSrtp(false);
}
enable_srtp_last_ = enable_srtp_;
if (enable_self_hosted_) {
config_center_->SetSelfHosted(true);
} else {
@@ -136,10 +136,7 @@ int GuiApplication::StreamWindow() {
continue;
}
std::string tab_label =
enable_srtp_
? std::string(ICON_FA_SHIELD_HALVED) + " " + props->remote_id_
: props->remote_id_;
const std::string& tab_label = props->remote_id_;
if (ImGui::BeginTabItem(tab_label.c_str(), &props->tab_opened_)) {
props->tab_selected_ = true;
ImGui::SetWindowFontScale(0.6f);
@@ -308,4 +305,4 @@ int GuiApplication::StreamWindow() {
return 0;
}
} // namespace crossdesk
} // namespace crossdesk
@@ -0,0 +1,50 @@
#include "config_center.h"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdexcept>
using crossdesk::ConfigCenter;
static void Check(bool result, const char* message) {
if (!result) throw std::runtime_error(message);
}
int main(int argc, char** argv) {
try {
Check(argc == 2, "provide a new isolated output directory");
const std::filesystem::path root(argv[1]);
Check(std::filesystem::create_directory(root), "output directory must be new");
unsigned cases = 0;
for (const auto* setting : {"new", "missing", "false", "true", "invalid"}) {
const auto path = root / (std::string(setting) + ".ini");
if (std::string(setting) != "new") {
std::ofstream file(path);
file << "[Settings]\nlanguage=2\nturn_mode=2\nfile_transfer_save_path=/saved-path\n";
if (std::string(setting) != "missing") file << "enable_srtp=" << setting << '\n';
}
ConfigCenter config(path.string());
Check(config.IsEnableSrtp(), "fresh and legacy settings must request SRTP");
if (std::string(setting) != "new") {
Check(config.GetLanguage() == ConfigCenter::LANGUAGE::RUSSIAN &&
config.GetTurnMode() == ConfigCenter::TURN_MODE::FORCE_UDP &&
config.GetFileTransferSavePath() == "/saved-path",
"SRTP migration preserves unrelated preferences");
}
CSimpleIniA saved;
Check(saved.LoadFile(path.string().c_str()) >= 0 &&
saved.GetBoolValue("Settings", "enable_srtp", false),
"SRTP migration persists before any later settings save");
Check(config.Save() == 0 && config.Load() == 0 && config.IsEnableSrtp(),
"settings save/reload cannot disable SRTP");
ConfigCenter reopened(path.string());
Check(reopened.IsEnableSrtp(), "SRTP survives restart");
++cases;
}
std::cout << "PASS SRTP configuration migration cases=" << cases << '\n';
} catch (const std::exception& error) {
std::cerr << "FAIL: " << error.what() << '\n';
return 1;
}
}
+6
View File
@@ -339,6 +339,12 @@ function setup_targets()
add_includedirs("apps/desktop/src", "apps/desktop/src/config_center",
{public = true})
target("config_center_srtp_test")
set_kind("binary")
set_default(false)
add_deps("config_center")
add_files("apps/desktop/tests/config_center_srtp_test.cpp")
target("assets")
set_kind("headeronly")
add_includedirs("apps/desktop/src/gui/assets/localization",
+801
View File
@@ -0,0 +1,801 @@
# 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)。
+303
View File
@@ -0,0 +1,303 @@
# UU 与 CrossDesk 直连差异:双端对照测试
日期:2026-09-14。状态:U01 和 X01 已收到部分双端证据并完成机制分析;尚未形成
符合原始固定条件的完整首次连接/重连对照。X02/U02 待执行。用户确认必须保留 UU
管理连接来操作对端,后续按第 9 节调整条件,不要求断开唯一操作通道。
目标是定位 UU 在当前网络中更容易直连的原因:端口选择、双端发包时序、失败重试,
还是重连时保留了可用通道。本轮先做 8 次连接;只检查连接路径与协议证据,画面、
声音、控制和切换质量按用户要求免验。它不是正式成功率评估,不替代原方案 P5。
## 1. 固定条件
- 使用此前异网组合:Mac 本机与 Windows `10.15.73.245` 所在网络;不要换成同
局域网的 Windows。每批保存实际地址、网关、网卡,地址变化后按新环境另记。
- 统一由 Mac 控制 Windows;固定软件版本,CrossDesk 使用同一套编译产物和
STUN/TURN 配置。UU 使用自己的服务,记录抓包可见的服务器,不假设服务相同。
- clumsy 点击 Stop 并退出;本轮不注入丢包、断网或带宽限制。两端关闭代理/TUN/
VPN 后保持不变;如不能关闭,标记为另一种网络条件,不混合比较。
- 每批只测试一种软件,另一种软件退出;暂停其他远控、下载。Windows 需由本地
人员操作,不能依赖即将退出的 UU 会话完成进程重启。这是原始隔离条件;若依赖
UU 管理通道操作对端,按第 9 节执行,不要求终止该通道,不声称满足完全隔离。
- CrossDesk 双端默认请求 SRTP,运行默认 `pool-wide` 并设置 `TRACE=1`;每批固定实验参数。
原 U01/X01 批次为 64 个端点、512 个随机顺序目标、100 PPS;后续按第 10 节
使用 128 个端点、1024 个离散递增目标、128 PPS,两批均为 10 秒探索与 3 秒
检查收尾。记录实际协商参数,禁止把不同版本与预算混成同一批结果。
- 保留系统已有时间设置,记录带时区时间;分析时通过同一报文关联两端,不能直接
相减 Mac/Windows 的墙钟得到单程延迟。
## 2. 批次与“冷启动”的含义
| 顺序 | 批次 | 软件 | 同一抓包中的两次连接 |
| --- | --- | --- | --- |
| 1 | U01 | UU | 进程重启后首次连接 → 保留进程重连 |
| 2 | X01 | CrossDesk | 进程重启后首次连接 → 保留进程重连 |
| 3 | X02 | CrossDesk | 进程重启后首次连接 → 保留进程重连 |
| 4 | U02 | UU | 进程重启后首次连接 → 保留进程重连 |
这四批合计 8 次连接,每种软件首次连接 2 次、保留进程重连 2 次。顺序反转用于
减少先后偏差,数量只够诊断线索;若结果混杂,再追加完整对照块,保留全部失败。
本轮用 U/X 命名,不占用原方案 A/off、B/wide-only、C/pool-wide 的分组含义。
“进程重启”要求两端真正退出相关前台和网络后台进程,重新启动后确认 PID。
关闭窗口不一定退出后台。如果 UU 的网络服务仍运行、无法通过正常方式退出,
将该批标为“前台重启、后台常驻”,不要当作完全进程重启,不盲目终止未知服务。
即使进程全部退出并安静 30 秒,也不能保证路由器/运营商 NAT 映射已经过期,
因此本轮不声称是“NAT 状态完全清空的冷启动”。不为此重启路由器或改变公网 IP。
若首次连接失败,第二次应标为“保留进程重试”,不能宣称复用了已成功的直连。
## 3. 每批固定操作
一批一对抓包,覆盖两次连接及中间空闲期,不在断开时停止抓包。
1. 两端先启动本批抓包;退出相关应用并核对后台/PID/端口,记录 `process_exit`。
2. 保持两款软件不发测试流量至少 30 秒,记录 `quiet_end`。若后台仍发包如实记录。
3. 只启动本批软件,记录 `app_start`,保存版本、PID、端口快照。
4. Mac 点击连接前记录 `first_connect_click`;从点击起观察 60 秒。无论最终为
直连、中继、失败或仍连接中,都保留结果,不仅留下成功样本。
5. 记录 `first_disconnect` 并断开远控会话,保持软件和后台运行 30 秒;在第
15 秒保存端口快照,观察是否仍有原直连 tuple 的保活或数据报。
6. 不重启进程,记录 `reconnect_click`,再次连接同一对端并观察 60 秒。
7. 记录 `second_disconnect` 并断开,继续抓包 15 秒,保存最后端口快照;随后
停止抓包、保存计数与日志,再正常退出软件。
每次观察期间保持有桌面数据传输;无需人工评价质量。记录应用显示的直连/中继
及其变化时间,但最终路径以双端物理抓包核验为准。长时间仍在连接中应单独分类。
先完成 U01/X01 四次连接即可检查取证是否齐全,再继续 X02/U02。
## 4. 抓包命令
本轮采集 UDP、TCP、ICMP 和 IPv6 ICMP,避免遗漏 UU 可能选择的其他传输路径。
不沿用历史公网端口过滤器。两端都抓当前实际联网的物理接口;每批使用新目录。
### Mac:抓包终端
当前本机 `en1` 为 `192.168.31.125`;先用 `ifconfig en1` 核对。每批修改 U01
为对应批次,复制终端打印的实际目录,避免把后续文件误认为仍在上一次路径。
```sh
umask 077
punch_batch=U01
punch_run_dir="$(mktemp -d "/private/tmp/crossdesk-uu-${punch_batch}.XXXXXX")"
printf '本批目录:%s\n' "$punch_run_dir"
sudo tcpdump -i en1 -nn -s 0 -U -Z dijunkun \
-w "$punch_run_dir/mac.pcap" \
'udp or tcp or icmp or icmp6' 2>&1 | tee -i "$punch_run_dir/mac-capture.log"
```
完成整批后 Ctrl+C;等待 tcpdump 输出 captured/received/dropped 统计并返回
提示符。`tee -i` 让 tee 不先被 Ctrl+C 中断,便于保存最终统计;仍应检查日志。
### Windows:管理员 PowerShell 抓包终端
```powershell
$punchDumpcap = 'C:\Program Files\Wireshark\dumpcap.exe'
& $punchDumpcap -D
$punchNic = Read-Host '输入上方实际联网的以太网接口编号'
$punchBatch = 'U01'
$punchRunDir = Join-Path $env:USERPROFILE ("Desktop\CrossDesk-UU-Compare\{0}-{1}" -f $punchBatch, (Get-Date -Format 'yyyyMMdd-HHmmss-fff'))
if (Test-Path $punchRunDir) { throw '本批目录已存在,请换新目录' }
New-Item -ItemType Directory -Path $punchRunDir -ErrorAction Stop | Out-Null
Write-Host "本批目录:$punchRunDir"
& $punchDumpcap -i $punchNic -s 0 -B 16 -f 'udp or tcp or icmp or icmp6' -w (Join-Path $punchRunDir 'windows.pcapng') 2> (Join-Path $punchRunDir 'windows-capture.log')
```
另开终端查看 `windows-capture.log` 确认成功启动;完成整批后 Ctrl+C,等待文件
关闭。保存 pcapng 接口统计和日志;统计缺失记为缺测,不填零。`-D` 用于列接口,
`-i` 选择接口,dumpcap 默认输出 pcapng,详见 [Wireshark 官方说明](https://www.wireshark.org/docs/man-pages/dumpcap.html)。
## 5. 操作时间、端口和版本
在各端另开一个记录终端,把目录变量设置为本批抓包终端打印的实际目录。每个
关键操作前记录时间,事件名采用第 3 节的名称;如点击晚于记录,应说明大致延迟。
Mac 示例(将路径替换为真实值,后续事件只改最后一行的事件名):
```sh
punch_run_dir='/private/tmp/替换为本批实际目录'
date '+%Y-%m-%dT%H:%M:%S%z first_connect_click' >> "$punch_run_dir/events-mac.txt"
```
在退出后、启动后、首次连接中、断开后第 15 秒、重连中及最终断开后分别执行快照;
阶段名依次用 `after_exit`、`after_start`、`first_connected`、`idle_15s`、
`reconnected`、`after_disconnect`。替换 `punch_stage`,不要覆盖同名快照:
```sh
punch_stage=after_start
date '+%Y-%m-%dT%H:%M:%S%z' > "$punch_run_dir/$punch_stage-time.txt"
ps -axo pid,ppid,comm > "$punch_run_dir/$punch_stage-processes.txt"
sudo lsof -nP +c 0 -iUDP -iTCP > "$punch_run_dir/$punch_stage-sockets.txt"
```
Windows 记录终端:
```powershell
$punchRunDir = '替换为本批实际目录'
"$(Get-Date -Format o) first_connect_click" | Add-Content (Join-Path $punchRunDir 'events-windows.txt')
$punchStage = 'after_start'
Get-Date -Format o | Set-Content (Join-Path $punchRunDir "$punchStage-time.txt")
Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId, Name, ExecutablePath | Export-Csv (Join-Path $punchRunDir "$punchStage-processes.csv") -NoTypeInformation -Encoding UTF8
Get-NetUDPEndpoint | Select-Object LocalAddress, LocalPort, OwningProcess, CreationTime | Export-Csv (Join-Path $punchRunDir "$punchStage-udp.csv") -NoTypeInformation -Encoding UTF8
Get-NetTCPConnection | Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, State, OwningProcess | Export-Csv (Join-Path $punchRunDir "$punchStage-tcp.csv") -NoTypeInformation -Encoding UTF8
```
UDP 的 `OwningProcess` 用于关联所属 PID,见 [Microsoft 文档](https://learn.microsoft.com/en-us/powershell/module/nettcpip/get-netudpendpoint?view=windowsserver2025-ps)。
快照不是无遗漏的 socket 跟踪;相同 PID/端口也不能独立证明同一个 OS socket。
结合空闲期持续报文和下一次连接的外层端点,只对实际可见的通道复用作结论。
每批记录两端应用版本;CrossDesk 保存主仓库及 MiniRTC 提交、EXE SHA-256,UU
保存版本和实际网络进程对应可执行文件 SHA-256。Mac 可用 `shasum -a 256 <路径>`,
Windows 可用 `Get-FileHash -Algorithm SHA256 <路径>`;进程快照帮助定位后台程序。
不要导出命令行、账号密码或完整配置文件。日志和抓包放在私有目录,不提交到 Git。
CrossDesk 两端启动命令(X01/X02):
```sh
cd /Users/dijunkun/crossdesk
MINIRTC_UDP_PUNCH_TRACE=1 xmake r crossdesk
```
```powershell
Set-Location E:\SourceCode\crossdesk
$env:MINIRTC_UDP_PUNCH_TRACE = '1'
.\build\windows\x64\debug\crossdesk.exe
```
如果之前设置过其他 `MINIRTC_UDP_PUNCH_*` 调参变量,先只清除这一前缀的实验
变量,再设置模式和 TRACE;两端保持相同默认参数。旧版 Windows 不用于这组对照。
运行目录如与示例不同,以本轮已核对的构建位置为准;测试中不重编译或换版本。
## 6. 交付材料与判定
每批提交一份 Mac 抓包、一份 Windows 抓包、操作时间和上述快照。X 批次另附
两端 MiniRTC 日志;Mac 通常在 `~/Library/Logs/CrossDesk`,Windows 通常在
`%LOCALAPPDATA%\CrossDesk\logs`,便携构建也检查程序目录的 logs。按时间匹配,
不要把前一次日志拼入本轮;UU 若可正常导出连接诊断日志则一并提供,没有也记录。
逐次记录:首次/重连、前次路径、后台是否退出、连接开始时刻、应用显示路径、
最终抓包确认路径、首次直接探测、首个直接接收、首次持续直连数据、端口数、
目标端口集合与顺序、每端口重试次数、失败/跳过原因,以及抓包与版本缺测项。
分析采用以下规则:
- 通过双端地址转换前后的同一载荷、时序及所属进程核对真实直连。仅 UI 标记、
低 RTT、单个短包或与某服务器交换数据,均不足以证明直连。空闲保活不能当作
新远控会话的数据;需关联连接后的持续传输。
- CrossDesk 分开统计普通 ICE 直连、增强直连、中继、协商未启动、探测未命中、
命中后验证失败;命中成功仍使用仓库 wire 核验。UU 的内部日志契约与密钥未知,
不直接套用 `verify_punch_wire.py`,不把无法解读的包伪装为认证检查成功。
- 若 UU 重连沿用空闲期持续存活的直连端点,首次连接却要重新探测,优先检查通道
保持/复用差异。若进程重启后也稳定优于 CrossDesk,则重点对比目标端口分布、
双端发包相对顺序、重复探索与失败后的后续尝试;不能只比较 231/49 和 512/64。
- UU 若选择 IPv6/TCP,单独报告该路径成功;不混为 CrossDesk v1 的 IPv4 UDP
穿透成功,也不从结果中删除。判断某类协议完全没有流量前,先核对抓包覆盖。
- 本轮结束后先形成有证据的差异和待验证假设,再决定单因素改动。不同端口策略、
重试策略和通道保持应分别验证;不通过延长原 ICE 窗口、强制选路或重启 DTLS
绕过接入。需要更改既定资源/协议边界时先修订对应方案。
正式收益评估仍使用 [v1 实验步骤](../deps/submodules/minirtc/tests/ice/lab/punch-v1-experiments.md)
的 A/B/C 各至少 30 次;本轮保留进程的重连样本存在关联,不当作独立冷启动样本
计算普遍成功率。若要量化 UU 与 CrossDesk 差距,还需另行制定双方启动条件一致、
覆盖多组网络的对照样本,不由这 8 次诊断推导百分比结论。
## 7. U01 已收证据(2026-09-14)
Windows 附件覆盖 14:52:55–14:53:53;Mac 有 14:46:37–14:52:10、
14:52:51–14:53:49 两段,只有后半段具有对应 Windows 抓包。时间为 Asia/Shanghai,
两端墙钟约差 1.36 秒,耗时使用单端时钟或同载荷校准。
后半段确认 UU 新建直连:Mac 复用同一本地 UDP 端点探测 475 个不同公网端口,
按端口数值递增发送;Windows 使用 150 个不同本地端口向 Mac 固定公网端点探测,
共 303 个请求,按首次发包排序的第 59 个端点命中。命中后同一端点继续进行 STUN
检查和提名,并持续承载双端能够匹配的实际数据。Mac 的 UU 日志也记录 relay →
直连候选对切换。从 Mac 增强探测开始到首次直接入站约 5.14 秒,到 RTP 形状数据
约 5.23 秒;不是从用户点击连接开始计时。
较早一次 UU 连接仅有 Mac 侧证据:223 个不同探测目标,约 2.39 秒首次命中。
两次的 Mac 本地/公网端点及远端公网端点不同,日志显示分别创建 transport 并重新
探测。后半段没有沿用前一次成功的传输 tuple,但未证明两端进程已经完全退出。
上述端口数量均为这次观测值,不是固定配置或最优参数。
同时间 CrossDesk 日志及抓包显示还有活动远控连接,UU 也枚举了 TUN 候选;缺少
完整操作事件、Windows 较早抓包和进程生命周期证据。因此本批作为机制诊断样本,
不判为严格 U01 首次/重连对照通过,不计算正式成功率,也不据此修改资源预算。
本次命中端点排名 59,不能仅以 150 与 64 的数量差归因 CrossDesk 的失败。
已用独立 tcpdump 过滤复核三份抓包的探测计数;后半段双端载荷匹配为 Mac →
Windows 698/698、Windows → Mac 4284/4286,另两份为命中前短探测。52 个直接
STUN 包的 FINGERPRINT 校验通过,含提名的请求有匹配成功响应;没有将此等同于
UU MESSAGE-INTEGRITY HMAC 已验证。原始日志与抓包未写入仓库。
本机私有报告与可复现脚本位于
`/private/tmp/crossdesk-uu-U01-analysis-3mz4000c/`。U02 补齐连续首次/重连过程;
用户后来确认需要 UU 管理通道,后续按第 9 节调整后台条件。若已有较早 Windows
文件,可补入 U01;本次已经确认的探测和传输证据继续保留。
## 8. X01 已收证据(2026-09-14)
Mac 同次日志包含五次连接,Windows `cd9.pcapng` 覆盖前四次。前三次双方均按
预算发包:Mac 同一端点向 512 个随机离散目标发送,Windows 64 个本地端点各
探测 3 次,共 192 个请求。双方均未见探测到达,10 秒探索与 3 秒收尾后保持
中继;这些结果属于未命中,不是命中后 ICE 接入失败。
第四次 Mac 第 498 个目标命中 Windows 第 19 个端点;同一端点回复 ACK、继续
完成 STUN 提名并传输。Mac 在 START 后 5.023 秒选择增强直连,最终发送 509 个
探测后停止。双端匹配到 Mac → Windows 2574 个、反向 456 个 RTP 形状数据包;
直接 tuple 未见 DTLS 重握手。Windows 发完三轮探测后约 2.31 秒,命中端点仍能
接收探测并应答,说明主动发包结束没有使它提前关闭。
第五次在探索开始约 9.16 秒后断开;Windows 没有覆盖,Mac 只捕获前 159 个请求,
日志记录 512 个,单独保留为取消且抓包不全,不当作完整失败。Mac 末尾抓包
丢包计数缺失,Windows ifdrop=0。前三次失败也不能仅由内侧抓包唯一归因到 NAT
映射或过滤规则。
四次完整会话的 Mac 探测均与运行时 trace 逐项匹配;系统 tcpdump 独立计数与
解析器一致。后补 Windows `minirtc-20260914-150934.log` 覆盖第 4、5 次,第四次
已通过仓库 `verify_punch_wire.py` 严格核验(1/1):双方认证接收 trace、各 6 个
STUN 检查包的 MESSAGE-INTEGRITY/FINGERPRINT、提名和直接传输相互对应。
Windows 的同一 handle 在 probe-rx 后 promote,再被 enhanced selected;停止
探索时只保留选中端点,继续传输至断开。关闭后抓包未见原基础及池端点再出站。
第五次取消时 Windows 日志确认额外端点计数归零,仍无对应的完整抓包。前三次
Windows 运行日志缺测,不将第四次门禁通过扩展为全部会话的完整核验。
用户确认此次保留 UU 连接是操作对端所需。UU 服务日志及物理流量确认已有管理
直连贯穿测试,应与 CrossDesk 新连接分开。此次控制方向为 Windows → Mac,
与已分析 U01 新会话方向不同;连接间等待约 2–8 秒,未按原计划观察 60 秒、
等待 30 秒。因此记为快速重复连接诊断,保留三次未命中、一次成功和一次不完整
取消,不计算正式成功率。私有报告位于 `/private/tmp/crossdesk-X01-analysis-5r9xbi9i/`。
## 9. 保留 UU 管理通道时的后续约定
用户在 X01 后确认必须使用 UU 操作对端。后续优先采用可执行的条件:
- 保留必要的 UU 管理连接,不终止相关服务或进行会中断唯一操作通道的进程重启。
第 3 节完全退出与安静期的步骤不适用于这种情况;标记为“管理连接保持开启的
新会话/保留进程重连”,不标为全部进程重启,也不标为 NAT 状态已清空。
- X02/U02 两款软件保持相同的管理连接背景。记录管理连接的端点和持续时间,
它不计入被测新会话的成功样本。若 UU 无法在保留管理连接时建立独立测试会话,
应标为缺测;不把原管理通道已经直连当作一次新的 UU 打洞成功。
- 被测新会话固定控制方向。每次连接观察 60 秒,断开被测会话后保留进程等待
30 秒,再重连观察 60 秒;两端持续抓包,最后断开被测会话后再等 15 秒停止。
抓包需要新目录并记录实际路径,避免提前停止或覆盖文件。
- X01 第 4 次成功样本的认证和端点生命周期证据已补齐,不要求为此重做网络过程。
第 1–3 次 Windows 日志及第 5 次完整抓包的缺测状态继续保留。
这是根据操作条件对实验方法的调整,没有改变功能开关、探测预算或 ICE 接入
规则。与完全退出后台的原始批次分开记录,不将两类条件混成一个成功率。
## 10. U01/X01 后的新参数批次
用户要求 CrossDesk 改用离散递增目标并提高数量。当前默认 pool=128、目标=1024、
128 PPS;目的端口仍在 1024–65535 无重复随机抽取,仅发送顺序改为递增。本地
pool 端口由系统分配,不假设本地递增能使 NAT 公网端口递增。
两端同步 MiniRTC 源码并重新构建、重启 CrossDesk,记录二进制 SHA 与 HELLO/START
实际预算;显式旧环境变量和旧 manifest 会覆盖新默认值,旧参数对端协商也会降低
预算。新批次保持第 9 节的 UU 管理通道,抓包使用新目录。核对 anchor 未命中时
目标端口是否严格递增、pool 是否分配 128 个端点;最多 1024 次 PROBE/ACK 共享
预算与原截止仍有效,命中后实际发送数量可以减少。
命中仍需关联同一 handle 的标准 ICE 检查、提名与后续传输;未命中保留中继和
清理记录。此批同时修改数量、速率和顺序,不能单独归因排序带来的收益,也不将
X01 的旧样本追溯计为新参数的验证。画面、声音、控制及切换质量继续按用户要求免验。