流媒体服务器

WebRTC ICE概述

一、概述

ICE全称Interactive Connectivity Establishment:交互式连通建立方式。
ICE参照RFC5245建议实现,是一组基于offer/answer模式解决NAT穿越的协议集合。
它综合利用现有的STUN,TURN等协议,以更有效的方式来建立会话。
ICE是通过综合运用STUN,TURN,RSIP等NAT穿透方式,使之能在最适合的情况下工作,以弥补单独使用其中任何一种所带来的固有缺陷。对于SIP来说,ICE只需要定义一些SDP(Sessionescription Protoc01)附加属性即可,对于别的多媒体信令协议也需要制定一些相应的机制来实现。

ICE的算法可以分为以下几个流程:

(1)收集本地传输地址
    会话者从服务器上获得主机上一个物理(或虚拟)接口绑定一个端口的本地传输地址。
    
(2)启动STUN
    与传统的STUN不同,ICE用户名和密码可以通过信令协议进行交换。
    
(3)确定传输地址的优先级
    优先级反映了UA在该地址上接收媒体流的优先级别,取值范围0到1之间,按照被传输媒体流量来确定。

(4)构建初始化信息(Initiate Message)
    初始化消息由一系列媒体流组成,每个媒体流的任意Peer之间实现最人连通可能性的传输地址是由公网L转发服务器(如TURN)提供的地址。

(5)响应处理
    连通性检查和执行ICE算法中描述的地址收集过程。

(6)生成接受信息(Accept Message)
    若接受则发送Accept消息,其构造过程与InitiateMessage类似。

(7)接受信息处理
    接受过程需要发起者使用Send命令,由服务器转发至响应者。

(8)附加ICE过程
    Initiate或Accept消息交换过程结束后,双方可能仍将继续收集传输地址。
           简单的说,就是SIP终端在注册时,访问STUN服务器;当发起呼叫信息(INVITE)或接收到
        呼叫信息回应(200 OK)之前根据NAT/防火墙类型通过访问STUN服务器进行对RTP进行地址收集;
        然后在RTP的地址端口启动接收线程RSTUN服务程序;最后发送SIP消息,收集的地址放列SDP消
        息中的alt属性中。STUN(简单的用UDP穿透NAT),后面最大的区别是支持TCP穿透(即使用中
        继穿透NAT)。STUN的扩展。简单的说,TURN与STURN的共同点都是通过修改应用层中的私网地
        址达到NAT穿透的效果,异同点是TURN是通过两方通讯的“中间人”方式实现穿透。一般来说,实
        现P2P通信无法实现时,TURN就会派上用场

二、STUN的报文结构为:

消息头
所有的STUN消息都包含20个字节的消息头,包括16位的消息类型,16位的消息长度和128位的事务ID。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |0 0|     STUN Message Type     |         Message Length        |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                         Magic Cookie                          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                                                               |
 |                     Transaction ID (96 bits)                  |
 |                                                               |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                                                              |
 |              2 byte attrtype  2 byte attrlength  body        |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  

