穿越防火墙的奥秘:ICE 协议详解
“后”疫情时代,以线上为主的学习、工作、交流、娱乐方式成了常态,这一现象背后是实时音视频技术的不断创新和突破。为了给企业和开发者提供极致的音视频体验,拍乐云技术团队除了采用广布 DC,将服务下沉到最后一公里之外, 还会根据应用场景切换技术方案,如果仅有两个终端参与通信,会选择媒体直连方案以降低服务器开销。现在的电脑和设备通常都位于防火墙之后,无法简单建立直连,于是防火墙穿越技术应运而生。穿越防火墙的技术方案多种多样,本文将详细介绍其中一个框架——ICE 协议,帮助大家掌握防火墙穿越的基本流程。
#01
什么是直连?
直连模式也称为 P2P 模式(Peer to Peer 下文称 peer 为终端),而 P2P 模式成功的第一步就是要建立连接。如何让处在各自 NAT[RFC3489]后面的设备建立连接,也就是在系统设计的时候尽量提高 P2P 的连接成功率显得非常重要。接下来我们就是选一种标准协议[RFC 8445] ICE (Interactive Connectivity Establishment)进行分析,一探 NAT 后的设备是如何尝试 Peer to Peer 通信的。
(经典系统架构图)
#02
ICE 的建连过程
ICE 实现 NAT 穿透的所要完成的核心处理包括候选地址信息的收集,之后对收集到的地址进行排序、配对,然后执行连通性检查。
一个终端有多种候选传输地址(ip 地址和端口用于特定传输协议)用以与其他终端进行通信, 它可能包含:
o 直接连接的网络接口上的传输地址(网卡地址)
o NAT 公共端的翻译后的传输地址 (Server reflexive 服务反向地址)
o turn 服务分配的传输地址 (Relayed address 服务中继地址)
01
名词解释
Transport Address:包含 IP、port 和传输协议。
Candidate:除了 Transport Address 外还包括类型、优先级、foundation 还有 Base。
Base:Host candidate 关联一个 Server reflexive candidate 。
02
候选地址收集
终端必须确定所有的候选的地址。这些地址包括本地网络接口的地址和由它派生的其他所有地址。本地网络地址包括本地网卡地址、VPN 网络地址、MIP 网络地址等。派生地址指的是通过本地地址向 STUN 服务器发送 STUN 请求获得的网络地址,这些地址分为两类,一类是通过 STUN 的绑定得到的地址,称为服务器反向候选地址(Server Reflexive Candidates)或服务器反向地址。另一类是通过 turn 服务中继得到的,称为中继候选地址(Relay Candidates)。
终端通过各个主机候选地址向 STUN server 发送一个 Bind Request 和向 Turn server 发送 Allocate 消息获取额外的服务器反向地址和服务器中继地址。
下图描述的是通过 turn 服务同时发现 Server Reflexive Candidates 和 Relay Candidates 的过程:
(获取 server reflexive 和 server relay address 的时序图)
当终端发送 turn allocate 消息从 IP 为 IP 端口为 Port(主机候选地址)经过 NAT,NAT 将绑定一个 IP 为 IP1 和端口为 Port1 的地址,映射该候选服务器反向地址到主机候选地址。数据包从主机候选地址出来后,被 NAT 转换成服务器反向地址;最后发往目标地址。发往服务器反向候选地址的数据包,被 NAT 转换为主机候选地址,并转发到终端。
当终端和 STUN 服务器之间存在多重 NAT,那么 STUN 请求将会针对每一个 NAT 创建一个绑定,但是,只有最外部的服务器反向地址会被终端发现。如果终端不在任何 NAT 之后,那么,base 候选传输地址将与服务器反向地址相同,服务器反向地址可以忽略。
当 turn allocate 到达 turn server 后,turn 服务器为该请求分配一个 IP 为 IPy 端口为 Porty 的地址,并加此信息到产生的应答消息中,同时把服务器反向地址 IP1 及 Port1 也加到应答消息中发回终端。turn server 提供中继,把将要加入会话的终端携带的信息转发给对端,这样终端不但有了自己的本地候选地址信息,还有对端的候选地址信息,为后续的建连提供了地址信息基础。
03
候选地址排序、配对
终端 1 收集到所有的候选地址后,就将它们按优先级高低进行排序,再通过信令信道发送给终端 2。这些候选地址作为 SDP 请求的属性被传输。当终端 2 收到请求,它执行相同的地址收集过程,并且把它自己的候选地址作为响应消息发给请求者。这样,每个终端都将有一个完整的包含了双方候选地址的列表,然后准备执行连通性检查。
连通性检查的基本原理是:
o 按照优先顺序对候选地址进行排序;
o 利用每个候选地址发送一个检查包;
o 收到另一个终端的认可检查包。
下图是连接检测:
(有效地址对的筛选)
终端将本地地址集和远程地址集进行配对,如本地有 2 个地址,对端有 3 个地址,那么配成 2*3=6 对地址对。终端 A 选择本地的一个候选地址向终端 B 的的服务器反向地址发送一个 STUN 请求,并收到了 Stun 的 response,称该地址对是可接收的。当终端 A 地址对中的本地地址收到地址对中远程地址的一个 STUN 请求,并成功地响应,则称该地址对为可发送的。若一个地址对是可接收的,同时又是可发送的,则称该地址对是有效的,即这个地址对通过连通性检查。以上描述的是一个地址对的筛选过程,多个地址对同时进行多次这样的往返交互终端 A 和终端 B 就可以筛查出所有的有效地址对。
04
对候选地址进行排序
由于收集候选地址时,收集的是所有的候选地址,为了能够更快更好的找到能够正常工作的候选地址对,对所有组合进行排序是势在必行的。在此说明进行排序的两个基本原则:
o 终端为它的每个候选地址设置一个数值的优先级,这个优先级连同候选地址对一起发送给通信的对端。
o 综合本地的和远程的候选地址的优先级,计算出候选地址对的优先级,这样,双方的同一个候选地址对的优先级相同。以此排序,则通信双方的排序结果相同。
详细算法:见实现规范。
05
冻结候选
每个候选者都和一个叫 FOUNDATION 属性相关,两个候选者的 foundation 是“相同”的--是相同的主机候选者,且用了相同的 stun 服务协议。否则它们的 foundation 是不一样的 。候选地址对也有 foundation,只是和两个候选地址相关联。初始阶段,仅有唯一 foundation 的地址对进行检测,其他的对候选地址对都是处于“冻结”状态。当可连接检测成功后,在解冻和当前 foundation 一样的候选地址对,这样可以避免重复检查表面上看起来更可能连接成功,但实际上会失败的候选地址对。
#03
ICE 的实现规范
01
Foundations 计算
两个候选地址要有相同的 foundation ID:
o 一样的类型(主机、服务器映射、中继、对端映射)。
o Base 有相同的 IP(端口可以不一样)。
o 用的相同的传输协议。
o 服务器反向和中继地址,当 stun 或则 turn server 获取到它的 IP 是一样的。
否则就要用不同的 ID。
02
候选地址对的排序
候选地址 priority 的计算公式:
priority = (2^24)*(type preference) + (2^8)*(local preference) + (2^0)*(256 - component ID)
类型首选项必须是 0 到 126(含 0 和 126)之间的整数,0 为最低优先级,126 为最大优先级。
本地首选项必须是 0 到 65535(含 0 和 65535)之间的整数。0 为最低优先级,65535 为最高优先级。
The component ID 必须是 0 到 256(含 0 和 256)之间的整数。
类型首选项和本地首选项的选择准则:
第一标准:标准是媒体中介的运用,比如 turn server, VPN server 或 NAT,中继地址就是这种类型,另一种就是主机地址从 VPN 接口得到的。中继地址的类型首选项值必须小于主机地址,建议用主机候选地址上的值为 126、100 给服务器反向地址、110 给远端地址、0 给中继地址。
其他的标准有基于 IP 地址簇、安全及网络拓扑(这里由于编幅原因就不详细展开)。
03
角色的选择
对于 ICE 流程中的每个会话,每个终端都扮演一个角色。有两个角色--控制和被控制。被控制终端负责最后一对候选配对的选择,用于通信。这意味着提名候选地址对,用于每个媒体流。控制终端被告知哪些候选对用于每个媒体流。
决定角色的规则如下:
o 双端都是全实现:一端在发起请求的时候是控制者,另外一端则是被控制者,双端都要跑 ICE 的状态机,做可连接性检测。
o 一端是全实现, 另外一端是轻实现:全实现的发起请求作为控制者跑 ICE 的状态机,做可连接性检测;轻实现者作为被控制者。
o 双端都是轻实现:一端在发起请求的时候是控制者,另外一端则是被控制者。
04
角色冲突的解决
在一些特殊的流程下,可能会导致会话的双方都认为自己是控制者或者被控制者。如下图所示:
(角色冲突的情景)
controller 为 B2BUA ,作为中间关联起 A 和 B 之间的呼叫。对于 A 和 B 来说,都是 offerer,这样会导致均认为自己在 ICE 流程中扮演 controlling 的角色。
为了解决角色冲突,在 connectivity check 的阶段,发送的 bind request 要求携带 role 相关的 STUN 属性,ICE-CONTROLLED 或者是 ICE-CONTROLLING,这两个属性都会携带一个 Tie breaker(取值 0- 2^64 - 1)这样的字段,其中包含 一个本机产生的随机值。收到该 bind request 的一方会检查这两个字段,如果和当前本机的 role 冲突,则检查本机的 tie breaker 值和消息中携带的 tie breaker 值进行判定本机合适的 role。判定的方法为 Tie breaker 值大的一方为 controlling。如果判定本端变更角色,就会直接修改;如果判定对端变更角色,则对此 bind request 发送 487 错误响应,收到此错误响应的一端改变角色就可以了。
(这里就用到了 STUN 请求新增的属性 0x8029 ICE-CONTROLLED 0x802A ICE-CONTROLLING 以及 STUN 错误反馈 487 Role Conflict: 终端指定的角色和 server 指定的角色冲突)。
05
候选地址对优先级计算及排序
一旦候选地址对确定好后,就要给它们计算优先级。G 为控制端地址的优先级,D 为被控制端的地址优先级。
候选地址对的优先级计算公式:
pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0)
一旦分配了候选对优先级,就会候选对按优先级降序配对。如果两对具有相同的优先级,它们之间的顺序是任意的。
06
候选地址对状态机
(地址对状态转换图)
o 开始的时候终端将所有的候选地址对的状态设置为 Frozen。
o 终端检查第一个媒体流中的列表:
对于所有具有相同 Foudation 的候选对,把最小 componentID 的地址对设置为 Wait 状态,如果数量
超过一个,那么具有最高优先级的地址对设置为 Wait 状态。
o 处理过程的结果要么成功要么失败。
07
连接检查
此流程只有全量实现才有,又分为平常检查和触发检查,两者都是由定时驱动。
终端持有一个先进先出队列,称为触发队列,里面放着下次有机会触发的 candidate pair。当定时器触发后,终端从触发队列里拿出最上面的 candidate pair,执行连接检查,设置该 candidate pair 状态为 In Progress。如果触发队列为空,就执行平常检查。
一旦终端完成组织好 candidate pairs 后,就给个这些活动的检查列表设置定时器,有 N 个就设置 N 个定时器,时间间隔为 Ta*N 秒。
当定时器触发后但没有触发检查可发送,终端必须切换到平常检查流程如下:
08
ICE Restart
终端可能为媒体流重启 ICE,那么原来的又变回到了新的状态。和新的会话不同的点就是在重启的过程中,媒体依然可以从前面有效的地址对发送。一个终端必须重启 ICE,当列情况出现的时候:
o 生成 offer 是为了更改指定的媒体流。换句话说,如果终端想生成一个更新的 offer,而 ICE 又没有使用,将会为该媒体流重启 ICE
o 终端更改它的实现级别,这通常只发生在第三方呼叫控制用例中。
这些规则意味着将 c 行中的 IP 地址设置为 0.0.0.0 将导致 ICE 重启。因此,ICE 实现不得使用此机制进行呼叫保持,而必须使用 sdp 协议中的 a=inactive 和 a=sendonly 。要重新启动 ICE,代理必须更改 offer 中媒体流的 ice-pwd 和 ice-ufrag。
09
地址信息的传递
我们都知道 ICE 协议只负责收集通信地址对它们进行排序,要让通信双方都知道自己处在的网络环境和通信地址,还要通过服务器中继,多媒体通信一般都通过 SDP[ RFC 4566]协议进行封装。下面是用于 ICE 协议的 attribute extensions.
a=ice-pwd:asd88fgpdd777uzjYhagZg a=ice-ufrag:8hhY
a=candidate:1 1 UDP 2130706431 10.0.1.1 8998 typ host 0 network-id 1 network-cost 10 a=candidate:2 1 UDP 1694498815 192.0.2.3 45664 typ srflx raddr 1 network-id 2 network-cost 50
sdp 中传递 ice-ufrag 和 ice-pwd 用于 stun 信息的安全有效校验。candidate 属性携带有效的通信地址信息,依次是从 1 开始递增的 component-id、通信协议(tcp 或 udp)、foundation、IP、port、type、relate_address、network-id 和 network-cost。
#04
总结
本文介绍了几个 ICE 协议的重要基本知识,而它只是建立 P2P 的框架,要配合其他的协议才能满足直连需求,而且 ICE 协议本身涵盖了很多主题和实现规范:分为轻量和全量的实现。如果大家对完整的过程和描述感兴趣,可以认真阅读 RFC 文档。
参考文献:
STUN:https://datatracker.ietf.org/doc/rfc5389/
评论