ICE 协议
参考:
1. 概述
ice(Interactive Connectivity Establishment, 交互式连接建立)是 webrtc 中建立媒体连接通道的方式,它主要是为了处理 nat 场景下网络连接的建立。ice 协议的实现依赖于其它协议,比如信令交换的协议、stun/tune 协议等。其整体过程如下所示:
在 rfc8445 中,对于 ice 的整个过程做了非常详细的描述。本篇文章主要在 rfc8445 内容的基础上,记录自己对于 ice 过程的理解。注意,这里不涉及 trickle ice 扩展。
2. ice 角色与模式
2.1 ice 角色
ice 角色分为 controlling role 和 controlled role,控制端会决定选择哪一对连接作为最终的媒体通道。区分控制端与被控端有如下规则(rfc8445 6.1.1节):
- 在两端都是 full ice 的情况下,发起 offer 的一端为 controlling 端
- 在一端是 lite ice,一端是 full ice 的情况下,那么 full ice 一端必须是 controlling 端
- 在两端都是 lite ice 的情况下,发起 offer 的一端为 controlling 端
2.1.1 role conflict(角色冲突)
ice agent 在连通性检查阶段发送 stun binding request 时:
- 如果 agent 是 controlling 端,那么会携带 ICE-CONTROLLING Attributes
- 如果 agent 是 controlled 端,那么会携带 ICE-CONTROLLED Attributes
如果 peer ice agent 收到 stun binding request 后,会检查此角色属性,是否与本端角色冲突:
- 如果冲突,比较本端 tie-breaker 值与 role attribute 中 tie-breaker 的值大小
- 如果本端 tie-breaker 较大,本端角色属性不变,回复 487(role conflict) 给对端,让对端切换角色
- 如果本端 tie-breaker 较小,切换本端角色属性,正常回复 stun binding response 给对端,对端角色属性不变
tie-breaker 的取值在 rfc8445 中没有详细描述,但是可以看如下的实现(https://github.com/ireader/sdk/blob/master/libice/src/ice-agent.c):
ice->tiebreaking = (intptr_t)ice * rand();
即其是一个随机数。
2.2 ice 模式
ice agent 可以工作在 full 或者 lite 模式下:
- full ice,即需要发送连通性检查,也需要回复对端的连通性检查响应
- lite ice,只回复对端的连通性检查响应,不主动发起连通性检查,这种一般是 agent 部署在公网下的情况。
3. candidate(候选地址)
candidate 在 ice 中称为网络候选地址,会话双方通过信令交换 candidates 来建立媒体通道,candidate 示例参见:https://www.cnblogs.com/moonwalk/p/15867396.html
3.1 candidate 与 component、stream 的关系
candidate 与在其之上与的 component 具有层级关系,而 component 也与在其之上与的 stream 具有层级关系:
实际上每收集到一个 host candidate 后,会遍历添加到每个 stream 及其下的 components 中:
HostCandidate host_a, host_b, host_c;
for (Stream& s : streams) {
for (Component& c : components) {
add_host_candidate(s, c, host_a);
add_host_candidate(s, c, host_b);
add_host_candidate(s, c, host_c);
}
}
然后以每个 host candidate 为 base 地址,通过 stun 服务器得到 server reflexive address;通过 tune 服务器得到 relay address(rfc8445 5.1.1.2节)。
3.2 候选地址类型
分类如下:
To Internet
|
|
| /------------ Relayed
Y:y | / Address
+--------+
| |
| TURN |
| Server |
| |
+--------+
|
|
| /------------ Server
X1':x1'|/ Reflexive
+------------+ Address
| NAT |
+------------+
|
| /------------ Host
X:x |/ Address
+--------+
| |
| Agent |
| |
+--------+
Figure 2: Candidate Relationships
- host address,即本地机器网卡上的 ip 地址,本机网卡可能有多个
- server reflexive address,即通过 stun/turn 服务器得到的 host address 经过 nat 映射后的地址(turn 分配请求也能得到 srflx 地址)
- relayed address,即在 turn 服务器上分配的地址
- peer reflexive address,即对端看到的 stun binding request 源地址,此地址较为特殊,ice 代理在地址收集阶段无法直接发现此类型地址,此地址的处理将在后文论述
3.3 componet-id
componet-id 用于区分不同的传输媒体类型,其中,rtp 对应 1,rtcp 对应 2。不同的 componet 需要一个独立的媒体通道。
在 sdp 中可以通过指定 a=rtcp-mux 属性,则表明 rtp 与 rtcp 将共用同一个媒体通道,那么只会出现一个 componet id 等于 1。
3.4 foundation
foundation 用于区分不同的 candidates 是否属于同一类,有如下规则(rfc8445 5.1.1.3节):
- 类型相同,即同属于 host、relay、prflx、srvflx 中的某一个
- base 地址的 ip 地址相同(port 没有要求)
- relay 和 reflexive 地址的 ip 地址相同
- 同一网络传输类型,即同属于 tcp、udp 中的某一个
例如,ice agent 通过同一个 ip 地址的两个不同 port,向同一个 stun 服务器发起了 stun binding request 请求,得到的两个 server reflexive 地址(ip:port 对),就属于同一个 foundation。
3.5 priority
每个 candidate 都有不同的优先级,计算 priority 的公式如下(rfc8445 5.1.2.1节):
priority = (2^24)*(type preference) +
(2^8)*(local preference) +
(2^0)*(256 - component ID)
其中:
- type preference,host(126), prflx(110), srflx(100), relay(0)
- local preference,rfc8445 没有指明具体值,但是可以看到如下实现(https://github.com/ireader/sdk/tree/master/libice/src/ice-candidate.h):
// (1 << 10) * struct sockaddr_storage::ss_family
v = (1 << 10) * c->host.ss_family;
3.6 base address
有如下规则(rfc8445 4节):
- host candidate,base 是其自身
- srvflx address,base 是发送 stun binding request 给 stun 服务器的 host address
- relayed candidate,base 是其自身
- prflx address,base 是发送 stun binding request 给对端的 host address
这里需要注意的是,host、srvflx、prflx 地址的 base address 很好理解,就是 ice agent 发送数据的内网源地址。
但是 relayed address 的 base address 并不是 ice agent 发送数据的内网源地址,而是 turn 服务器分配的 ip:port 地址对。
4. ice 过程
4.1 收集 local candidates
local candidate 即本端的所有 candidates,对应对端的是 remote candidates。
前面介绍了 candidate 相关内容,ice 的第一步就是收集 local candidates。
4.1.1 收集 candidate 的 Ta 和 RTO 定时器
-
Ta 定时器:
由于许多 nat 设备映射绑定内外网地址有最小时间间隔限制(rfc8445 B.1节),所以收集 stun reflexive address 和 turn relayed address 都需要每隔 Ta 时间再发送一次请求(turn relayed address 也需要 nat 进行地址映射绑定),所以收集这两种地址是串行化的。
收集 host address 没有 Ta 定时器限制。
Ta 定时器取值最小 5ms,默认 50ms。 -
RTO 定时器:
stun 和 turn 的事务请求需要支持事务重传机制。
在 rfc5389 7.2.1节中,规定默认 RTO interval(时间间隔)为 500ms,然后经过 0ms, 500ms, 1500ms, 3500ms, 7500ms, 15500ms, 31500ms 时间间隔的重传后(7 次重传),超过 39500ms 的事务确认失败。
但是在 rfc8445 14.3节中,RTO 的计算规则为:
During the ICE gathering phase, ICE agents SHOULD calculate the RTO
value using the following formula:
RTO = MAX (500ms, Ta * (Num-Of-Cands))
Num-Of-Cands: the number of server-reflexive and relay candidates
可见,越晚收集到的 srvflx 和 relayed 地址,重传间隔也越长。
4.1.2 host address
在 linux 机器上,收集本机网卡上的 ip 地址,可以通过 getifaddrs() 函数获取所有网卡上的所有 ip 地址,然后根据自定义的规则过滤掉不需要的地址。
收集完 ip 地址后,可以任意选择一个配置的端口,作为 host 地址的 ip:port 对,然后添加到 local candidates 列表中。
注意,一般一个 host ip 地址只会分配一个 port,除非根据不同本地策略分配绑定多个 port。
4.1.3 server reflexive address
获得 server reflexive address 会有两种情况:
- 向 turn 服务器发送 allocate request 失败时,继续发送 stun binding request 以获得 server reflexive address(rfc8445 5.1.1.2节)
- 直接向 stun 服务器发送 stun binding request 以获得 server reflexive address
收集 host address 总是第一步,在收集完后,会以 host address 为 base 地址,通过 host address 的 ip:port 地址对,向配置的所有 stun 服务器发送 stun binding request 请求。
stun 服务器收到请求后,会得到 ice agent 通过 nat 映射后的 ip:port 地址对,随后在响应的 stun binding response 中,会携带 nat 映射后的公网 ip:port 地址对(XOR-MAPPED-ADDRESS)。
这里假设有 n 个 host address(ip:port 唯一),有 m 个配置的 stun 服务器,那么会发起 n*m 次 server reflexive address 地址收集(笛卡尔积)。
4.1.4 relayed address
一般通过与 server reflexive address 收集时相同的内网地址(注意,非 base address),向 turn 服务器发送 allocate request,得到 turn 服务器分配的 relay 地址。
allocate request 需要使用 long-term credential mechanism 身份认证。
4.1.5 peer reflexive address
peer reflexive address 较为特殊,此地址是在连通性检查阶段,从对端回复的 stun binding response 中得到的(XOR-MAPPED-ADDRESS),是对端看到的本端的 ip:port 地址对。
但是在 rfc8445 7.2.5.3.1 节中,说明了 ice agent 得到了 peer reflexive address 后,不会与 remote candidates 组成 pair,而是由本地策略选择是否将此地址通过信令发送给对端。
4.2 保活
在 rfc8445 5.1.1.4 节中对此有说明:
- 对于 srvflx address,nat 映射会有一个有效期,在完成 ice 之前,需要保持此 nat 映射
- 对于 realy address,turn 服务器会有分配地址的有效期
- 对于 host address,主机网络可能随时发生变化,因此需要定时进行地址检测(webrtc/rtc_base/network.cc::UpdateNetworksContinually() 源码中使用 2s 的定时器)
注意,保活不受 Ta 或 RTO 定时器控制。
4.3 删除重复的 candidate
在 rfc8445 5.1.3 节中对此有说明,如果两个 candidates 的地址一样,且 base 地址也一样,则应该删除掉优先级低的。
比如部署在公网的 ice agent,其 srvflx candidate 与某个 host candidate 是相同的,那么根据优先级,srvflx candidate 会被删除掉。
4.4 交换 candidates
通过信令交换 candidates 参看:https://www.cnblogs.com/moonwalk/p/15867396.html
5. checklist 的初始化
一个 sdp 中可能存在多个 media streams,如果没有指明 a=group-bundle 属性,那么这些 media streams 都需要建立独立的媒体连接通道。
在 ice 中,对每一个独立的 media stream 的处理,称为一个 checklist。
checklist 有3种状态,比较简单:
- Running,初始状态
- Completed,checklist 下所有 components 都完成了提名
- Failed,checklist 下至少有一个 components 提名失败
5.1 candiadte pair
收到 remote candiates 后,就可以与 local candidates 组成 candidate pair 了。
candidate pair 是一对对潜在网络通道,后续会在这些 pair 上做连通性检测以选取最终的网络连接。
local-remote candidate 生成 pair 有如下规则(rfc8445 6.1.2.2节):
- 网络类型相同。都是 udp 或者 tcp
- 对应的 media stream 相同
- component id 相同
注意,并没有要求 foundation 相同。candidate pair 与 checklist、candidate 的图示如下:
+--------------------------------------------+
| |
| +---------------------+ |
| |+----+ +----+ +----+ | +Type |
| || IP | |Port| |Tran| | +Priority |
| ||Addr| | | | | | +Foundation |
| |+----+ +----+ +----+ | +Component ID |
| | Transport | +Related Address |
| | Addr | |
| +---------------------+ +Base |
| Candidate |
+--------------------------------------------+
* *
* *************************************
* *
+-------------------------------+
| |
| Local Remote |
| +----+ +----+ +default? |
| |Cand| |Cand| +valid? |
| +----+ +----+ +nominated?|
| +State |
| |
| |
| Candidate Pair |
+-------------------------------+
* *
* ************
* *
+------------------+
| Candidate Pair |
+------------------+
+------------------+
| Candidate Pair |
+------------------+
+------------------+
| Candidate Pair |
+------------------+
Checklist
Figure 5: Conceptual Diagram of a Checklist
每生成一个 candidate pair,都会将其加入到所属的 checklist 中,每个独立的 media stream 都对应一个 checklist,每个 checklist 中,有多个 candidate pairs。
5.1.1 pair priority
candidate pair 的优先级取决于 local 和 remote candidate 的优先级,计算方式如下(rfc8445 6.1.2.3):
pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0)
一个计算示例如下(https://github.com/ireader/sdk/tree/master/libice):
static inline void ice_candidate_pair_priority(struct ice_candidate_pair_t* pair, int controlling)
{
uint32_t G, D;
G = controlling ? pair->local.priority : pair->remote.priority;
D = controlling ? pair->remote.priority : pair->local.priority;
pair->priority = ((uint64_t)1 << 32) * MIN(G, D) + 2 * MAX(G, D) + (G > D ? 1 : 0);
}
5.1.2 pair foundation
candidate pair 的 foundation 取决于 local 和 remote candidate 的 foundation,一个计算示例如下(https://github.com/ireader/sdk/tree/master/libice):
static inline void ice_candidate_pair_foundation(struct ice_candidate_pair_t* pair)
{
// condidate-pair 的 foundation 仅仅由 local-remote 的 foundation 拼接而成
snprintf(pair->foundation, sizeof(pair->foundation), "%s\n%s", pair->local.foundation, pair->remote.foundation);
}
5.1.3 pruning the pairs
rfc8445 6.1.2.4 节对于 pairs 剪枝的描述如下:
- local candidate 替换:
回头看 server reflexive address,其 base address 是 host address。
由于 ice agent client 并不能直接通过 server reflexive address 地址发送请求(只能通过 host address 发送请求),ice 会将所有 local candidate 是 reflexive address 的 pair,使用其 base candidate 进行替换。 - pair 剪枝:
同时,如果两个 pair,其 local candidate 的 base address 相同,且 remote candidate 也相同,那么优先级低的 pair 将会被删除。
所以可以预见,所有 local candidate 是 server reflexive address 的 pair 都会被删除(local candidate 是 host address 的所有 pair 组合,优先级总是高于 local candidate 是 server reflexive address 组成的 pair)。
因此,ice agent 实现组成 pair 的时候,直接不使用 reflexive candidate 与任何 remote candidates 组成 pair 就行了,参看 https://github.com/libnice/libnice/blob/master/agent/conncheck.c 中 conn_check_add_for_candidate_pair() 函数的实现。
Q:既然 server reflexive candidate 会被删掉,那么为什么还要收集此地址呢?
A:原因在于需要在 NAT 上创建一个映射,以便对端消息能成功发送过来(当然,不一定能成功发过来,取决于 NAT 类型)。
5.2 进行 checklist 初始化
5.2.1 candidate pair states
在 rfc8445 6.1.2.6 节中,详细描述了 candidate pair 的状态:
- Frozen,所有 pairs 的初始状态
- Waiting,等待发送 check 的状态
- In-Progress,已经发送了 check
- Succeeded,check 成功
- Failed,check 失败
状态转移如下:
+-----------+
| |
| |
| Frozen |
| |
| |
+-----------+
|
|unfreeze
|
V
+-----------+ +-----------+
| | | |
| | perform | |
| Waiting |-------->|In-Progress|
| | | |
| | | |
+-----------+ +-----------+
/ |
// |
// |
// |
/ |
// |
failure // |success
// |
/ |
// |
// |
// |
V V
+-----------+ +-----------+
| | | |
| | | |
| Failed | | Succeeded |
| | | |
| | | |
+-----------+ +-----------+
Figure 6: Pair State Finite State Machine (FSM)
5.2.2 初始化为 waiting 状态
每个 candidate pair 都有其对应的 pair foundation,同一个 checklist 中的同一个 pair foundation 可能对应多个 candidate pairs。
初始化的步骤就是选取一部分 candidate pairs,从 frozen 状态设为 waiting 状态,waiting 状态的 pair 后续会优先被 check。
选取哪些 pair 呢?有如下规则(rfc8445 6.1.2.6节):
- 初始化动作是以 pair foundation 为处理单位的
- 所有 checklist 内,根据 pair foundation 进行分组,这时会出现多个 pairs 分到同一个 foundation 组
- 对于每一种不同的 pair foundation,选取出一个 pair 来将状态设置为 waiting 状态
- 每种 pair foundation 只能选取一个,从第一个 checklist 开始检查,即优先选择第一个 checklist 的 pair(checklist 的优先级可以根据本地策略自由设置)
- 选取的 pair 必须是 component id 最小的,且同一 component id 中优先级最高的
rfc8445 6.1.2.6 节给了如下示例:
Each row (m1, m2,...) represents a checklist associated with a
data stream. m1 represents the first checklist in the checklist
set.
Each column (f1, f2,...) represents a foundation. Every candidate
pair within a given column share the same foundation.
f-cp represents a candidate pair in the Frozen state.
w-cp represents a candidate pair in the Waiting state.
1. The agent sets all of the pairs in the checklist set to the
Frozen state.
f1 f2 f3 f4 f5
-----------------------------
m1 | f-cp f-cp f-cp
|
m2 | f-cp f-cp f-cp f-cp
|
m3 | f-cp f-cp
2. For each foundation, the candidate pair with the lowest
component ID is placed in the Waiting state, unless a
candidate pair associated with the same foundation has
already been put in the Waiting state in one of the
other examined checklists in the checklist set.
f1 f2 f3 f4 f5
-----------------------------
m1 | w-cp w-cp w-cp
|
m2 | f-cp f-cp f-cp w-cp
|
m3 | f-cp w-cp
Table 1: Pair State Example
In the first checklist (m1), the candidate pair for each foundation
is placed in the Waiting state, as no pairs for the same foundations
have yet been placed in the Waiting state.
In the second checklist (m2), the candidate pair for foundation f4 is
placed in the Waiting state. The candidate pair for foundations f1,
f2, and f3 are kept in the Frozen state, as candidate pairs for those
foundations have already been placed in the Waiting state (within
checklist m1).
In the third checklist (m3), the candidate pair for foundation f5 is
placed in the Waiting state. The candidate pair for foundation f1 is
kept in the Frozen state, as a candidate pair for that foundation has
already been placed in the Waiting state (within checklist m1).
Once each checklist have been processed, one candidate pair for each
foundation in the checklist set has been placed in the Waiting state.
为什么要以 pair foundation 为列进行选取呢,个人理解是,pair foundation 代表的是不同网络类型,每种网络类型都先尝试 check 一次是一种公平的抉择,否则同一个网络类型尝试多次,就会影响潜在的有效或更快的网络类型的成功 check。
6 连通性检查策略
注意,此部分中,rfc8445 与 rfc5245 描述的步骤有很多区别,rfc5245 的步骤更加复杂,rfc8445 则更加简单,这里以最新的 rfc8445 为准进行介绍。
6.1 Triggered-Check Queue
ice 中进行连通性检查有两种队列,一种是 ordinary check queue,另一种是 triggered check queue。
- ordinary check queue,即按部就班根据规则进行连通性检查的 queue(实际上一般代码实现中并没有这种队列,这里 queue 是一种抽象说法)
- triggered check queue,即需要优先进行 check 的 candidate pairs 的 queue
triggered check queue 中的 candidate pairs 的来源有如下几种情况:
- ice agent 服务端。某个 candidate pair,还处于 frozen 或 waiting 状态,但是收到了对端的连通性检查
- ice agent 客户端。某个 candidate pair 完成了连通性检查,接着要进行 pair nominated(提名)
6.2 定时器 Ta
除了地址收集时使用定时器 Ta,连通性检查也会使用定时器 Ta。
定时器 Ta 使得连通性检查串行化,每次定时器 Ta 被触发,都会选择一个 pair 进行连通性检查,不管之前的连通性检查成功或失败。
Ta 默认为 50ms(rfc8445 14.2节),也可以根据本地策略自定义时间,最小为 5ms。
6.3 进行连通性检查
前面初始化所有 checklist 后,得到了一批 waiting 状态的 candidate pair 等待连通性检查。接下来规则如下(rfc8445 6.1.4.2节):
- 选取第一个 running 状态的 checklist,checklist 的排序取决于本地规则,rfc8445 没有规定
- 如果当前 checklist 中 triggered-check queue 不为空,则优先从 triggered-check queue 队列中 pop 出队头的 pair,将状态设置为 in-progress,开始发送 check。注意此 pair 可能是带 nominated flag 的 pair(已经完成连通性检查)
- 如果所有 checklist 中没有处于 waiting 状态的 pair,那么会遍历每一个 pair foundation,将第一个 frozen 状态的 pair(排序取决于本地规则,没有具体规定),置为 waiting 状态。注意,关于此点,rfc8445 中没有详细描述其规则,可以参看 https://github.com/libnice/libnice/blob/master/agent/conncheck.c 中 priv_conn_check_unfreeze_next() 函数的实现
- 从所有处于 waiting 状态的 pairs 中,取出优先级最高的 pair(优先级相同的,取 component id 最小的),将状态设置为 in-progress,开始发送 check
- 如果当前 checklist 没有 waiting 状态的 pair 可以进行 check,那么不用再次等待 Ta 触发(因为还没有发送任何 check),直接切换到下一个 running 状态的 checklist,即重新进入第一步
注意,local candidate 是 relayed address 的 pair,需要先向 turn 服务器发送 CreatePermission 请求,然后通过 Send/Data 或 Channel 机制来传输数据。参看 https://www.cnblogs.com/pannengzhi/p/5048965.html。
6.4 连通性检查失败
连通性检查失败会将 candidate pair 设置为 failed 状态。下面会讨论几种失败的情况(具体参看 rfc8445 7.2.5.2节)。
6.4.1 Non-Symmetric Transport Addresses(非对称地址)
收到 stun binding response 后,需要检查:
- request 的 source ip:port 对与 response 的 target ip:port 对相同
- request 的 target ip:port 对与 response 的 source ip:port 对相同
6.4.2 ICMP Error(ICMP 错误)
icmp 中 host 或者 port 不可达错误。
6.4.3 Timeout(超时)
事务超时重传需要用到 RTO 定时器,在 rfc5389 7.2.1节中,规定默认 RTO interval(时间间隔) 500ms,然后经过 0ms, 500ms, 1500ms, 3500ms, 7500ms, 15500ms, 31500ms 时间间隔的重传后(7次最大重传次数),超过 39500ms 的事务确认失败。
但是在 rfc8445 14.3节中,RTO 的计算规则为:
For connectivity checks, agents SHOULD calculate the RTO value using
the following formula:
RTO = MAX (500ms, Ta * N * (Num-Waiting + Num-In-Progress))
N: the total number of connectivity checks to be performed.
Num-Waiting: the number of checks in the checklist set in the
Waiting state.
Num-In-Progress: the number of checks in the checklist set in the
In-Progress state.
Note that the RTO will be different for each transaction as the
number of checks in the Waiting and In-Progress states change.
可以参看 https://github.com/libnice/libnice/blob/master/agent/conncheck.c 中 priv_compute_conncheck_timer() 函数的实现(注意,此实现中没有计算 N)。
6.4.4 Unrecoverable STUN Response
ice agent server 回复了严重错误的 response。参考 rfc5389 7.4.3 节关于错误响应的描述。
6.5 连通性检查成功
连通性检查成功会将 candidate pair 设置为 success 状态。成功的规则是:
- 收到 binding success response
- Symmetric Transport Addresses,即 request 与 response 的 source-target 地址对称
下面会讨论几种成功后的处理(具体参看 rfc8445 7.2.5.3节)。
6.5.1 Discovering Peer-Reflexive Candidates(发现 prflx address candidate)
stun binding response 必须携带 xor-mapped-address,此地址是 peer ice agent server 看到的 stun binding request 的源地址(ip:port 对)。
本端收到 binding response 后,会检查 xor-mapped-address 中的地址是否与 local candidates 匹配,如果不匹配,则为一个新的 peer reflexive candidate。
但是注意,根据 rfc8445 7.2.5.3.1 节的描述,local peer reflexive candidate 并不会与 remote candidates 组成 pair,而是根据本地规则选择是否将此 candidate 通过信令发送给 peer,让 peer 作为 remote candidate 与 local candidate 组成 pair。
本端不与 remote candidates 组成新 pair 的原因是,携带此 xor-mapped-address 的 pair 已经连通性检查通过了,不需要再组重复的 pair。
6.5.2 Constructing a Valid Pair(构造有效 pair)
在连通性检查通过后,会构造一个 valid pair。构造 valid pair 的 local candidate 是从 binding response 得到的 mapped address;remote candidate 是 stun request 的 remote candidate。
每个 checklist 有一个 valid pair list,存储了 valid pair(连通性检查通过的 pair)。valid pair list 并不是直接存储这里构造的 valid pair。
由于 valid pair 的 local candidate 是 mapped address,而不是 stun request 的 local candidate,那么有如下几种情况(rfc8445 7.2.5.3.2 节):
- 如果 mapped address 与 stun request 的 local candidate 相同,那么将发送 stun request 的 candidate pair 加入到 valid pair list 中
- 如果不同,且 valid pair 与 checklist 中其它的 pair 相同,将匹配的 pair 加入到 valid pair list 中
- 如果没有 pair 与 valid pair 相匹配,那么需要计算此 valid pair 的优先级(具体参看文档),然后插入到 valid pair list 中
为什么不是直接将发送 stun binding request 的 pair 插入到 valid list 中呢?
6.5.3 Updating Candidate Pair States(更新所有 pairs 的状态)
将上一节选取的 valid pair 和发送 stun binding request 的 pair 都标记为 succeed 状态。
同时,需要将 pair foundation 相同的 pair 都从 frozen 状态更新为 waiting 状态。
6.6 Triggered Checks(触发请求)
作为 ice agent server 端,在收到 stun binding request 后,会构造一个 pair,local candidate 是接收数据的 ip:port 地址对,remote candidate 是 ice agent 看到的发送数据的 ip:port 地址对。
ice agent server 会将此 pair 与 local candidate pairs 进行对比(rfc8445 7.3.1.4 节):
- 如果此 pair 存在于 local candidate pairs 中:
- 如果此 pair 已经处于 succeed 状态,什么都不用做
- 如果此 pair 处于 In-progress 状态,取消 check 事务。并将此 pair 加入到 triggered-check queue 中,并将状态重置为 waiting 状态
- 如果此 pair 处于 Waiting、Frozen、Failed 状态,将此 pair 加入到 triggered-check queue 中,并将状态重置为 waiting 状态
- 如果此 pair 不存在于 local candidate pairs 中:
- 将此 pair 插入 checklist 中(host address 对应的 checklist)
- 同时将此 pair 插入到 triggered-check queue 中,并将状态设置为 waiting 状态
7. 提名
rfc8445 2.3节:
L R
- -
STUN request -> \ L's
<- STUN response / check
<- STUN request \ R's
STUN response -> / check
STUN request + attribute -> \ L's
<- STUN response / check
Figure 4: Nomination
7.1 ice controlling 端
ice controlling 端负责主动进行 nominating pair(rfc8445 8.1.1节):
- 从 valid list 中取一个 valid pair 之后,发送携带 USE-CANDIDATE 属性的 binding request(先放置到 triggered-queue 中)
- 在进行提名的时候,其它普通连通性检查也会继续
- 一次只会挑选一个 valid pair 进行提名,只有当前提名的 pair 失败后,才会挑选下一个,否则,不能同时提名多个 pair
rfc8445 中对于具体选取哪一个 valid pair 来进行提名没有具体说明,只说根据本地策略来进行挑选。可以参看:https://github.com/ireader/sdk/tree/master/libice/src/ice-checklist.c 中 ice_checklist_conclude() 函数的实现。
此函数的实现是从所有 valid pair 中挑选优先级最高的来优先进行提名检查。
7.1.1 提名成功
一个 checklist 的某个 component 提名成功:
- 此 component 下的其它连通性检查事务都会取消
- checklist 和 triggered-check queue 中同一个 component 的 pair 都会被删除
- valid list 清空,不会再进行任何提名操作
同时注意,只有一个 checklist 的所有 components(rtp、rtcp)都成功提名一个 pair,整个 checklist(stream) 才会提名成功
7.1.2 提名失败
将此 pair 标记为 failed,并继续从 valid list 中挑选下一个 valid pair。
如果某些 stream 提名失败,或者某个 stream 下的某个 component 提名失败,需要根据本地策略,进行 offer-answer 重协商或者直接通话失败。
7.2 ice controlled 端
ice controlled 端被动接收 nominating pair:
- 如果提名的 pair 触发了 triggered check,那么 pair 将会添加到 triggered-check queue 中,成功响应后,才会提名成功(回应 nomination check,参看 rfc8445 7.3.1.4 节)
- 如果提名的 pair,匹配 valid list 中的 pair,则提名成功
- 一个 checklist 的某个 component 提名成功,此 component 下的其它连通性检查事务都会取消,component 相关的 checklist 和 triggered-check queue 中的 pair 会被删除
7.3 aggressive nomination(激进提名)
L R
- -
STUN request + flag -> \ L's
<- STUN response / check
<- STUN request \ R's
STUN response -> / check
Figure 5: Aggressive Nomination
即 ice controlling 端无需等待 valid pair 中连通性检查成功的 pair,而是在每次发起连通性检查的时候就携带 nomination 标志,一般最先提名成功的 pair 就是最终选择的 pair,如果有多个 pair 提名成功,选择优先级最高的。
在 rfc8445 中已经废弃了此方法,但是实际上很多 ice 实现都会支持此种提名。
7.4 为什么 ice handshake 需要双端 check
在 L's check 中,R 回复的 stun response,L 不一定收到了,所以需要双端 check。
8. ice restart(ice 重启)
ice 在如下情况下需要重启:
- 提名成功的 pair,需要一直保活。如果保活失败,或者收到 ICMP 错误,应该根据本地策略重启 ice
- 本机网络发生变化,例如切换 wifi 等,host address 的定时检查逻辑检测出来网络变化,也应该根据本地策略重启 ice
ice 重启需要重新进行 offer-answer 协商,只要 sdp 中的 ice 认证字段 ice-ufrag 和 ice-pwd 发生了变化,那么就表明 ice 需要重启。
9. ice-lite 的考虑
9.1 rtc 端口复用
部署在公网上的 rtc 服务器,如果每次媒体通道建立都要消耗一个新的端口,可能会给运维安全和维护带来很多问题。因此,许多运行在 ice-lite 的服务端 rtc 服务都会只开放一个或两个端口来复用所有媒体数据传输通道。
那么一个端口如何区分不同会话呢?:
- offer-answer 中,能够得到 peer-ice-ufrag 和 local-ice-ufrag,将其组成 key,并绑定会话,得到 ufrag_key-session
- 媒体连接建立首先会收到 stun binding request,如果携带 USE-CANDIDATE Attribute,那么取出其中的 USERNAME Attribute,作为 ufrag_key 查找已经存在的会话 session
- 同时将此 stun binding request 的网络地址(transport、peer_ip、peer_port、local_ip、local_port 五元组) 通过本地规则映射成一个数值(数值比字符串容易加快比较),并绑定会话,得到 address_key-session
- 后续收到 RTP/RTCP 包后,都通过同样的规则将网络地址映射成数值,然后作为 address_key 查找已经存在的会话 session