DTLS协议基础:背景、对比、握手与记录层
各位同学,咱们今天聊DTLS。说实话,很多做物联网的同学一听到DTLS就头大——「不就是TLS加了个D嘛,有啥区别?」嗯,我当年也是这么想的。直到我在一个智能路灯项目里,因为没搞懂DTLS的握手细节,导致设备在弱网环境下反复重连,差点把服务器打挂。从那以后,我再也不敢小看这个「D」了。
DTLS协议背景:为什么需要它?
先说说背景。CoAP跑在UDP上,这是它的优势——轻量、无连接、适合资源受限设备。但问题来了:UDP本身不提供任何安全机制。你想想看,一个智能门锁的指令在网络上裸奔,谁都能抓包篡改,这能行吗?
有人会说:「那直接用TLS不行吗?」不行。TLS依赖TCP的可靠传输,而UDP丢包、乱序、重复都是家常便饭。TLS的握手协议假设底层是可靠的,一旦丢包就直接断开连接。这在物联网场景下简直是灾难。
所以IETF在RFC 6347里定义了DTLS——Datagram Transport Layer Security。说白了,就是在UDP之上实现一个和TLS同等安全级别的协议,但能容忍数据报的不可靠特性。
核心要点:DTLS不是重新发明轮子,它复用了TLS的密码套件和握手逻辑,只是在处理不可靠传输上做了针对性改造。
DTLS与TLS对比:到底差在哪?
我习惯用一个表格来对比,这样一目了然。咱们直接看:
| 对比维度 | TLS | DTLS |
|---|---|---|
| 传输层 | TCP(可靠、有序) | UDP(不可靠、无序) |
| 握手方式 | 标准三次握手 | 增加重传定时器 + 无状态Cookie |
| 记录层 | 连续序列号 | 显式序列号 + 位图防重放 |
| 分片处理 | TCP自动重组 | 应用层手动分片与重组 |
| 抗DDoS | 依赖TCP半连接队列 | 无状态Cookie验证 |
| 密码套件 | 完全一致 | 完全一致 |
你看,密码套件是完全一样的。这意味着什么?意味着你在TLS上能用的加密算法,DTLS基本都能用。但实现细节上,DTLS多了不少「脏活累活」。
我的经验:如果你在选型时纠结TLS还是DTLS,记住一条原则——设备能跑TCP且网络稳定,用TLS;设备资源受限、需要低延迟、或者网络环境恶劣(比如大量丢包),果断选DTLS。
DTLS握手流程:多出来的那几步
握手是DTLS最核心的部分。咱们先看一张整体流程图,再拆开讲。
看到没?DTLS握手比TLS多了个HelloVerifyRequest的步骤。这是干嘛用的?防DDoS攻击。服务端收到ClientHello后,不直接回复ServerHello,而是先发一个带Cookie的HelloVerifyRequest。客户端必须原样带回这个Cookie,服务端才继续握手。
注意:这个Cookie是无状态的。服务端不需要保存任何会话信息,只需要验证客户端能正确返回Cookie即可。我见过有人把Cookie存在数据库里,那性能直接崩了——千万别这么干。
握手过程中还有一个容易被忽略的点:重传机制。DTLS的每条握手消息都关联一个重传定时器。如果超时没收到响应,就重传。重传次数和间隔可以配置,我一般建议初始超时1秒,最多重传5次。太频繁会加剧网络拥塞,太少又会导致握手失败。
DTLS记录层:数据怎么打包?
握手完成后,应用数据就走记录层了。DTLS记录层的格式和TLS基本一致,但有两个关键改动:
- 显式序列号:TLS的序列号是隐式的,靠TCP保证顺序。DTLS不行,它把序列号明文写在记录头里,接收方用位图(Bitmap)做防重放检测。
- 分片与重组:UDP报文有大小限制(通常MTU 1500字节,减去IP和UDP头,留给DTLS的也就1400多字节)。如果应用数据太大,DTLS必须自己分片。
记录层的格式长这样:
struct {
ContentType type; // 1字节:20=ChangeCipherSpec, 21=Alert, 22=Handshake, 23=Application
ProtocolVersion version; // 2字节:DTLS 1.2 = 0xFEFD
uint16 epoch; // 2字节:密钥周期,每次ChangeCipherSpec递增
uint48 sequence_number; // 6字节:显式序列号
uint16 length; // 2字节:负载长度
opaque fragment[DTLSPlaintext.length]; // 负载数据
} DTLSPlaintext;
这里有个坑:epoch字段。每次密钥更新(比如重新握手),epoch就加1。接收方必须检查epoch和序列号的组合,防止旧密钥的数据被重放。我曾经在一个项目中,因为epoch处理不当,导致设备在重连后还能解密旧会话的数据包——这相当于防重放完全失效了。
避坑指南:实现DTLS记录层时,建议用64位整数来存储epoch+sequence_number的组合。epoch占高16位,sequence_number占低48位。这样比较大小、做防重放位图都方便。
再说说分片。DTLS的分片是在握手消息级别做的,不是记录级别。什么意思?一条握手消息可能跨多个DTLS记录。接收方需要根据消息序号(Message Sequence)和分片偏移量来重组。我见过一些开源实现,分片重组逻辑写得稀烂,遇到乱序分片就直接丢包——这在物联网场景下是致命的。