上手 WebRTC DTLS 遇到不少 BUG?浅谈 DTLS Fragment

上一篇《详解 WebRTC 传输安全机制:一文读懂 DTLS 协议》详细阐述了 DTLS。本文将结合 DTLS 开发中遇到的问题,详细解读 DTLS 的一些基础概念以及 Fragment 的机制,并进一步深究 DTLS 协议。html

做者|泰一git

审校|进学、莫战github

前言

最近在作 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 转发模块依然会接收失败呢?app

这是由于证书虽然被分片,可是在发送到 TURN 模块时并无按照分片独立发送,仍然是所有打包到了同一个 UDP 数据报中进行发送,因此接收确定会失败。函数

下面,咱们将一块儿了解下 DTLS Fragment 的机制。首先要理清几个概念。this

Message、Record、Flight

DTLS 协议分为两层:底层的 record protocol 和上层的 handshake protocolchange cipher spec protocolalert protocol 以及 application data protocolDTLS

Remark:握手协议、密码规格变动协议、警告协议、应用数据协议均在 DTLS 记录协议的上层,这四种协议统称为 DTLS 握手协议。

Note:关于记录和握手这两层协议各自的做用,这里就再也不赘述,能够参考 WebRTC 中 DTLS 的应用

DTLS Message 是一条完整的 DTLS 消息。好比握手消息:Client Hello、Certificate、Client Key Exchange 等;好比密码规格变动消息:Change Cipher Spec。

DTLS Record 是记录层(Record Layer)的概念,能够认为它是一个壳子,里面装载着 DTLS Message,以下图:

image.png

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 数据报发送。

image.png

如上图所示,本次 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 offsetfragment 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 的边界?这个问题,之后有时间再调研吧。

感谢阅读。

参考

DTLS 1.2 TLS 1.2

「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。公众号后台回复【技术】可加入阿里云视频云技术交流群,和做者一块儿探讨音视频技术,获取更多行业最新信息。

相关文章
相关标签/搜索