上一篇《详解 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. postman接口自动化测试之添加Tests检查点

    一.概念 Postman的Tests本质上是JavaScript代码,通过我们编写测试代码,每一个Tests返回True,或是False,以判断接口返回的正确性. 其实,每一个Tests实际上就是一个 ...

  2. MySQL在线DDL工具 gh-ost

    一.简介 gh-ost基于 golang 语言,是 github 开源的一个 DDL 工具,是 GitHub's Online Schema Transmogrifier/Transfigurator ...

  3. 攻防世界 reverse BABYRE

    BABYRE   XCTF 4th-WHCTF-2017 int __cdecl main(int argc, const char **argv, const char **envp) { char ...

  4. ZooKeeper 会话的秘密

    本文作者:HelloGitHub-老荀 Hi,这里是 HelloGitHub 推出的 HelloZooKeeper 系列,免费开源.有趣.入门级的 ZooKeeper 教程,面向有编程基础的新手. 项 ...

  5. docker部署nodejs项目应用

    之前笔者弄了一套nestjs项目放在自己服务器上,并用pm2管理进程. 现在要把pm2停止,尝试一下用docker容器,那么首先要安装docker 一.安装docker 由于笔者服务器的系统是cent ...

  6. java面试-CAS底层原理

    一.CAS是什么? 比较并交换,它是一条CPU并发原语. CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B.当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什 ...

  7. 用递归求n皇后问题

    此问题是指在n*n的国际象棋棋盘上 ,放置n个皇后,使得这n个皇后均不在,同一行,同一列,同一对角线上,求出合法的方案的数目. 本题可以简单转化为就是求n的全排列中的数放在棋盘上使得这几组数,符合均不 ...

  8. Java(81-93)【数组】

    1.省略格式 静态初始化的时候格式还可以省略一下 int[ ] arrayA={10,20,30}; 静态和动态都可以拆 int[] arrayB; arrayB=new int[ ]{11,21,3 ...

  9. Queue API的几种实现详解

    目录 Queue API的几种方法的使用 ArrayBlockingQueue原理及源码解析 ArrayBlockingQueue的成员变量 ArrayBlockingQueue的offer和put方 ...

  10. Spring(11) - Introductions进行类扩展方法

    Introductions(引用),在 Aspect 中称为类型间的声明,使切面能够声明被通知的对象(拦截的对象)实现给定的接口,并提供该接口的实现. 简单点说可以将一个类的实现方法复制到未实现的类中 ...