ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

使用 stice 实现webrtc RFC 6062 TCP Allocations

使用 stice 实现webrtc RFC 6062 TCP Allocations
  1. 概述

RFC 6062(Traversal Using Relays around NAT (TURN) Extensions for TCP Allocations)扩展了 TURN 协议,使中继分配(allocation)能够使用 TCP 作为中继传输协议。这使得 WebRTC 的 ICE-TCP(RFC 6544)候选者可以通过 TURN 中继建立端到端 TCP 连接,适用于 UDP 被完全封锁的网络环境。
1.1 与标准 TURN UDP 中继的区别
维度 标准 TURN (RFC 8656) RFC 6062 TCP Allocation
REQUESTED-TRANSPORT 17 (UDP) 6 (TCP)
控制连接 UDP 或 TCP/TLS TCP(Mode-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 连接性检查。
  1. 协议流程详解
    2.1 分配建立(Allocate)

客户端通过 Mode-A 控制连接发送 Allocate 请求,REQUESTED-TRANSPORT 属性值为 6(TCP):

Client (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 类型的中继候选者(tcptype=passive),通过 SDP 信令发送给对端。
2.2 权限创建(CreatePermission)

RFC 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() 发送。
  1. stice API 使用指南
    3.1 启用 RFC 6062 TCP Allocation

在 stice 中,启用 RFC 6062 TCP allocation 只需将 TURN 服务器的传输类型设置为 STICE_TURN_TRANSPORT_TCP:

#include <stice/stice.h>

stice_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 allocation
turn.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_ = true,Allocate 请求中 REQUESTED-TRANSPORT = 6。 Agent 创建 turnTcpTransport_(Mode-A 控制连接,Raw 帧模式)。 收集到的中继候选者类型为 relayed,tcptype=passive。 formPairs() 时自动调用 ensurePermission() 和 sendConnect()(主动方)。 CONNECT 成功或 CONNECTION-ATTEMPT 到达时自动创建 turnDataConn_(Mode-B 数据连接,RFC4571 帧模式)并发送 CONNECTION-BIND。

3.2 启用 ICE-TCP

RFC 6062 TCP relay 通常与 ICE-TCP(RFC 6544)配合使用。启用 ICE-TCP:

// 启用 ICE-TCP(active + 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 中的格式:

a=candidate: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 passive:TCP 候选者类型为 passive(RFC 6062 中继候选者总是 passive)

3.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 包含 tcptype=passive 的中继候选者
// 通过信令发送给对端
}

static void on_recv(stice_agent_t *agent, const char *data, size_t size, void *user) {
// 通过 Mode-B 数据连接收到的应用数据
}

  1. 内置测试与验证
    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 tcp

turn-tcp 模式测试完整流程:分配 → CreatePermission → CONNECT → CONNECTION-BIND → 数据交换。
5. 常见问题与排错
5.1 CONNECT 失败(错误码 447 Connection Timeout or Failure)

原因:TURN 服务器无法与对端中继地址建立 TCP 连接。

排查:

确认对端已完成 TURN 分配并获得中继候选者。 确认对端已为本地中继地址创建 CreatePermission(RFC 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 == true(STUN 检查仅在绑定完成后通过数据连接发送)。 检查候选对是否被识别为 pairIsTurnTcpRelay(本地候选者为 relayed 且 transport=TCPPassive)。

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 端口。 长期凭证认证配置正确。
  1. 设计决策与限制
    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 兼容目标)
返回列表