WebRTC ICE概述
一、概述
ICE全称Interactive Connectivity Establishment:交互式连通建立方式。
ICE参照RFC5245建议实现,是一组基于offer/answer模式解决NAT穿越的协议集合。
它综合利用现有的STUN,TURN等协议,以更有效的方式来建立会话。
ICE是通过综合运用STUN,TURN,RSIP等NAT穿透方式,使之能在最适合的情况下工作,以弥补单独使用其中任何一种所带来的固有缺陷。对于SIP来说,ICE只需要定义一些SDP(Sessionescription Protoc01)附加属性即可,对于别的多媒体信令协议也需要制定一些相应的机制来实现。
ICE的算法可以分为以下几个流程:
(1)收集本地传输地址
会话者从服务器上获得主机上一个物理(或虚拟)接口绑定一个端口的本地传输地址。
(2)启动STUN
与传统的STUN不同,ICE用户名和密码可以通过信令协议进行交换。
(3)确定传输地址的优先级
优先级反映了UA在该地址上接收媒体流的优先级别,取值范围0到1之间,按照被传输媒体流量来确定。
(4)构建初始化信息(Initiate Message)
初始化消息由一系列媒体流组成,每个媒体流的任意Peer之间实现最人连通可能性的传输地址是由公网L转发服务器(如TURN)提供的地址。
(5)响应处理
连通性检查和执行ICE算法中描述的地址收集过程。
(6)生成接受信息(Accept Message)
若接受则发送Accept消息,其构造过程与InitiateMessage类似。
(7)接受信息处理
接受过程需要发起者使用Send命令,由服务器转发至响应者。
(8)附加ICE过程
Initiate或Accept消息交换过程结束后,双方可能仍将继续收集传输地址。
简单的说,就是SIP终端在注册时,访问STUN服务器;当发起呼叫信息(INVITE)或接收到
呼叫信息回应(200 OK)之前根据NAT/防火墙类型通过访问STUN服务器进行对RTP进行地址收集;
然后在RTP的地址端口启动接收线程RSTUN服务程序;最后发送SIP消息,收集的地址放列SDP消
息中的alt属性中。STUN(简单的用UDP穿透NAT),后面最大的区别是支持TCP穿透(即使用中
继穿透NAT)。STUN的扩展。简单的说,TURN与STURN的共同点都是通过修改应用层中的私网地
址达到NAT穿透的效果,异同点是TURN是通过两方通讯的“中间人”方式实现穿透。一般来说,实
现P2P通信无法实现时,TURN就会派上用场
二、STUN的报文结构为:
消息头
所有的STUN消息都包含20个字节的消息头,包括16位的消息类型,16位的消息长度和128位的事务ID。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0 0| STUN Message Type | Message Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Magic Cookie |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Transaction ID (96 bits) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| 2 byte attrtype 2 byte attrlength body |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
头两位必须是0,当网络中收到stun和其他数据包时,可以基于该两位的值是否为0判断是stun包。(当然这种判断方式不太严谨
- Message Type 总共占14位,其中方法Method 占12位,类型Class占两位。展开后具体格式如下
0 1
2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+
|M |M |M|M|M|C|M|M|M|C|M|M|M|M|
|11|10|9|8|7|1|6|5|4|0|3|2|1|0|
M11 到M0 表示12位的Method,C1和C0两位表示Class。
用12位表示Method,理论上可以表示2^12个Method,实际应用中主要用到一个Binding方法。
Class 占2位,可以表示2^2=4 种类型
四种类型分别为:request、sucess response、failure response、indication
具体如下:(注意协议文档中的0b00,代表二进制00)
对于每种Method都会对应请求、成功响应、错误响应、指示四种类型。Method和Class 拼接在一起组成Message Type
enum MessageType
{
// see @ https://tools.ietf.org/html/rfc3489#section-11.1
BindingRequest = 0x0001,
BindingResponse = 0x0101,
BindingErrorResponse = 0x0111,
SharedSecretRequest = 0x0002,
SharedSecretResponse = 0x0102,
SharedSecretErrorResponse = 0x0112,
};
Message Length
Message Length 表示除了20字节头以外的所有数据长度。由于STUN属性都是以4字节的倍数填充,因此这个字段最后两位总是等于零,这也是辨别STUN包的一个方法之一。
Magic Cookie
固定的4个字节,具体值为:0x2112A442
Magic Cookie用来和客户端映射的外网IP地址做异或,形成XOR-MAPPED-ADDRESS属性。见下文分析。
Transaction ID
12字节的事务ID。对于一个客户端,客户端和服务端的事务id相等,它被用来作为客户端的唯一标识。
stun协议20字节的消息头后,就是0到N个的属性消息。
三、属性消息
属性格式
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Value (variable) ....
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
获取AttrType 和 AtrrLenght方法:
四、介绍几个常用属性
USERNAME
USERNAME 是有sdp中的ice-ufrag拼接而成,具体的拼接格式为 answer sdp 中的ice-ufrag: offer sdp中的ice-ufrag
在服务器单端口改造时,由于所有客户端的stun包都发到服务器的同一端口,且客户端发送sdp信令(tcp或者websocket协议)的端口和发送stun包(udp协议)的端口不是同一个,所以可用USERNAME区分不同的客户端。
USE_CANDIDATE
USE_CANDIDATE 客户端的icecandidate。
ICE-CONTROLLING
用来表示通信双方的ice角色,ICE-CONTROLLING 表示控制端,用来选择最终进行连接性检测的候选地址对;ICE-CONTROLLED表示被控端,被告知最终用哪个地址对进行通信。
在实际通信过程中,双方都可能是ICE-CONTROLLING或者ICE-CONTROLLED,当双方值相同时就会产生冲突。为了解决这个冲突,引入一个 tie-breake字段,tie-breake值大的一端是控制端。
Attribute::FINGERPRINT
如果turn包属性中带了FINGERPRINT,需要信息校验。具体mediasoup中实现。
MESSAGE-INTEGRITY
消息完整性。
先解释HMAC-SHA1 算法。
HMAC 是一种基于密钥的报文完整性验证方法,其安全性是建立在Hash加密算法基础上的。HMAC利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。
SHA1 是一种哈希算法,可以生成一个被称为消息摘要的160位(20字节)散列值
stun包中被加密的具体数据为:
从stun头到该字段(包含该字段)的所有数据,加密中所用的密码是ice-pwd中表示的密码。
加密完成的值,作为MESSAGE-INTEGRITY的属性值保存到stun包中。
mediasoup中具体实现见:StunPacket.cpp 的Serialize()函数
stun包信息会大概会带以下信息:USERNAME, ICE_CONTROLLING, USE_CANDIDATE, PRIORITY, MESSAGE_INTEGRITY和FINGERPRINT。
五、stun包的合法性判断
mediasoup基于MESSAGE-INTEGRITY 和USERNAME来验证stun包的合法性。
StunPacket::Authentication StunPacket::CheckAuthentication(
const std::string& localUsername, const std::string& localPassword)
{
MS_TRACE();
switch (this->klass)
{
case Class::REQUEST:
case Class::INDICATION:
{
// Both USERNAME and MESSAGE-INTEGRITY must be present.
if (!this->messageIntegrity || this->username.empty())
return Authentication::BAD_REQUEST;
// Check that USERNAME attribute begins with our local username plus ":".
size_t localUsernameLen = localUsername.length();
if (
this->username.length() <= localUsernameLen || this->username.at(localUsernameLen) != ':' ||
(this->username.compare(0, localUsernameLen, localUsername) != 0))
{
return Authentication::UNAUTHORIZED;
}
break;
}
// This method cannot check authentication in received responses (as we
// are ICE-Lite and don't generate requests).
case Class::SUCCESS_RESPONSE:
case Class::ERROR_RESPONSE:
{
MS_ERROR("cannot check authentication for a STUN response");
return Authentication::BAD_REQUEST;
}
}
// If there is FINGERPRINT it must be discarded for MESSAGE-INTEGRITY calculation,
// so the header length field must be modified (and later restored).
if (this->hasFingerprint)
// Set the header length field: full size - header length (20) - FINGERPRINT length (8).
Utils::Byte::Set2Bytes(this->data, 2, static_cast<uint16_t>(this->size - 20 - 8));
// Calculate the HMAC-SHA1 of the message according to MESSAGE-INTEGRITY rules.
const uint8_t* computedMessageIntegrity = Utils::Crypto::GetHmacShA1(
localPassword, this->data, (this->messageIntegrity - 4) - this->data);
Authentication result;
// Compare the computed HMAC-SHA1 with the MESSAGE-INTEGRITY in the packet.
if (std::memcmp(this->messageIntegrity, computedMessageIntegrity, 20) == 0)
result = Authentication::OK;
else
result = Authentication::UNAUTHORIZED;
// Restore the header length field.
if (this->hasFingerprint)
Utils::Byte::Set2Bytes(this->data, 2, static_cast<uint16_t>(this->size - 20));
return result;
}
FINGERPRINT
指纹属性,用来检测数据传输过程中是否发生了错误。前面数据完整性是验证数据是否完整,这里是验证消息内部具体的数据是否出错了。
计算规则:
对FINGERPRINT以外的所有数据进行crc-32循环冗余校验(FINGERPRINT是stun包最后一个属性),并把得到的值与0x5354554e异或得到最终的值。
服务端收到stun包以后,再按同样的规则生成一遍,判断自己生成的值和FINGERPRINT属性中所带的值是否相等,相等说明数据正确,否则数据传输中发生了错误。
mediasoup中生成FINGERPRINT和判断FINGERPRINT是否相等的代码如下:
// Add MESSAGE-INTEGRITY.
if (addMessageIntegrity)
{
// Ignore FINGERPRINT.
if (addFingerprint)
Utils::Byte::Set2Bytes(buffer, 2, static_cast<uint16_t>(this->size - 20 - 8));
// Calculate the HMAC-SHA1 of the packet according to MESSAGE-INTEGRITY rules.
const uint8_t* computedMessageIntegrity =
Utils::Crypto::GetHmacShA1(this->password, buffer, pos);
Utils::Byte::Set2Bytes(buffer, pos, static_cast<uint16_t>(Attribute::MESSAGE_INTEGRITY));
Utils::Byte::Set2Bytes(buffer, pos + 2, 20);
std::memcpy(buffer + pos + 4, computedMessageIntegrity, 20);
// Update the pointer.
this->messageIntegrity = buffer + pos + 4;
pos += 4 + 20;
// Restore length field.
if (addFingerprint)
Utils::Byte::Set2Bytes(buffer, 2, static_cast<uint16_t>(this->size - 20));
}
else
{
// Unset the pointer (if it was set).
this->messageIntegrity = nullptr;
}
// Add FINGERPRINT.
if (addFingerprint)
{
// Compute the CRC32 of the packet up to (but excluding) the FINGERPRINT
// attribute and XOR it with 0x5354554e.
uint32_t computedFingerprint = Utils::Crypto::GetCRC32(buffer, pos) ^ 0x5354554e;
Utils::Byte::Set2Bytes(buffer, pos, static_cast<uint16_t>(Attribute::FINGERPRINT));
Utils::Byte::Set2Bytes(buffer, pos + 2, 4);
Utils::Byte::Set4Bytes(buffer, pos + 4, computedFingerprint);
pos += 4 + 4;
// Set flag.
this->hasFingerprint = true;
}
else
{
this->hasFingerprint = false;
}