1. 程式人生 > 其它 >SRS4.0之RTMP轉WebRTC04 ---- ICE互動分析

SRS4.0之RTMP轉WebRTC04 ---- ICE互動分析

簡介

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