ICE 详解
# 简介
# ICE简介
ICE的全称Interactive Connectivity Establishment
(互动式连接建立),由IETF的MMUSIC工作组开发出来的,它所提供的是一种框架,使各种NAT穿透技术可以实现统一。
ICE跟STUN和TURN不一样,ICE不是一种协议,而是一个框架(Framework),它整合了STUN和TURN。
# 框架图
如下:其中所有的服务Relay Server与STUN Server都可以部署到同一服务器上。
- 双方通信的Peer 有两台机子A和B ,他们都是在NAT之后,并且这两个终端都会在NAT后面形成一个映射后的公网的IP地址。
- 有两个NAT,在NAT外面有两个 STUN 服务,这里的STUN服务主要用于Peer终端进行NAT穿越使用的(去判断NAT类型和获取终端在公网中的IP)。STUN 服务可以有两个,也可以是一个或多个。
- Relay server,即 TURN Server,具有中继的功能,大多数情况下也具有STUN Server的功能。
# 通讯流程
那我们就来看看这ICE是如何进行工作并且使这个两个终端最终进行媒体流的通讯的?两侧终端都是有网卡(IP),可以进行通讯。让这个终端去得到所有能够连接到这个终端B的通路。那这个终端都有哪些通路呢?
- 内网直接通信:如果两个对端是在同一个局域网内,那么这俩就直接通过这个本地的IP地址就可以进行通讯。
- 穿越NAT:终端首先访问STUN服务,通过STUN服务,能获取到NAT映射后的终端的公网地址(IP+端口)。并且终端双方可以通过服务器,获取得到对方的公网地址,就可以进行NAT穿越。那如果穿越成功了,他们也直接就能通过NAT进行通讯了。
- TURN服务进行中继:就是走中继。那这个终端通过NAT将数据转给TURN中继,中继服务,再向另外一个端去转发数据。
- ICE框架:ICE的基本的功能就是:收集终端双方所有的通路(因为终端可能包含多个网卡,必定包含多个通路);对所有通路进行检测,看能不能通,那通了之后,那么ICE的这个工作就算结束了。
# 基础概念
ICE的全称为Interactive Connectivity Establishment
,即交互式连接建立。初学者可能会将其与网络编程的ICE 弄混,其实那是不一样的东西。在网络编程中,如C++的ICE库,都是指Internet Communications Engine,是一种用于分布式程序设计的网络通信中间件。我们这里说的只是交互式连接建立。
ICE 是一个用于在offer/answer
模式下的NAT传输协议,主要用于UDP下多媒体会话的建立,其使用了STUN协议以及TURN 协议,同时也能被其他实现了offer/answer
模型的的其他程序所使用,比如SIP(Session Initiation Protocol
).
使用offer/answer
模型(RFC3264)的协议通常很难在NAT之间穿透,因为其目的一般是建立多媒体数据流,而且在报文中还携带了数据的源IP和端口信息,这在通过NAT时是有问题的。RFC3264还尝试在客户端之间建立直接的通路,因此中间就缺少了应用层的封装。这样设计是为了减少媒体数据延迟,减少丢包率以及减少程序部署的负担。然而这一切都很难通过NAT而完成。
有很多解决方案可以使得这些协议运行于NAT环境之中,包括应用层网关(ALGs),Classic STUN以及Realm Specific IP+SDP
协同工作等方法。不幸的是,这些技术适应性很差,在某些网络拓扑下工作很好,而在另一些环境下表现又很差,因此我们需要一个单一的、可自由定制的解决方案。以便能在所有环境中都能较好工作。
# ICE Candidate
ICE Candidate 是什么?
ICE Candidate 是一个地址,包含协议、IP、端口、类型等。其中类型是主机类型(是经过NAT反射后地址还是中继地址,或者其他类型)
获取到这些candidate之后,终端之间要交换这些candidate,那使用什么进行交换呢?通过候选者对(双方各自取一个candidate,组成候选者对)形成通路(是否可以互通,还需要进行连通性检查),使用SDP,SDP 是对于这个媒体信息以及网络信息的一个描述规范。
这个规范,最终是通过信令将这个SDP发送给对方,双方拿到各自的对方的SDP,那么就能识别出对方都有哪些通路,并且同时了解自己有哪些通路。
Candidate类型包含以下种类:
- 主机候选者:主机网卡的IP地址和端口。
- 反射候选者:NAT反射之后的公网IP地址和端口(不是伪公网)。
- 中继候选者:TURN服务提供的IP地址和端口。
# ICE具体工作
ICE 主要有如下工作:
- 收集 Candidate
- CandidatePair 排序
- 连通性检查
为什么要进行 CandidatePair 排序?
形成多组候选者对之后,要根据一套算法进行排序,优先级最高的候选者对先做测试,因为有可能是先通的,这样就节省了时间。
# Candidate的获取
Candidate 关系图:
- 本地即为agent实际就相当于一个终端,有本地网卡,网卡有IP,就是host的类型的主机候选者。
- 经过NAT到TURN服务或者STUN服务。(一般情况下,STUN服务和TURN服务是部署在同一台机子上的,程序同时具有STUN和TURN两者的功能。)因此,通过以上服务就可以拿到主机的映射地址,就是NAT之后映射的公网地址(IP和端口),也就是反射候选者。
- 通过向TURN服务发送一个allocate请求,如果成功了TURN服务会开通中继地址(IP地址和端口),作为中继候选者。
这三种类型就是通过以上方式获取,通过发送一个请求可以获取这几种的候选者。
总结一下,收集 Candidate 包括如下三种类型:
- 主机候选者(Host Candidate):获取本机所有IP和指定端口。
- 反射候选者(Reflexive Candidate):是通过向STUN/TURN服务发送请求的时候获取到的映射后的NAT转换后的公网IP和端口。
- 中继候选者(Relay Candidate):是通过向TURN服务发送一个allocate的请求,为数据的转发开通一个新的IP和端口,就是中继地址。
拿到这些候选者之后,需要通过SDP交换信息。
# SDP格式
ICE信息的描述格式通常采用标准的SDP,其全称为Session Description Protocol,即会话描述协议。
SDP(Session Description Protocol)是一种信息格式的描述协议,本身不属于传输协议,可以被其他传输协议用来交换必要的信息,如SIP和RTSP等。
一个SDP会话描述包含如下部分:
- 会话名称和会话目的。
- 会话的激活时间。
- 构成会话的媒体(media)。
- 为了接收该媒体所需要的信息(如地址,端口,格式等)。
因为在中途参与会话也许会受限制,所以可能会需要一些额外的信息:
- 会话使用的的带宽信息
- 会话拥有者的联系信息
一般来说,SDP必须包含充分的信息使得应用程序能够加入会话,并且可以提供任何非参与者使用时需要知道的资源状况,后者在当SDP同时用于多个会话声明协议时尤其有用。
SDP是基于文本的协议,使用ISO 10646
字符集和UTF-8
编码。SDP字段名称和属性名称只使用UTF-8
的一个子集US-ASCII
,因此不能存在中文。虽然理论上文本字段和属性字段支持全集,但最好还是不要在其中使用中文。SDP会话描述包含了多行如下类型的文本:
<type>=<value>
其中type是大小写敏感的,其中一些行是必须要有的,有些是可选的,其中可选的元素标记为*,所有元素都必须以固定顺序给出,固定的顺序极大改善了错误检测,同时使得处理端设计更加简单。
一个标准的SDP案例:
v=0
o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5
s=SDP Seminar
i=A Seminar on the session description protocol
u=http://www.example.com/seminars/sdp.pdf
e=j.doe@example.com (Jane Doe)
c=IN IP4 224.2.17.12/127
t=2873397496 2873404696
a=recvonly
m=audio 49170 RTP/AVP 0
m=video 51372 RTP/AVP 99
a=rtpmap:99 h263-1998/90000
2
3
4
5
6
7
8
9
10
11
12
13
- v:version,版本信息,一般都是0;
- o:owner,表示此SDP归谁所有,比如案例中主机名字jdoe,有多个系列号,最后包含一个IP地址。(注意:这IP地址并不一定是最终要进行传输的IP的地址,在WEBRTC里并不是用这个IP,而是使用candidate中的IP。)
- c:connection,表示连接这个网络的IPV4。
- m:media,表示本次交互的媒体信息,如audio也就是音频,它使用的是RTP的协议,
- a:attribute,表示上条信息所使用的参数(属性),如对于这个音频它有一个参数a=rtpmap,即音频的编码方式是PCMU,采样率是8000。
最重要的是最后两行,它检测到有两种Candidate:
- 第一条是UDP的,IP 是 10.0.1.1端口是 8998,类型是host;
- 第二种也是UDP的,IP是192.0.2.3端口是45664,类型是穿越NAT的映射地址。
这里没有中继地址,即同一局域网内可以互通,或者就是穿越NAT走P2P。
标准的SDP格式如下:
会话描述:
v= (protocol version,协议版本)
o= (originator and session identifier,发起者和会话标识符)
s= (session name,会话名称)
i=* (session information,会话信息)
u=* (URI of description,URI 描述)
e=* (email address,邮件地址)
p=* (phone number,电话号码)
c=* (connection information -- not required if included in
all media,连接信息)
b=* (zero or more bandwidth information lines,带宽信息)
One or more time descriptions ("t=" and "r=" lines; see below)
z=* (time zone adjustments,时区调整)
k=* (encryption key,加密密钥)
a=* (zero or more session attribute lines,会话属性)
Zero or more media descriptions
时间信息描述:
t= (time the session is active,会话处于活动状态的时间)
r=* (zero or more repeat times)
多媒体信息描述(如果有的话):
m= (media name and transport address,媒体名称和传输地址)
i=* (media title,媒体标题)
c=* (connection information -- optional if included at
session level,连接信息)
b=* (zero or more bandwidth information lines,带宽信息)
k=* (encryption key,加密密钥)
a=* (zero or more media attribute lines,媒体属性)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
所有元素的type都为小写,并且不提供拓展。但是我们可以用a(attribute)字段来提供额外的信息。
参考:
# Candidate Pair
获取得到所有的candidate之后,就要形成候选对,通过检测连通性之后,形成通路。
如何形成 CandidatePair?
- 一方收集到所有 Candidate 之后通过信令传输给对端;
- 对端收到后开始收集 Candidate;
- 双方都完成 Candidate 收集之后,将 Candidate 形成 CandidatePair。
# 连通性检测
检查 CandidatePair 连通性包括如下三个步骤:
- 对所有 CandidatePair 进行优先级排序;
- 对排序后的 CandidatePair 进行发送检查,检查是否能成功发送请求;
- 对排序后的 CandidatePair 进行接受检查,检查是否能成功接收到回应。
在实际过程中,为了节省时间,发送跟接收是串行的。
# 原理
# Offer/Answer模型
SDP用来描述多播主干网络的会话信息,但是并没有具体的交互操作细节,因此 RFC3264 定义了一种基于SDP的 offer/answer
模型。在该模型中,会话参与者的其中一方生成一个SDP报文构成offer,其中包含了一组offerer希望使用的多媒体流和编解码方法,以及offerer用来接收改数据的IP地址和端口信息。offer传输到会话的另一端(称为answerer),由answerer生成一个answer,即用来响应对应offer的SDP报文。answer中包含不同offer对应的多媒体流,并指明该流是否可以接受。
RFC3264只介绍了交换数据过程,而没有定义传递offer/answer报文的方法,后者在RFC3261/SIP
即会话初始化协议中描述。值得一提的是,offer/answer
模型也经常被SIP作为一种基本方法使用。offer/answer
模型在SDP报文的基础上进行了一些定义,工作过程不在此描述,需要了解细节的朋友可以参考RFC3261。
# ICE工作流程
一个典型的ICE工作环境如下,有两个端点L和R,都运行在各自的NAT之后(他们自己也许并不知道),NAT的类型和性质也是未知的。 L和R通过交换SDP信息在彼此之间建立多媒体会话,通常交换通过一个SIP服务器完成:
+-----------+
| SIP |
+-------+ | Srvr | +-------+
| STUN | | | | STUN |
| Srvr | +-----------+ | Srvr |
| | / \ | |
+-------+ / \ +-------+
/<- Signaling ->\
/ \
+--------+ +--------+
| NAT | | NAT |
+--------+ +--------+
/ \
/ \
/ \
+-------+ +-------+
| Agent | | Agent |
| L | | R |
| | | |
+-------+ +-------+
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ICE的基本思路是,每个终端都有一系列传输地址(包括传输协议,IP地址和端口)的候选,可以用来和其他端点进行通信。
其中可能包括:
- 直接和网络接口联系的传输地址(host address)
- 经过NAT转换的传输地址,即反射地址(server reflective address)
- TURN服务器分配的中继地址(relay address)
虽然潜在要求任意一个L的候选地址都能用来和R的候选地址进行通信。但是实际中发现有许多组合是无法工作的。举例来说,如果L和R都在NAT之后而且不处于同一内网,他们的直接地址就无法进行通信。ICE的目的就是为了发现哪一对候选地址的组合可以工作,并且通过系统的方法对所有组合进行测试(用一种精心挑选的顺序)。
为了执行ICE,客户端必须要识别出其所有的地址候选,ICE中定义了三种候选类型,有些是从物理地址或者逻辑网络接口继承而来,其他则是从STUN或者TURN服务器发现的。很自然,一个可用的地址为和本地网络接口直接联系的地址,通常是内网地址, 称为HOST CANDIDATE,如果客户端有多个网络接口,比如既连接了WiFi又插着网线,那么就可能有多个内网地址候选。
其次,客户端通过STUN或者TURN来获得更多的候选传输地,即SERVER REFLEXIVE CANDIDATES
和RELAYED CANDIDATES
,
如果TURN服务器是标准化的,那么两种地址都可以通过TURN服务器获得。当L获得所有的自己的候选地址之后,会将其按优先级排序,然后通过signaling通道发送到R。候选地址被存储在SDP offer报文的属性部分。当R接收到offer之后,就会进行同样的获选地址收集过程,并返回给L。
这一步骤之后,两个对等端都拥有了若干自己和对方的候选地址,并将其配对。组成CANDIDATE PAIRS
。为了查看哪对组合可以工作,每个终端都进行一系列的检查。每个检查都是一次STUN request/response
传输,将request从候选地址对的本地地址发送到远端地址。连接性检查的基本原则很简单:
- 以一定的优先级将候选地址对进行排序;
- 以该优先级顺序发送checks请求;
- 从其他终端接收到checks的确认信息。
两端连接性测试,结果是一个4次握手过程:
L R
- -
STUN request -> \ L's
<- STUN response / check
<- STUN request \ R's
STUN response -> / check
2
3
4
5
6
7
8
值的一提的是,STUN request的发送和接收地址都是接下来进多媒体传输(如RTP和RTCP)的地址和端口,所以,客户端实际上是将STUN协议与RTP/RTCP
协议在数据包中进行复用(而不是在端口上复用)。
由于STUN Binding request用来进行连接性测试,因此STUN Binding response中会包含终端的实际地址,如果这个地址和之前学习的所有地址都不匹配,发送方就会生成一个新的candidate,称为PEER REFLEXIVE CANDIDATE
,和其他candidate一样,也要通过ICE的检查测试。
# 连接性检查(Connectivity Checks)
所有的ICE实现都要求与STUN(RFC5389)兼容,并且废弃Classic STUN
(RFC3489)。ICE的完整实现既生成checks
(作为STUN client
),也接收checks
(作为STUN server
),而lite实现则只负责接收checks。这里只介绍完整实现情况下的检查过程。
为中继候选地址生成许可(Permissions)。
从本地候选往远端候选发送
Binding Request
。
在Binding请求中通常需要包含一些特殊的属性,以在ICE进行连接性检查的时候提供必要信息。
PRIORITY
和USE-CANDIDATE
终端必须在其request中包含PRIORITY属性,指明其优先级,优先级由公式计算而得。如果有需要也可以给出特别指定的候选(即USE-CANDIDATE属性)。
ICE-CONTROLLED
和ICE-CONTROLLING
在每次会话中,每个终端都有一个身份。总共有两种身份,即受控方(controlled role)和主控方(controlling role)。主控方负责选择最终用来通讯的候选地址对,受控方被告知哪个候选地址对用来进行哪次媒体流传输,并且不生成更新过的offer来提示此次告知。发起ICE处理进程(即生成offer)的一方必须是主控方,而另一方则是受控方。如果终端是受控方,那么在request中就必须加上ICE-CONTROLLED
属性,同样如果终端是主控方,就需要ICE-CONTROLLING属性。
- 生成
Credential
作为连接性检查的Binding Request
必须使用STUN的短期身份验证。验证的用户名被格式化为一系列username段的联结,包含了发送请求的所有对等端的用户名,以冒号隔开;密码就是对等端的密码。
- 处理Response。
当收到Binding Response
时,终端会将其与Binding Request
相联系,通常通过事务ID。随后将会将此事务ID与候选地址对进行绑定。
- 失败响应
如果STUN传输返回487(Role Conflict)错误响应,终端首先会检查其是否包含了ICE-CONTROLLED
或ICE-CONTROLLING
属性。如果有ICE-CONTROLLED
,终端必须切换为controlling role
;如果请求包含ICE-CONTROLLING
属性,则必须切换为controlled role
。切换好之后,终端必须使产生487错误的候选地址对进入检查队列中,并将此地址对的状态设置为Waiting
。
成功响应,一次连接检查在满足下列所有情况时候就被认为成功:
- STUN传输产生一个
Success Response
。 - response的源IP和端口等于
Binding Request
的目的IP和端口。 - response的目的IP和端口等于
Binding Request
的源IP和端口。
- STUN传输产生一个
终端收到成功响应之后,先检查其mapped address
是否与本地记录的地址对有匹配,如果没有则生成一个新的候选地址,即对等端的反射地址。如果有匹配,则终端会构造一个可用候选地址对(valid pair)。通常很可能地址对不存在于任何检查列表中,检索检查列表中没有被服务器反射的本地地址,这些地址把它们的本地候选转换成服务器反射地址的基地址,并把冗余的地址去除掉。
# 总结
本文介绍了一种完整的NAT环境通信解决方案ICE,并且对其中涉及到的概念SDP和offer/answer模型也作了简要介绍。ICE是使用STUN/TURN工具性质的最主要协议之一,其中TURN一开始也被设计为ICE协议的一部分。值的一提的是,本文只是对这几种协议作了概述性的说明,而具体工作过程和详细的属性描述都未包含,因此如果需要根据协议来实现具体的应用程序,还需要对RFC的文档进行仔细阅读。