简介:上一篇《详解 WebRTC 传输平安机制:一文读懂 DTLS 协定》具体论述了 DTLS。本文将联合 DTLS 开发中遇到的问题,具体解读 DTLS 的一些根底概念以及 Fragment 的机制,并进一步深究 DTLS 协定。
作者|泰一
审校|进学、莫战
前言
最近在做 J 和 G 这两套 RTC 零碎的 DTLS-SRTP 握手加密工作,要求应用 CA 机构颁发的证书。在本机调试的过程中发现:G 零碎应用 CA 证书,DTLS 握手胜利,而 J 零碎则握手失败。
通过几番调试与剖析,定位到了起因:J 零碎相较于 G 零碎多了一个 TURN 转发模块,该模块设置的接收缓冲区的上限值为 1600 字节,而 CA 证书的大小则有近 3000 字节,因而 TURN 模块转发给客户端的证书不残缺,导致 DTLS 握手失败。
大家都晓得,WebRTC 的 DTLS 应用的是 自签名 的证书,这个证书个别不会太大,如下图所示,只有 286 字节。
然而,如果要应用 CA 颁发的证书,那么这个证书可能会很大,如下图所示,竟达到了 2772 字节,显然超出了 TURN 模块的接收缓冲区的大小。
上图中,你可能留神到了这个 CA 证书被分成了两片(two fragments),这其实是 DTLS 协定层做的。不过值得思考的是,CA 证书的每一片的大小都未超出 TURN 模块接收缓冲区的 1600 字节的限度,然而为什么 J 零碎的 TURN 转发模块仍然会接管失败呢?
这是因为证书尽管被分片,然而在发送到 TURN 模块时并没有依照分片独立发送,依然是全副打包到了同一个 UDP 数据报中进行发送,所以接管必定会失败。
上面,咱们将一起理解下 DTLS Fragment 的机制。首先要理清几个概念。
Message、Record、Flight
DTLS 协定分为两层:底层的 record protocol 和下层的 handshake protocol、change cipher spec protocol、alert protocol 以及 application data protocol。
Remark:握手协定、明码规格变更协定、正告协定、利用数据协定均在 DTLS 记录协定的下层,这四种协定统称为 DTLS 握手协定。
Note:对于记录和握手这两层协定各自的作用,这里就不再赘述,能够参考:详解 WebRTC 传输平安机制:一文读懂 DTLS 协定。
DTLS Message 是一条 残缺的 DTLS 音讯。比方握手音讯:Client Hello、Certificate、Client Key Exchange 等;比方明码规格变更音讯:Change Cipher Spec。
DTLS Record 是记录层(Record Layer)的概念,能够认为它是一个壳子,外面装载着 DTLS Message,如下图:
Message 和 Record 是一对一或者一对多的关系。也就是说,一个 Record 不肯定装了一条残缺的 Message。因为有可能是多个 Record 组成一个残缺的 Message。
如果 Message 很小,未超过 MTU 的限度,那么一个 Record 足以装下一条 Message;如果 Message 很大,超过 MTU 的限度,那么就须要多个 Record 来装这条 Message。即这条 DTLS Message 会被宰割为多个 Fragment,而后别离装入多个 Record。
Remark:最大传输单元(Maximum transmission Unit, MTU)是数据链路层的概念,MTU 限度的是数据链路层的 payload 大小,也就是其下层协定的大小,比方 IP、ICMP。在以太网中,链路层的 MTU 是 1500 字节。
比方,Certificate 这个握手音讯,证书大小很容易就超过 MTU 的限度,那么这个音讯就会被宰割为多个 Fragment 并被别离寄存到多个 DTLS Record,每个 Fragment 的大小要保障不超过 MTU 的限度(PS:导读的第二张图就是一个理论的例子)。
Flight 中文解释为“航班”或者“航程”,是一个或者一组打包好的 Message,这组 Message 属于同一个“航程”,视为一个整体,通过单个 UDP 数据报发送。
如上图所示,本次 DTLS 握手一共有 4 个 Flight。Flight2 是 Server Hello、Certificate、Server Hello Done 这三条 Message 的组合,其中 Certificate 这条 Message 被宰割为两个 Fragment,装到两个 Record 中。Flight2 通过大小为 2969 字节的 UDP 数据报发送进来。
Remark:Flight2 这个 2969 字节的 UDP 包是在本机环境下调试、抓包失去的,并不代表 MTU 有这么大,在理论的网络中,不会呈现这种远超 MTU 限度的数据包。
到这里,对于 Message、Record、Flight 的概念就讲完了,三者之间的关如下图:
Fragment
上面咱们谈谈,DTLS 为什么要对 DTLS Message 做分片。
咱们晓得,受以太网 MTU 影响,UDP 数据报最大为 1500 字节,超出这个限度就会被 IP 层分片(PS:以太网 MTU 设置为 1500 字节是为了最大化信道传输利用率)。
然而如果 IP 层分片机制被禁止呢?这就会导致大于 1500 字节的 UDP 数据报在 IP 层被抛弃。因而,DTLS 要对音讯做分片,来满足 IP 层对报文大小的要求。DTLS1.2: Message Size 这一节解释了这个起因。
By contrast, UDP datagrams are often limited to <1500 bytes if IP fragmentation is not desired. In order to compensate for this limitation, each DTLS handshake message may be fragmented over several DTLS records, each of which is intended to fit in a single IP datagram.
因而,DTLS 的分片机制很简略:在发送时把 DTLS Message 宰割成多个间断的 DTLS Record,在接管时缓存分片,直到领有残缺的 DTLS Message。
咱们能够应用 OpenSSL 的这两个 API 设置 MTU 的大小:
SSL_set_options(dtls, SSL_OP_NO_QUERY_MTU);
SSL_set_mtu(dtls, 1500);
下面的代码设置了 MTU 为 1500,那么当 DTLS Message 大小超过 1500 字节,就会触发 DTLS 的分片机制,同理,如果设置 MTU 为 300,那么当 DTLS Message 大小超过 300 字节,就会分片。如果不进行设置,那么 MTU 会走默认值。如下图所示,证书音讯被宰割成了若干个大小为 288 字节的固定的 Fragment。
Remark:TLS 底层是 TCP 协定,为字节流式传输,因而 TLS 没有音讯分片机制。
咱们还可应用上面的 API 设置 Fragment 的大小的下限:
SSL_set_max_send_fragment(dtls, 1500);
最初,咱们回到 导读 形容的问题:证书音讯实际上的确被宰割为两片并别离存储到两个 Record,然而因为在发送的时候还是打包到了一个 UDP 数据报,因而,过大的 UDP 数据报导致 TURN 模块并未接管残缺。
更具体的起因是:咱们应用的是内存型的 BIO,在应用层调用 BIO_get_mem_data
失去的是对于 DTLS Message 的一块间断的内存(尽管这块内存中的证书音讯曾经被 DTLS 切成两个间断的 Fragment 并存在两个 Record 中),而应用层在获取到这块内存后就间接通过 sendto
函数发送给了对端,因而,这个 UDP 报文当然还是特地大,导致接管失败。
回过头来再看下 导读 中证书音讯分片的这张图,两个 Record 的 message sequence
字段值雷同,阐明这是同一个 DTLS Message 的两个 Fragment。且每个 Record 都有 fragment offset
和 fragment length
这两个字段,用来标识分片的边界。所以,咱们能够依据这两个字段去解析出每一个独立的 Fragment。
当然,依据 Record 头部的 Length
字段足以确定边界,这会使应用层的解析更加不便。所以,要解决这个问题,应用层要做的是:对从 BIO 获取到的这块音讯内存进行解析,失去每个 Record 的边界,而后将每个 Record 以独立的 UDP 报文发送进来。具体的解析代码这里就不贴出来了,非常简单。
最初,在实践中发现,DTLS Record 不能跨 UDP 数据报发送,DTLS 1.2: Transport Layer Mapping 这一节也交代了这一点。也就是说,应用层要严格的依照 Record 的边界解析出每一个 Record,别离通过独立的 UDP 数据报发送,而不能依照本人的志愿随便划分为若干个 UDP 数据报发送。因为这可能会导致某个 DTLS Record 被切分到多个 UDP 数据报发送,从而导致接收端 DTLS 无奈将收到的 DTLS Records 重组为残缺的 DTLS Message。
下图是 DTLS 分片独立发送后的成果:
有趣味的读者能够参考我写的 DTLS demo,它实现了简略的 DTLS 握手和分片独立发送。也能够参考 开源视频服务器 SRS 的 DTLS 实现,更加简洁和详尽。
总结
对于超过 MTU 限度的 DTLS Message,DTLS 会把它宰割为多个 Fragment,并别离存储到各个 DTLS Record 中,因而一个 Fragment 肯定是一个 DTLS Record。对于未超过 MTU 限度的 DTLS Message,则不会被分片,也是存储到 DTLS Record 中,因而一个 DTLS Record 不肯定是一个 Fragment,也有可能是一个残缺的 DTLS Message。另外,MTU 的大小以及 Fragment 的最大值都能够应用 OpenSSL 的 API 进行设置。
因为咱们通过内存型 BIO 获取到了存储了各个 DTLS Message 的这块间断内存后,间接将其打包为 Flight,并通过独自的 UDP 数据报文发送,因而这个 UDP 包依然还是那么大,超出了 TURN 模块接收缓冲区的下限和 MTU 的限度。所以为了做到真正的分片独立发送,须要应用层本人去做 Fragment 的解析(其实就是解析 Record 的边界),并别离通过独立的 UDP 报文发送。
咱们在解决了一个问题后,还要再问一下本人有没有引入新的问题。
独立发送每个 DTLS Record,尽管解决了 DTLS Message 超过 MTU 限度的问题,然而这也减少了 UDP 报文的数量,因而丢包的概率也会相应的减少,DTLS 重传次数减少,握手的成功率升高。解决这个问题的一个办法是:不用每个 DTLS Record 都独自 UDP 发送,能够多个 DTLS Record 发送,只有能保障它们加起来的大小不超过 MTU 的限度就能够。
同时,咱们也要问一下本人有没有更好的办法。
比方目前的解决办法是应用层本人实现 Record 解析并独立发送,那么 OpenSSL 是否曾经有相干的 API 实现相似的性能,再比方 BIO 有没有相干的 API 能够通知咱们读取的内存数据中 Record 的数量以及每个 Record 的边界?这个问题,当前有工夫再调研吧。感激浏览。
参考
- DTLS1.2,rfc6347
- TLS1.2,rfc5246
版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。