要解决的问题
假设存在这样一种 RTC 建连场景:客户端和服务器在 DTLS 握手完成之后才开始信令交互和媒体数据传输。这种建连场景使得客户端的信息(比如各种 ID 和 SDP)要等到 DTLS 握手成功之后才能被服务器知晓。
某次建连失败(DTLS 握手失败,但是你不知道是这个原因)。你很自然的使用日志系统去排查问题,但是却尴尬的发现日志系统没有展示任何日志(DTLS 握手失败导致客户端的 ID 体系没有在服务器建立,而日志系统需要依赖这些 ID 才能展示服务器日志)。
也就是说:
- 如果建连的第一步 DTLS 握手失败了,那么日志系不会展示任何服务器日志。
- 无论 DTLS 握手成功还是失败,日志系统都不会展示任何 DTLS 握手这个阶段的日志。
因此只好:
- 去日志存储数据库查询这次建连的日志。
- 手动登服务器查询这次建连的日志。
这两种查询方式很麻烦,而且如果是服务集群挂载在 SLB 后面,难不成要挨个登录服务器去查询日志在哪台机器上?
最后你使出九牛二虎之力,终于定位到建连失败的原因是 DTLS 握手失败。你开始思考,如果日志系统能展示 DTLS 握手阶段的日志,那不就一眼定位到 DTLS 失败的问题了吗?
是的,只要能在 DTLS 握手阶段知道客户端的 ID,那么 DTLS 相关的埋点就能在日志系统展示!
那么,如何才能做到这一点呢?解铃还需系铃人,DTLS 的问题 DTLS 来解。我们通过使用 DTLS 扩展来传递客户端 ID 信息,就能轻松搞定这个问题。
DTLS 扩展
DTLS 的扩展在 Hello 消息(Client Hello 或者 Server Hello)中携带,也叫做 Hello Extensions。扩展的格式为 TLV 格式,即 Type、Length、Value。Type 取值范围是 0 ~ 65535,0 ~ 59 是已定义的扩展类型,其余基本为保留和未分配。
如果我们要自定义扩展类型,则可以从未分配的扩展类型(比如 60 ~ 2569)中选取。
下图展示了 ClientHello 中的各个扩展。
下面展示了 ServerHello 中的各个扩展。
关于这些 Extensions,有以下几点规则:
- ServerHello 中不能出现 ClientHello 中没有携带的扩展,否则握手必须被取消。
- 当 Hello 消息中有多个不同类型的扩展时,这些扩展的顺序可能是任意的。
- 同一类型的扩展只能有一个。
增加自定义扩展
下面,我们看下如何在客户端增加自定义扩展类型,来传递客户端 ID。
其实很简单,只需要在 ssl context 创建好之后调用这个 API 即可。
int SSL_CTX_add_client_custom_ext(
SSL_CTX *ctx, unsigned int ext_type,
custom_ext_add_cb add_cb,
custom_ext_free_cb free_cb, void *add_arg,
custom_ext_parse_cb parse_cb,
void *parse_arg);
客户端要增加自定义扩展,因此只要传入 ctx,ext_type,add_arg,实现 add_cb 和 free_cb 即可。
unsigned int ext_type = 323;
char* add_arg = "taiyi0323";
SSL_CTX_add_client_custom_ext(
ctx,
ext_type,
ext_add_cb,
ext_free_cb,
add_arg,
NULL,
NULL);
可以按照上述方式调用,返回 1 代表成功,0 代表失败。
下图展示了我们自己增加的自定义扩展。
解析自定义扩展
使用 API 解析
服务器收到 Client Hello 后,就需要解析自定义扩展,拿到客户端的 ID,服务器需要调用下面的 API。
int SSL_CTX_add_server_custom_ext(
SSL_CTX *ctx, unsigned int ext_type,
custom_ext_add_cb add_cb,
custom_ext_free_cb free_cb, void *add_arg,
custom_ext_parse_cb parse_cb,
void *parse_arg);
服务端只需要解析自定义扩展,因此只要传入 ctx,ext_type,parse_arg,实现 parse_cb 即可。
char* parse_arg = NULL;
SSL_CTX_add_server_custom_ext(
ctx,
823,
NULL,
NULL,
NULL,
parse_cb,
parse_arg);
可以按照上述方式调用,返回 1 代表成功,0 代表失败。
自己实现解析
也可以自己实现自定义扩展的解析函数,注意以下几点:
- random 字段及之前的 49 字节固定,可以直接跳过,不做解析
- 由于多个扩展的顺序是任意的,所以不能按照顺序来解析扩展,而是在循环中解析
- 在解析多字节字段的时候注意网络字节序问题,高位字节要放到低地址处
- 注意异常判断,比如 buffer 剩下的数据长度不满足待解析的数据长度
总结
协议离不开扩展,很多时候我们都忽视了扩展的价值。比如通过 DTLS 自定义扩展传递客户端 ID 信息,解决了日志系统展示日志的问题。扩展能够扩展我们的思路,能够带来创新,比如 AliRTC 的 GRTN 协议(链接),就是对 RTCP APP 的扩展。希望大家以后在学习协议时,在工程实战时能够重视协议的扩展,让它发挥出该有的光和热。
本文没有展示 DTLS 自定义扩展的代码实现细节,完整的实现细节可以参考我写的 一个简单的 DTLS 握手 demo。
谢谢阅读。