SRS4.0之RTMP转WebRTC04 ---- ICE交互分析

时间:2021-09-29
本文章向大家介绍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

原文地址:https://www.cnblogs.com/vczf/p/15346360.html