上一篇《详解 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 protocolchange cipher spec protocolalert 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 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

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

上手 WebRTC DTLS 遇到很多 BUG?浅谈 DTLS Fragment的更多相关文章

  1. 浅谈Android Fragment嵌套使用存在的一些BUG以及解决方法

    时间 2014-03-18 18:00:55 eoe博客 原文  http://my.eoe.cn/916054/archive/24053.html 主题 安卓开发 自从Android3.0引入了F ...

  2. 【原】从一个bug浅谈YUI3组件的资源加载

    篇前声明:为了不涉及业务细节,篇内信息统一以某游戏,某功能代替 前不久,某游戏准备内测客户端,开发人员测试过程中发现某功能突然不灵了,之前的测试一切ok,没有发现任何异常,第一反应是,游戏内浏览器都是 ...

  3. Java线上问题排查神器Arthas快速上手与原理浅谈

    前言 当你兴冲冲地开始运行自己的Java项目时,你是否遇到过如下问题: 程序在稳定运行了,可是实现的功能点了没反应. 为了修复Bug而上线的新版本,上线后发现Bug依然在,却想不通哪里有问题? 想到可 ...

  4. iOS应用架构浅谈

    (整理至http://www.cocoachina.com/ios/20150414/11557.html) 缘由 从事iOS工作一年多了,主要从事QQ钱包SDK开发和财付通app维护,随着对业务的慢 ...

  5. 浅谈Hybrid技术的设计与实现第三弹——落地篇

    前言 接上文:(阅读本文前,建议阅读前两篇文章先) 浅谈Hybrid技术的设计与实现 浅谈Hybrid技术的设计与实现第二弹 根据之前的介绍,大家对前端与Native的交互应该有一些简单的认识了,很多 ...

  6. 浅谈Hybrid技术的设计与实现第二弹

    前言 浅谈Hybrid技术的设计与实现 浅谈Hybrid技术的设计与实现第二弹 浅谈Hybrid技术的设计与实现第三弹——落地篇 接上文:浅谈Hybrid技术的设计与实现(阅读本文前,建议阅读这个先) ...

  7. 浅谈Hybrid技术的设计与实现

    前言 浅谈Hybrid技术的设计与实现 浅谈Hybrid技术的设计与实现第二弹 浅谈Hybrid技术的设计与实现第三弹——落地篇 随着移动浪潮的兴起,各种APP层出不穷,极速的业务扩展提升了团队对开发 ...

  8. 浅谈五大Python Web框架

    转载:http://feilong.me/2011/01/talk-about-Python-web-framework 说到Web Framework,Ruby的世界Rails一统江湖,而Pytho ...

  9. 【转载】浅谈游戏开发之2D手游工具

    浅谈游戏开发之2D手游工具 来源:http://www.gameres.com/459713.html 游戏程序 平台类型: iOS Android  程序设计: 其它  编程语言:   引擎/SDK ...

随机推荐

  1. SpringBoot自动配置探究

    @SpringBootApplication @SpringBootApplication表示SpringBoot应用,标注在某个类上说明这个类是SpringBoot的主配置类,SpringBoot就 ...

  2. Java关于整型类缓存[-128,127]之间的数字

    我们在学习Java的包装类Integer.Long的时候可能会遇到这个问题: ①Integer a = 500;// Integer a = Integer.valueOf(500); 等价于上面的 ...

  3. 从I/O多路复用到Netty,还要跨过Java NIO包

    本文是Netty系列第4篇 上一篇文章我们深入了解了I/O多路复用的三种实现形式,select/poll/epoll. 那Netty是使用哪种实现的I/O多路复用呢?这个问题,得从Java NIO包说 ...

  4. Python基础之:Python中的类

    目录 简介 作用域和命名空间 class 类对象 类的实例 实例对象的属性 方法对象 类变量和实例变量 继承 私有变量 迭代器 生成器 简介 class是面向对象编程的一个非常重要的概念,python ...

  5. (原创)IconFont(矢量图标字体)在Winform中的应用

    一.前言 很多时候,使用矢量图形可以带来非常美观的界面效果,比如SVG的使用.但是Winform原生是不支持显示SVG图像的,所以退而求其次,可以使用IconFont来实现相似的矢量效果. 先来个图解 ...

  6. Dapper, Ef core, Freesql 插入大量数据性能比较(二)

    在上一篇文章中,我们比较出单表插入9999行数据,Dapper > EfCore > Freesql.在本文中,我们来看看级联插入 构建9999行数据 List<Entity> ...

  7. Sql Server Report Service访问服务页面503解决方法

    这个问题可能性比较多,也有多个方案去解决,可以从如下方法里逐个测试 1.打最新的数据库补丁. 2.删除报表服务配置的密钥,重启报表服务. 3.修改报表服务器配置的用户账户为域管理员 4.找到报表服务器 ...

  8. Centos下搭建DNS域名解析服务器

    Centos下搭建DNS域名解析服务器 DNS  即Domain Name System(域名系统)的缩写,它是一种将ip地址转换成对应的主机名或将主机名转换成与之相对应ip地址的一种机制.其中通过域 ...

  9. Centos6无法使用yum解决办法

    12月后Centos 6 系统无法使用yum出现错误(文章底部看) 相信已经有一部分朋友今天连接到CentOS 6的服务器后执行yum后发现报错,那么发生了什么? CentOS 6已经随着2020年1 ...

  10. [ERROR]: gitstatus failed to initialize.

    1 问题描述 Manjaro升级后,zsh的主题p10k出现的问题. Your git prompt may disappear or become slow. Run the following c ...