目录
一. 前言
二. Lite ICE流程
三. STUN协议说明
STUN Header
STUN Body
四. mediasoup Lite ICE实现源码剖析
一. 前言
ICE 是一种交互式建立连接的流程协议。ICE 有两种模式(Full ICE 和 Lite ICE),Full ICE 要求建立连接的双方都要执行连通性检测,而 Lite ICE 则只要求响应 STUN binding request 信息即可,它不需要添加候选者并对候选者进行连通性检测。
媒体服务器一般都使用的是 Lite ICE 实现,因为媒体服务器通常部署在公网,它的网络没有限制,因此一般只需要客户端检测与媒体服务器提供的候选者地址是连通的即可。
二. Lite ICE流程
mediasoup 客户端要进行推流首先要创建通道,创建通道是通过 createWebRtcTransport 信令完成的,该信令返回的信息包含 {id, iceParameters, iceCandidates, dtlsParameters, sctpParameters},如下是一个示例值,包含了 ICE 的用户名,密码,使用的是 Lite ICE,以及候选者地址,优先级,协议等。
示例:{ id: ‘c0fe3f31-a764-40ff-88ff-69c14b83afda’,
iceParameters: { iceLite: true, password: ‘0pcc8bpoj1ug2s30p6ldh4qlgan1n5wd’, usernameFragment: '6jeiq5rqh7ke06dqzgnvzmg3iwtjxbqr' },
iceCandidates: [{"foundation":"udpcandidate","ip":"10.211.55.5","port":44445,"priority":1076302079,"protocol":"udp","type":"host"}],
dtlsParameters: { fingerprints: [ {"algorithm":"sha-224","value":"21:81:82:05:AD:52:C3:64:C1:F8:FF:44:65:79:97:51:85:D1:7A:20:13:6F:B0:B6:25:EA:B4:46"}, ... ], role: 'auto' },
sctpParameters: { MIS: 1024, OS: 1024, isDataChannel: true, maxMessageSize: 262144, port: 5000, sctpBufferedAmount: 0, sendBufferSize: 262144 } }
mediasoup 的 WebRtcTransport 对应 webrtc 的 RTCPeerConnection,mediasoup 客户端收到 createWebRtcTransport 的返回结果后会创建 WebRtcTransport(即对应一个 RTCPeerConnection),然后将 createWebRtcTransport 信令返回的 ICE 等参数信息设置到 remoteSdp 中,在媒体协商的时候将 remoteSdp 设置为 RTCPeerConnection 的remoteDescription,之后就开始发送 binding request 进行连通性检测。
设置的 remoteSdp 部分内容如下所示,可以看到 usernameFragment 的内容在 a=ice-ufrag,password 的内容在 a=ice-pwd 中,a=candidate 的内容为 iceCandidates 的内容。
抓包分析 Lite ICE 流程如下所示,mediasoup 客户端发送 STUN binding request 消息,携带 USERNAME, GOOG-NETWORK-INFO, ICE-CONTROLLING, PRIORITY, MESSAGE-INTEGRITY 以及 FINGERPRINT 属性。
mediasoup 服务端收到 STUN binding request 消息后,回复 STUN binding response 消息,携带 XOR-MAPPED-ADDRESS, MESSAGE-INTEGRITY, FINGERPRINT 属性。
之后 mediasoup 客户端发送 DTLS Client Hello 消息,说明客户端收到服务器回复的 STUN binding response 消息后 ICE 状态已经是成功连接的状态了。
三. STUN协议说明
STUN工作原理 这篇博客描述了 STUN 报文的格式,ICE 实现要求遵循 RFC5389 而不是 RFC3489,RFC5389 的 STUN 报文格式如下所示。
STUN Header
STUN Message Type 的 14bit 含义如下,M11-M0 表示 Method,目前只有 Binding 一种 Method,C1C0 表示 Class,C1C0=0b00 表示请求,C1C0=0b01 表示指示,C1C0=0b10 表示请求成功的响应,C1C0=0b11 表示请求失败的响应。
Message Length 表示 STUN 报文 Body 的长度
Magic Cookie 固定为 0x2112a442
Transaction ID 表示事务 ID,用于关联请求和响应,响应携带事务 ID 以表明这是对哪个请求的响应
STUN Body
STUN 消息体的内容是一系列属性,每个属性都是 Type, Length, Value 结构,我们主要讲解 Lite ICE 流程涉及到的属性含义,注意下面的属性值不完全属于 RFC5389 规范的定义,例如 PRIORITY,USE-CANDIDATE,ICE-CONTROLLING 是由 RFC5245 ICE 规范对 RFC STUN 协议的扩展,而 GOOG-NETWORK-INFO 是 webrtc 对 STUN 协议的扩展。
属性名称 | Type | 说明 |
USERNAME | 0x0006 | ICE用户名 |
GOOG-NETWORK-INFO | 0xC057 | 网络信息,包含network-id以及network cost信息 |
ICE-CONTROLLING | 0x802A | ICE控制角色,携带该属性表示是控制方,与之对应的为受控方(ICE-CONTROLLED),对于 mediasoup,客户端需要进行连通性检测,是控制方角色,而服务器是受控方角色 |
PRIORITY | 0x0024 | 优先级 |
MESSAGE-INTEGRITY | 0x0008 | 用于消息完整性验证,该值的计算是基于SHA-1的HMAC算法,密钥使用 ICE 的 password,消息体则使用 STUN 消息内容(不包含该属性本身以及 FINGERPRINT 属性的内容) |
FINGERPRINT | 0x8028 | 消息指纹 |
XOR-MAPPED-ADDRESS | 0x0020 | 异或地址 |
USE-CANDIDATE | 0x0025 | 该属性只会出现在ICE控制方,表示ICE控制方已经选择好候选地址并准备通信,ICE受控方收到该消息后将该消息发送的对端地址设置为通信的对端地址即可 |
四. mediasoup Lite ICE实现源码剖析
WebRtcTransport 收到数据包后调用 WebRtcTransport::OnPacketReceived,该函数会判断接收到的消息是什么类型的消息,如果是 STUN 协议的消息,就调用 OnStunDataReceived 进行处理,STUN 协议消息的判断即根据上述描述的 STUN 报文格式进行判断即可。
WebRtcTransport::OnStunDataReceived 会调用 RTC::StunPacket::Parse 解析消息,得到 StunPacket,之后调用 IceServer::ProcessStunPacket 进行处理。
IceServer 是 WebRtcTransport 的一个成员变量, 它负责维护 WebRtcTransport 的 ICE 当前状态信息,例如保存 ICE 用户名密码,连接状态,所有的对端候选地址以及当前选中使用的对端候选地址,如下是 IceServer 的成员变量。
class IceServer
{
public:
enum class IceState
{
NEW = 1,
CONNECTED,
COMPLETED,
DISCONNECTED
};
// ......
private:
// Passed by argument.
Listener* listener{ nullptr };
// Others.
std::string usernameFragment;
std::string password;
std::string oldUsernameFragment;
std::string oldPassword;
uint32_t remoteNomination{ 0u };
IceState state{ IceState::NEW };
std::list<RTC::TransportTuple> tuples;
RTC::TransportTuple* selectedTuple{ nullptr };
};
IceServer::ProcessStunPacket 处理流程如下所示,首先判断 STUN 消息是否是一个正常的消息,如果 Method 不是 binding,说明不是一个正常的 STUN Message,或者如果 STUN binding request 消息没有携带 FINGERPRINT 属性,也不是一个正常的消息,返回错误响应即可。
接下来判断 STUN Message 是否携带了 MESSAGE-INTEGRITY, PRIORITY, USERNAME 等属性,如果当中任意一个属性不包含,则返回错误响应。
之后再调用 packet->CheckAuthentication 校验消息,CheckAuthentication 主要是校验 STUN Message 的用户名是否正确,以及 MESSAGE-INTEGRITY 是否正确。
StunPacket::CheckAuthentication 处理逻辑如下所示,首先是判断 USERNAME 属性分号前的用户名与服务器本地用户名是否相等,如果不相等则认证失败,如果相等,再判断 MESSAGE-INTEGRITY 属性是否正确,避免使用在传输过程中被篡改的消息。
接下来再判断如果 STUN Message 携带了 ICE-CONTROLLED 属性,则返回角色错误的失败响应,因为 mediasoup 客户端是控制方角色,mediasoup 服务端是受控方角色,所以 mediasoup 客户端发送的 STUN Message 不应该携带 ICE-CONTROLLED,而应当是 ICE-CONTROLLING。
如果前面的合法性都校验通过,接下来就是执行对 STUN binding request 的响应,即生成 STUN binding reponse 消息,并设置 XOR-MAPPED-ADDRESS 属性值,然后添加 MESSAGE-INTEGRITY 以及 FINGERPRINT 属性值。
至此 mediasoup 接收 STUN binding request 并回复 STUN binding response 的流程就讲解完成,收到 STUN binding request 最后一步处理函数是 HandleTuple(tuple, packet->HasUseCandidate(), packet->HasNomination(), nomination);
HandleTuple 的处理逻辑如下所示,客户端第一次发送的 STUN binding request 消息没有携带 USE-CANDIDATE 和 NOMINATION,并且此时是 New 状态,因此执行下面的逻辑,此时将当前的对端地址信息添加到 IceServer 的 tuples 成员变量,并设置当前 tuple 为 selectedTuple,设置为 selectedTuple 即表示这个对端地址信息是 WebRtcTransport 认为的客户端的通信地址,之后需要往客户端发送的媒体数据就会往这个地址发送,这些操作完成之后 ICE 状态变成 CONNECTED。
连通性检测成功后,mediasoup 客户端会再发送 STUN binding request 消息进行保活,此时发送的 STUN bindind request 会增加 USE-CANDIDATE 属性,表示 mediasoup 客户端已经确定使用该连接进行后续的通信。
当 ICE 处于 CONNECTED 状态,再收到 STUN binding request 消息后执行的逻辑如下,如果 STUN binding request 里包含 USE-CANDIDATE 则会将 tuple 设置为 selectedTuple,如果包含 NOMINATION 则必须属性里携带的提名值大于当前使用的提名值,处理完成后将 ICE 状态设置为 COMPLETED。
当 ICE 处于 COMPLETED 状态收到 STUN binding request 后的处理与 ICE 处于 CONNECTED 时收到 STUN binding request 的处理基本类似,如下。