簡介
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