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 1.2 完整握手流程 客户端 (Client) 服务端 (Server) ① ClientHello ② HelloVerifyRequest ③ ClientHello (with Cookie) ④ ServerHello + Certificate ⑤ Certificate + ClientKeyExchange ⑥ ChangeCipherSpec + Finished ⑦ Finished 关键差异点 • 步骤②是DTLS独有的 • 步骤③携带Cookie重发 • 每一步都有重传定时器

看到没?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)和分片偏移量来重组。我见过一些开源实现,分片重组逻辑写得稀烂,遇到乱序分片就直接丢包——这在物联网场景下是致命的。