简介
ICE全称Interactive Connectivity Establishment:交互式连通建立方式。
ICE参照RFC5245建议实现,是一组基于offer/answer模式解决NAT穿越的协议集合。
它综合利用现有的STUN,TURN等协议,以更有效的方式来建立会话。
ICE介绍
1.ICE的角色
分为 controlling和controlled。
Offer 一方为controlling角色,answer一方为controlled角色。
2.ICE的模式
分为FULL ICE和Lite ICE:
FULL ICE:是双方都要进行连通性检查,完成的走一遍流程。
Lite ICE: 在FULL ICE和Lite ICE互通时,只需要FULL ICE一方进行连通性检查, Lite一方只需回应response消息。这种模式对于部署在公网的设备比较常用。
3.Candidate
媒体传输的候选地址,组成candidate pair做连通性检查,确定传输路径,有如下属性:
Type 类型
Host: 这个地址是一个真实的主机,参数中的地址和端口对应一个真实的主机地址, 这个地址来源于本地的物理网卡或逻辑网卡上的地址,对于具有公网地址或者同一内网的端可以用。
Srvflx:这个地址是通过Cone NAT(锥形NAT)反射的类型,参数中的地址和端口是端发送 Binding 请求到 STUN/TURN server 经过NAT时,NAT 上分配的地址和端口。
Relay:这个地址是端发送 Allocate 请求到 TURN server ,由 TURN server 用于中继的地址和端口,该地址和端口是 TURN 服务用于在两个对等点之间转发数据的地址和端口,是一个中继地址端口。这个地址是端发送 Allocate 请求到 TURN server ,由 TURN server 用于中继的地址和端口(这个可能是本机或 NAT 地址)
Prflx:这个地址是通过 发送STUN Binding时,通过Binding获取到的地址。在建连检查期间新发生,参数中的地址和端口是端发送 Binding 请求到 STUN/TURN server 经过 NAT 时,NAT 上分配的地址和端口。这个地址是端发送 Binding 请求到对等端经过 NAT 时,NAT 上分配的地址和端口
Componet ID
传输媒体的类型,1代表RTP;2代表 RTCP。
WebRTC采用Rtcp-mux方式,也就是RTP和RTCP在同一通道内传输,减少ICE的协商和通道的保活。
Priority
Candidate的优先级。
如果考虑延时,带宽资源,丢包的因素,Type优先级高低一般建议如下顺序:
host > srvflx > prflx > relay
Base
是指candidate 的基础地址。
Srvflx address 的base 是本地host address。
host address和 relayed address 的base 是自身
交互抓包分析
SRS的交互相对比较简单,我们抓包分析一下:
主要分为两个部分:
1.通过HTTP请求,通过SDP实现ICE信息交互
2.使用STUN发送连通性检查请求
SDP的ICE信息
这里audio和video一样,只取audio
offer:
a=ice-ufrag:PA7e // 客户端用户名 a=ice-pwd:F1o3tHlhk6OPBtXo8IdhZCRH // 客户端密码 a=ice-options:trickle // trickle方式表示媒体信息和ice后选项的信息可以分开传输
answer:
a=ice-lite // SRS是Lite ICE,只需要响应客户端的Binding请求 a=ice-ufrag:8p42d118 // SRS端用户名 a=ice-pwd:ok61un195fg8q8083yy06247w0xg483s // SRS端密码 a=candidate:0 1 udp 2130706431 10.151.3.77 8000 typ host generation 0 // {foundation} {component} {protocol} {priority} {ip} {port} typ {type} {generation} // 0 [foundation] : 标识符,用来识别两个candidate是否相等 // 1 [component] : 传输媒体类型 1表示RTP // ubp [protocol] : 协议类型 // 2130706431 [priority] : 优先级 // 10.151.3.77 [ip] : ip地址 // 8000 [port] : 端口 // host [type] : host类型,表示这是真实的主机地址 // generation : 代数。初始值是0,然后会不断+1,大的代数会覆盖掉低代数的候选地址。更新candidate的时候会+1,替换老的candidate
STUN消息格式
Stun Header:固定20个字节
STUN Message Type(14bits):消息类型。定义消息类型如下:
C1和C0两位表示类的编码:00表示request 01表示indication 10表示success response 11表示error response
常见类型:
0x0001 : Binding Request
0x0101 : Binding Response
0x0111 : Binding Error Response
Message Length(16bits):消息长度,不包含STUN Header的20个字节。
Magic Cookie(32bits):固定值0x2112A442,用于反射地址的异或(XOR)运算。
Transaction ID(96bits):事务ID标识符,请求对应的响应具有相同的标识符。
STUN属性类型
STUN 消息头后跟着多个属性,每个属性都采用 TLV 编码,type 为 16 位的类型、lenght 为 16 位的长度、value 为属性值。
STUN的连通性请求
Request:
USERNAME:用户名,规则为“对端的ice-ufrag : 自己的ice-ufrag”。
ICE-CONTROLLING: 表示发起方,Tie breaker用来处理角色冲突,当冲入时,这个值大的为controlling
PRIORITY:优先级
MESSAGE-INTEGRITY:STUN 消息的 HMAC-SHA1 值,长度 20 字节,用于消息完整性认证。
FINGERPRINT:指纹认证,此属性可以出现在所有的 STUN 消息中,该属性用于区分 STUN 数据包与其他协议的包。
Response:
XOR-MAPPED-ADDRESS: 用于表示客户端外部IP地址,如果没有NAT,那么外部IP地址和内部IP地址是相同的。前8位保留,之后8位用于表示IP类型(IPV4/6)。之后16位表示端口号。这里强制使用IPV4版本,所以Address是32位:
Family:IP类型,0x01-IPV4、0x02-IPV6。
Port:端口。
Address:IP地址
SRS处理
代码处理比较简单
Request:
srs_error_t SrsStunPacket::decode(const char* buf, const int nb_buf) { srs_error_t err = srs_success; SrsBuffer* stream = new SrsBuffer(const_cast<char*>(buf), nb_buf); SrsAutoFree(SrsBuffer, stream); if (stream->left() < 20) { return srs_error_new(ERROR_RTC_STUN, "invalid stun packet, size=%d", stream->size()); } // 消息类型 message_type = stream->read_2bytes(); // 消息长度(不包含header 20bytes) uint16_t message_len = stream->read_2bytes(); // 固定值 0x2112A442 string magic_cookie = stream->read_string(4); // 事务ID标识符 transcation_id = stream->read_string(12); if (nb_buf != 20 + message_len) { return srs_error_new(ERROR_RTC_STUN, "invalid stun packet, message_len=%d, nb_buf=%d", message_len, nb_buf); } while (stream->left() >= 4) { uint16_t type = stream->read_2bytes(); uint16_t len = stream->read_2bytes(); if (stream->left() < len) { return srs_error_new(ERROR_RTC_STUN, "invalid stun packet"); } string val = stream->read_string(len); // padding if (len % 4 != 0) { stream->read_string(4 - (len % 4)); } switch (type) { // 对端的ice-ufrag : 自己的ice-ufrag case Username: { username = val; size_t p = val.find(":"); if (p != string::npos) { local_ufrag = val.substr(0, p); remote_ufrag = val.substr(p + 1); } srs_trace("stun recv:%s", username.c_str()); break; } case UseCandidate: { use_candidate = true; srs_verbose("stun use-candidate"); break; } // @see: https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-5.1.2 // One agent full, one lite: The full agent MUST take the controlling // role, and the lite agent MUST take the controlled role. The full // agent will form check lists, run the ICE state machines, and // generate connectivity checks. // 表示受控方 case IceControlled: { ice_controlled = true; srs_verbose("stun ice-controlled"); break; } // 表示发起方 case IceControlling: { ice_controlling = true; srs_verbose("stun ice-controlling"); break; } default: { srs_verbose("stun type=%u, no process", type); break; } } } return err; }
Response:
srs_error_t SrsStunPacket::encode_binding_response(const string& pwd, SrsBuffer* stream) { srs_error_t err = srs_success; string property_username = encode_username(); string mapped_address = encode_mapped_address(); // 消息类型0x0101 stream->write_2bytes(BindingResponse); // 消息长度(不包含头20字节) stream->write_2bytes(property_username.size() + mapped_address.size()); // 固定值0x2112A442 stream->write_4bytes(kStunMagicCookie); // 事务ID标识符 stream->write_string(transcation_id); // 用户名 stream->write_string(property_username); // 外部IP地址 stream->write_string(mapped_address); stream->data()[2] = ((stream->pos() - 20 + 20 + 4) & 0x0000FF00) >> 8; stream->data()[3] = ((stream->pos() - 20 + 20 + 4) & 0x000000FF); // sha1加密 char hmac_buf[20] = {0}; unsigned int hmac_buf_len = 0; if ((err = hmac_encode("sha1", pwd.c_str(), pwd.size(), stream->data(), stream->pos(), hmac_buf, hmac_buf_len)) != srs_success) { return srs_error_wrap(err, "hmac encode failed"); } string hmac = encode_hmac(hmac_buf, hmac_buf_len); stream->write_string(hmac); stream->data()[2] = ((stream->pos() - 20 + 8) & 0x0000FF00) >> 8; stream->data()[3] = ((stream->pos() - 20 + 8) & 0x000000FF); // 指纹认证 uint32_t crc32 = srs_crc32_ieee(stream->data(), stream->pos(), 0) ^ 0x5354554E; string fingerprint = encode_fingerprint(crc32); stream->write_string(fingerprint); stream->data()[2] = ((stream->pos() - 20) & 0x0000FF00) >> 8; stream->data()[3] = ((stream->pos() - 20) & 0x000000FF); return err; }
参考文档
按照时间顺序:
stun(rfc 3489) : https://tools.ietf.org/html/rfc3489
stun(rfc 5389,从rfc 3489演变来的) : https://tools.ietf.org/html/rfc5389
ice : https://tools.ietf.org/html/rfc5245