头两位必须是0,当网络中收到stun和其他数据包时,可以基于该两位的值是否为0判断是stun包。(当然这种判断方式不太严谨
在这里插入图片描述

  • Message Type 总共占14位,其中方法Method 占12位,类型Class占两位。展开后具体格式如下
0                 1
2  3  4 5 6 7 8 9 0 1 2 3 4 5
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+
|M |M |M|M|M|C|M|M|M|C|M|M|M|M|
|11|10|9|8|7|1|6|5|4|0|3|2|1|0|

M11 到M0 表示12位的Method,C1和C0两位表示Class。
在这里插入图片描述
用12位表示Method,理论上可以表示2^12个Method,实际应用中主要用到一个Binding方法。
Class 占2位,可以表示2^2=4 种类型

在这里插入图片描述
四种类型分别为:request、sucess response、failure response、indication
具体如下:(注意协议文档中的0b00,代表二进制00)
在这里插入图片描述
对于每种Method都会对应请求、成功响应、错误响应、指示四种类型。Method和Class 拼接在一起组成Message Type

enum MessageType
{
 // see @ https://tools.ietf.org/html/rfc3489#section-11.1 
    BindingRequest            = 0x0001,
    BindingResponse           = 0x0101,
    BindingErrorResponse      = 0x0111,
    SharedSecretRequest       = 0x0002,
    SharedSecretResponse      = 0x0102,
    SharedSecretErrorResponse = 0x0112,
};

Message Length
Message Length 表示除了20字节头以外的所有数据长度。由于STUN属性都是以4字节的倍数填充,因此这个字段最后两位总是等于零,这也是辨别STUN包的一个方法之一。

Magic Cookie
固定的4个字节,具体值为:0x2112A442
Magic Cookie用来和客户端映射的外网IP地址做异或,形成XOR-MAPPED-ADDRESS属性。见下文分析。
Transaction ID
12字节的事务ID。对于一个客户端,客户端和服务端的事务id相等,它被用来作为客户端的唯一标识。
stun协议20字节的消息头后,就是0到N个的属性消息。

三、属性消息

属性格式

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |         Type                  |            Length             |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                         Value (variable)                ....
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

获取AttrType 和 AtrrLenght方法:
在这里插入图片描述

四、介绍几个常用属性

USERNAME
USERNAME 是有sdp中的ice-ufrag拼接而成,具体的拼接格式为 answer sdp 中的ice-ufrag: offer sdp中的ice-ufrag
在服务器单端口改造时,由于所有客户端的stun包都发到服务器的同一端口,且客户端发送sdp信令(tcp或者websocket协议)的端口和发送stun包(udp协议)的端口不是同一个,所以可用USERNAME区分不同的客户端。
USE_CANDIDATE
USE_CANDIDATE 客户端的icecandidate。
ICE-CONTROLLING
用来表示通信双方的ice角色,ICE-CONTROLLING 表示控制端,用来选择最终进行连接性检测的候选地址对;ICE-CONTROLLED表示被控端,被告知最终用哪个地址对进行通信。

    在实际通信过程中,双方都可能是ICE-CONTROLLING或者ICE-CONTROLLED,当双方值相同时就会产生冲突。为了解决这个冲突,引入一个 tie-breake字段,tie-breake值大的一端是控制端。

Attribute::FINGERPRINT
如果turn包属性中带了FINGERPRINT,需要信息校验。具体mediasoup中实现。
在这里插入图片描述
在这里插入图片描述
MESSAGE-INTEGRITY
消息完整性。

先解释HMAC-SHA1 算法。
HMAC 是一种基于密钥的报文完整性验证方法,其安全性是建立在Hash加密算法基础上的。HMAC利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。
SHA1 是一种哈希算法,可以生成一个被称为消息摘要的160位(20字节)散列值
stun包中被加密的具体数据为:
从stun头到该字段(包含该字段)的所有数据,加密中所用的密码是ice-pwd中表示的密码。
加密完成的值,作为MESSAGE-INTEGRITY的属性值保存到stun包中。
mediasoup中具体实现见:StunPacket.cpp 的Serialize()函数
在这里插入图片描述
stun包信息会大概会带以下信息:USERNAME, ICE_CONTROLLING, USE_CANDIDATE, PRIORITY, MESSAGE_INTEGRITY和FINGERPRINT。

五、stun包的合法性判断

mediasoup基于MESSAGE-INTEGRITY 和USERNAME来验证stun包的合法性。

StunPacket::Authentication StunPacket::CheckAuthentication(
      const std::string& localUsername, const std::string& localPassword)
    {
        MS_TRACE();
        switch (this->klass)
        {
            case Class::REQUEST:
            case Class::INDICATION:
            {
                // Both USERNAME and MESSAGE-INTEGRITY must be present.
                if (!this->messageIntegrity || this->username.empty())
                    return Authentication::BAD_REQUEST;
                // Check that USERNAME attribute begins with our local username plus ":".
                size_t localUsernameLen = localUsername.length();
                if (
                  this->username.length() <= localUsernameLen || this->username.at(localUsernameLen) != ':' ||
                  (this->username.compare(0, localUsernameLen, localUsername) != 0))
                {
                    return Authentication::UNAUTHORIZED;
                }
                break;
            }
            // This method cannot check authentication in received responses (as we
            // are ICE-Lite and don't generate requests).
            case Class::SUCCESS_RESPONSE:
            case Class::ERROR_RESPONSE:
            {
                MS_ERROR("cannot check authentication for a STUN response");
                return Authentication::BAD_REQUEST;
            }
        }
        // If there is FINGERPRINT it must be discarded for MESSAGE-INTEGRITY calculation,
        // so the header length field must be modified (and later restored).
        if (this->hasFingerprint)
            // Set the header length field: full size - header length (20) - FINGERPRINT length (8).
            Utils::Byte::Set2Bytes(this->data, 2, static_cast<uint16_t>(this->size - 20 - 8));
        // Calculate the HMAC-SHA1 of the message according to MESSAGE-INTEGRITY rules.
        const uint8_t* computedMessageIntegrity = Utils::Crypto::GetHmacShA1(
          localPassword, this->data, (this->messageIntegrity - 4) - this->data);

        Authentication result;
        // Compare the computed HMAC-SHA1 with the MESSAGE-INTEGRITY in the packet.
        if (std::memcmp(this->messageIntegrity, computedMessageIntegrity, 20) == 0)
            result = Authentication::OK;
        else
            result = Authentication::UNAUTHORIZED;

        // Restore the header length field.
        if (this->hasFingerprint)
            Utils::Byte::Set2Bytes(this->data, 2, static_cast<uint16_t>(this->size - 20));

        return result;
    }

FINGERPRINT
指纹属性,用来检测数据传输过程中是否发生了错误。前面数据完整性是验证数据是否完整,这里是验证消息内部具体的数据是否出错了。

计算规则:
对FINGERPRINT以外的所有数据进行crc-32循环冗余校验(FINGERPRINT是stun包最后一个属性),并把得到的值与0x5354554e异或得到最终的值。

服务端收到stun包以后,再按同样的规则生成一遍,判断自己生成的值和FINGERPRINT属性中所带的值是否相等,相等说明数据正确,否则数据传输中发生了错误。

mediasoup中生成FINGERPRINT和判断FINGERPRINT是否相等的代码如下:

// Add MESSAGE-INTEGRITY.
        if (addMessageIntegrity)
        {
            // Ignore FINGERPRINT.
            if (addFingerprint)
                Utils::Byte::Set2Bytes(buffer, 2, static_cast<uint16_t>(this->size - 20 - 8));

            // Calculate the HMAC-SHA1 of the packet according to MESSAGE-INTEGRITY rules.
            const uint8_t* computedMessageIntegrity =
              Utils::Crypto::GetHmacShA1(this->password, buffer, pos);

            Utils::Byte::Set2Bytes(buffer, pos, static_cast<uint16_t>(Attribute::MESSAGE_INTEGRITY));
            Utils::Byte::Set2Bytes(buffer, pos + 2, 20);
            std::memcpy(buffer + pos + 4, computedMessageIntegrity, 20);

            // Update the pointer.
            this->messageIntegrity = buffer + pos + 4;
            pos += 4 + 20;

            // Restore length field.
            if (addFingerprint)
                Utils::Byte::Set2Bytes(buffer, 2, static_cast<uint16_t>(this->size - 20));
        }
        else
        {
            // Unset the pointer (if it was set).
            this->messageIntegrity = nullptr;
        }

        // Add FINGERPRINT.
        if (addFingerprint)
        {
            // Compute the CRC32 of the packet up to (but excluding) the FINGERPRINT
            // attribute and XOR it with 0x5354554e.
            uint32_t computedFingerprint = Utils::Crypto::GetCRC32(buffer, pos) ^ 0x5354554e;

            Utils::Byte::Set2Bytes(buffer, pos, static_cast<uint16_t>(Attribute::FINGERPRINT));
            Utils::Byte::Set2Bytes(buffer, pos + 2, 4);
            Utils::Byte::Set4Bytes(buffer, pos + 4, computedFingerprint);
            pos += 4 + 4;

            // Set flag.
            this->hasFingerprint = true;
        }
        else
        {
            this->hasFingerprint = false;
        }

交流QQ群:512923876 联系邮箱:wsx958191@126.com

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值