概述RFC 6062Traversal Using Relays around NAT (TURN) Extensions for TCP Allocations扩展了 TURN 协议使中继分配allocation能够使用 TCP 作为中继传输协议。这使得 WebRTC 的 ICE-TCPRFC 6544候选者可以通过 TURN 中继建立端到端 TCP 连接适用于 UDP 被完全封锁的网络环境。1.1 与标准 TURN UDP 中继的区别维度 标准 TURN (RFC 8656) RFC 6062 TCP AllocationREQUESTED-TRANSPORT 17 (UDP) 6 (TCP)控制连接 UDP 或 TCP/TLS TCPMode-A数据传输 UDP Send/Data 指示 ChannelData TCP 数据连接Mode-B对端连接建立 无需UDP 无连接 CONNECT / CONNECTION-ATTEMPT数据连接绑定 无需 CONNECTION-BIND CONNECTION-ID适用场景 普通 NAT 穿透 ICE-TCP / UDP 封锁环境1.2 stice 中的实现架构stice 将 RFC 6062 的实现分为两个 TCP 连接┌──────────────┐ Mode-A (控制) ┌──────────────┐│ │ ◄──────────────► │ ││ stice Agent │ STUN 事务 │ TURN Server ││ │ (Allocate/Refresh│ ││ │ /CONNECT/Creds) │ ││ │ │ ││ │ Mode-B (数据) │ ││ │ ◄──────────────► │ ││ │ 应用数据 STUN │ ││ │ 连接检查(RFC4571)│ │└──────────────┘ └──────────────┘Mode-A 控制连接turnTcpTransport_使用 FramingMode::Raw传输 Allocate、Refresh、CreatePermission、CONNECT 等 STUN 事务由 StunConn 自定界解析。 Mode-B 数据连接turnDataConn_使用 FramingMode::RFC4571在 CONNECTION-BIND 成功后透传应用数据和 STUN 连接性检查。协议流程详解2.1 分配建立Allocate客户端通过 Mode-A 控制连接发送 Allocate 请求REQUESTED-TRANSPORT 属性值为 6TCPClient (Mode-A) TURN Server│ ││── Allocate Request ──────────────────►││ REQUESTED-TRANSPORT: TCP(6) ││ ││◄── 401 Unauthorized ──────────────────││ REALM, NONCE ││ ││── Allocate Request ──────────────────►││ USERNAME, REALM, NONCE, MI ││ ││◄── Allocate Success Response ─────────││ XOR-RELAYED-ADDRESS (TCP passive) ││ LIFETIME │分配成功后客户端获得一个 TCP 类型的中继候选者tcptypepassive通过 SDP 信令发送给对端。2.2 权限创建CreatePermissionRFC 6062 仍然需要 CreatePermission。当对端尝试连接到本地中继地址时TURN 服务器会检查是否已为该对端创建权限。如果没有权限服务器会关闭入站 TCP 连接且不发送 CONNECTION-ATTEMPT 指示RFC 6062 §4.4。stice 在 formPairs() 阶段就提前为每个对端中继候选者调用 ensurePermission()确保权限在 CONNECT 之前就绪。2.3 主动模式CONNECT 请求当双方都获得 TCP 中继候选者后需要建立端到端 TCP 数据连接。RFC 6062 定义了两种角色主动方Active发送 CONNECT 请求要求 TURN 服务器与对端中继地址建立 TCP 连接。 被动方Passive等待对端连接收到 CONNECTION-ATTEMPT 指示后建立数据连接。stice 使用 ufrag 字典序比较来确定性地选择主动方shouldInitiateTcpConnect()local_ufrag remote_ufrag 的一方为主动方。这避免了在 ICE 角色冲突487解决之前双方同时发送 CONNECT 的问题。主动方 (Mode-A) TURN Server 被动方 (Mode-A)│ │ ││── CONNECT Request ───────►│ ││ XOR-PEER-ADDRESS │── TCP connect ──────────►││ │ ││ │◄── CONNECTION-ATTEMPT ────││ │ Indication ││ │ XOR-PEER-ADDRESS ││ │ CONNECTION-ID ││◄── CONNECT Success ────────│ ││ CONNECTION-ID │ │2.4 数据连接建立与 CONNECTION-BIND无论是主动方收到 CONNECT 成功响应还是被动方收到 CONNECTION-ATTEMPT 指示双方都需要打开一条新的 TCP 连接到 TURN 服务器Mode-B 数据连接。 在该连接上发送 CONNECTION-BIND 请求携带从 CONNECT 响应或 CONNECTION-ATTEMPT 指示中获得的 CONNECTION-ID。 收到 CONNECTION-BIND 成功响应后数据连接绑定完成可以传输应用数据。客户端 (Mode-B) TURN Server│ ││── TCP connect ───────────────────────►││ ││── CONNECTION-BIND Request ───────────►││ CONNECTION-ID: ││ (RFC 4571 framing) ││ ││◄── CONNECTION-BIND Success ───────────││ ││◄════ 应用数据 / STUN 检查 ═══════════►││ (RFC 4571 framing, transparent) │关键区别Mode-A 控制连接使用 Raw 帧模式STUN/ChannelData 自定界而 Mode-B 数据连接使用 RFC4571 帧模式每帧前加 2 字节长度前缀因为数据连接上传输的是原始应用数据和 STUN 消息的混合流需要长度前缀来定界。2.5 数据传输与 STUN 连接检查数据连接绑定完成后stice 将该对的所有 STUN 连接性检查和应用数据都路由到 Mode-B 数据连接sendStunBinding() 中检测到 pairIsTurnTcpRelay(e.pair) 时通过 sendTurnDataConn() 发送 STUN Binding 请求。 sendBindingResponse() 中检测到请求来自数据连接时响应也通过同一数据连接发回。 应用数据通过 stice_send() → sendViaSelectedPair() → sendTurnDataConn() 发送。stice API 使用指南3.1 启用 RFC 6062 TCP Allocation在 stice 中启用 RFC 6062 TCP allocation 只需将 TURN 服务器的传输类型设置为 STICE_TURN_TRANSPORT_TCP#include stice/stice.hstice_config_t config {0};stice_turn_server_t turn {0};turn.host “turn.example.com”;turn.port 3478;turn.username “user”;turn.password “pass”;// 关键设置为 TCP 传输stice 将自动使用 RFC 6062 TCP allocationturn.transport STICE_TURN_TRANSPORT_TCP;config.turn_servers turn;config.turn_servers_count 1;stice_agent_t *agent stice_create(config);stice_gather_candidates(agent);设置 transport STICE_TURN_TRANSPORT_TCP 后stice 内部会turn::Client 设置 isTcpAllocation_ trueAllocate 请求中 REQUESTED-TRANSPORT 6。 Agent 创建 turnTcpTransport_Mode-A 控制连接Raw 帧模式。 收集到的中继候选者类型为 relayedtcptypepassive。 formPairs() 时自动调用 ensurePermission() 和 sendConnect()主动方。 CONNECT 成功或 CONNECTION-ATTEMPT 到达时自动创建 turnDataConn_Mode-B 数据连接RFC4571 帧模式并发送 CONNECTION-BIND。3.2 启用 ICE-TCPRFC 6062 TCP relay 通常与 ICE-TCPRFC 6544配合使用。启用 ICE-TCP// 启用 ICE-TCPactive passive 模式stice_set_ice_tcp_mode(agent, STICE_ICE_TCP_MODE_ACTIVE_PASSIVE);可选模式STICE_ICE_TCP_MODE_NONE禁用 ICE-TCP默认 STICE_ICE_TCP_MODE_ACTIVE仅主动模式 STICE_ICE_TCP_MODE_PASSIVE仅被动模式 STICE_ICE_TCP_MODE_ACTIVE_PASSIVE主动被动推荐3.3 中继候选者的 SDP 格式RFC 6062 TCP 中继候选者在 SDP 中的格式acandidate:1 1 tcp 1673026431 192.0.2.1 50000 typ relay tcptype passive raddr 0.0.0.0 rport 0关键字段传输协议为 tcp而非 udp typ relay中继类型 tcptype passiveTCP 候选者类型为 passiveRFC 6062 中继候选者总是 passive3.4 回调与事件stice 的标准回调在 RFC 6062 场景下正常工作static void on_state_changed(stice_agent_t *agent, stice_state_t state, void *user) {// STICE_STATE_GATHERING → 收集中继候选者// STICE_STATE_CONNECTING → CONNECT / CONNECTION-BIND 进行中// STICE_STATE_CONNECTED → 数据连接绑定完成可传输数据// STICE_STATE_COMPLETED → ICE 完成}static void on_candidate(stice_agent_t *agent, const char *sdp, void *user) {// sdp 包含 tcptypepassive 的中继候选者// 通过信令发送给对端}static void on_recv(stice_agent_t *agent, const char *data, size_t size, void *user) {// 通过 Mode-B 数据连接收到的应用数据}内置测试与验证4.1 test_turn 中的 RFC 6062 专项测试运行所有 TURN 测试含 RFC 6062./build/Release/test_turn.exe仅运行 RFC 6062 标签的测试./build/Release/test_turn.exe “[rfc6062]”覆盖内容CONNECT 请求构建与发送 CONNECTION-BIND 请求构建 CONNECT 成功响应解析提取 CONNECTION-ID CONNECTION-ATTEMPT 指示解析 CONNECT 失败处理447 等错误码 主动被动双模式状态机4.2 test_candidate_pair 中的 TCP 中继对过滤./build/Release/test_candidate_pair.exe “[pair]”验证 RFC 6544 §5.2 的 TCP 候选对过滤规则特别是 RFC 6062 的例外两个 relayed passive 候选者可以配对通过 CONNECT/CONNECTION-ATTEMPT 建立端到端连接而非中继的 passive-passive 对被过滤。4.3 stserver 联合测试启动 stserver./stserver --config stserver.test.conf运行 TURN TCP relay 测试RFC 6062 Mode-B./test_stserver_all.exe 127.0.0.1 3478 turn-tcp运行 TURN relay 测试TCP 传输./test_stserver_relay.exe 127.0.0.1 3478 tcpturn-tcp 模式测试完整流程分配 → CreatePermission → CONNECT → CONNECTION-BIND → 数据交换。5. 常见问题与排错5.1 CONNECT 失败错误码 447 Connection Timeout or Failure原因TURN 服务器无法与对端中继地址建立 TCP 连接。排查确认对端已完成 TURN 分配并获得中继候选者。 确认对端已为本地中继地址创建 CreatePermissionRFC 6062 §4.4无权限时服务器不发送 CONNECTION-ATTEMPT。 检查防火墙是否允许 TURN 服务器的出站 TCP 连接到中继端口范围。 stice 会自动重试 CONNECT 最多 3 次间隔 500ms超过后标记候选对失败。5.2 数据连接建立后无数据传输原因CONNECTION-BIND 未成功或 STUN 连接检查未通过数据连接发送。排查检查日志中是否有 TURN data: opening data connection connId...。 检查 CONNECTION-BIND 是否收到成功响应。 确认 turnDataConn_-bound trueSTUN 检查仅在绑定完成后通过数据连接发送。 检查候选对是否被识别为 pairIsTurnTcpRelay本地候选者为 relayed 且 transportTCPPassive。5.3 双方同时发送 CONNECT原因RFC 6062 要求只有一方发送 CONNECT另一方等待 CONNECTION-ATTEMPT。stice 的处理使用 shouldInitiateTcpConnect() 基于 ufrag 字典序确定性选择主动方避免双方同时发送。如果 ufrag 尚未交换remote_.iceUfrag.empty()则回退到 ICE 角色Controlling 方为主动。5.4 与 coturn 的兼容性stice 的 RFC 6062 实现与 coturn 服务器兼容。使用 coturn 时确保服务器配置中启用 TCP 中继listening-port 同时监听 TCP。 中继端口范围min-port/max-port有足够的 TCP 端口。 长期凭证认证配置正确。设计决策与限制6.1 单数据连接限制当前 stice 每个 Agent 仅维护一个 turnDataConn_Mode-B 数据连接。这意味着在多对场景下同一时间只能有一个 TCP 中继对处于活跃数据传输状态。这是 ICE 场景的典型限制通常只有一个 selected pair但在需要同时维护多个 TCP 中继连接的场景下需要扩展。6.2 ufrag 排序选择主动方RFC 6062 规范未规定如何选择 CONNECT 发起方。stice 采用 ufrag 字典序比较与 libjuice 一致优点是在 ICE 角色冲突解决之前就能确定性地选择主动方确保 CONNECT 在 STUN 检查之前完成。6.3 CreatePermission 仍然需要RFC 6062 场景下 CreatePermission 不是可选的。被动方的 TURN 服务器在接受入站 TCP 连接时会检查权限无权限时关闭连接且不发送 CONNECTION-ATTEMPT。stice 在 formPairs() 阶段提前调用 ensurePermission() 确保权限就绪。7. 参考资料RFC 6062: Traversal Using Relays around NAT (TURN) Extensions for TCP Allocations RFC 8656: Traversal Using Relays around NAT (TURN): Relay Extensions to Session Traversal Utilities for NAT (STUN) RFC 6544: TCP Candidates with Interactive Connectivity Establishment (ICE) RFC 8445: Interactive Connectivity Establishment (ICE): A Protocol for Network Address Translator (NAT) Traversal pion/turn: Go 语言 TURN 实现stice 的移植参考 libjuice: C 语言 ICE 库stice 的 API 兼容目标