简介:音视频通信中,RTP流裸奔在公网上极易被窃听和篡改。SRTP(安全实时传输协议)作为RTP的安全扩展,通过AES加密与消息认证保障实时媒体流的机密性和完整性。在众多实现中,libsrtp凭借其稳定性、完整API和WebRTC生态的广泛采用,成为SRTP开源库的事实标准。无论是处理srtp.rar压缩包中的r_srtp版本,还是对接DTLS-SRTP密钥协商,工程师都需要掌握从源码编译、Policy配置到srtp_protect/unprotect调用链路的完整流程。本文面向VoIP、WebRTC网关及安防流媒体开发者,解析libsrtp的选型要点、编译参数、核心API握手逻辑及常见auth_fail排查技巧,帮助你在RTP层快速构建安全传输能力。 做音视频通话、WebRTC网关或者流媒体加密传输的同学,应该都见过一个叫srtp.rar的东西。文件名里经常还混着srtp open、r_srtp、开源库这些关键词,看着挺懵,但点破了就一句话:这是一份SRTP开源库的源码包。SRTP是Secure Real-Time Transport Protocol,解决的就是RTP音视频流在网络上裸奔的问题。这篇文章我就围绕这份srtp.rar,把SRTP是什么、开源库怎么选、拿到压缩包后怎么解压编译、核心API怎么调用、踩过的坑怎么排查,从头到尾走一遍。
适合正在做WebRTC、VoIP、视频会议、安防流媒体,或者想在RTP层加一层加密但还没搞清楚库怎么用的朋友。没有那么多协议术语轰炸,尽量用干活的时候最常见的情形来讲。
1. SRTP是什么,为什么音视频项目绕不开它
1.1 从RTP到SRTP:协议栈里的那一层加密
传统RTP协议本身没有任何安全机制。你抓包抓到一段RTP流,直接用Wireshark就能看到载荷内容,如果传的是G.711语音或者未加密的H.264,别人把包存下来就能还原成声音和画面。这在公网链路、跨运营商传输、SIP外呼接运营商网关这些场景下是很要命的事。
SRTP就是在RTP头部后面做了一层加密处理,同时对RTP头部的关键字段和载荷做消息认证,防篡改、防重放。它的封装格式和RTP基本兼容,原来的RTP头、扩展头、SSRC、序列号这些字段都还在,只是载荷被加密了,尾部追加了认证标签。所以SRTP不是替换RTP,而是在RTP外面套了一层安全壳。
真正干活的加密算法有两类:AES-CM(Counter Mode,计数器模式)和AES-GCM(Galois/Counter Mode,带认证的计数器模式)。AES-CM是传统方案,后面跟HMAC-SHA1做认证,WebRTC里最常见的AES_CM_128_HMAC_SHA1_80就是它;AES-GCM是后起之秀,单算法同时完成加密和认证,性能更好,AEAD_AES_128_GCM、AEAD_AES_256_GCM都在这条线上。选库的时候如果支持GCM,优先用GCM,能少一层HMAC计算,延迟也更低。
1.2 DTLS-SRTP和密钥协商的现实关系
SRTP本身只解决“数据加密”的问题,不解决“密钥从哪来”的问题。实际项目里最常见的密钥协商方式是DTLS-SRTP。DTLS是UDP版的TLS,先用DTLS握手建立起信任关系,然后通过RFC 5764定义的方式,从DTLS密钥材料里导出SRTP的主密钥和盐值。
WebRTC里面,SRTP的密钥基本都来自DTLS-SRTP协商。你会看到dtls这个热词和srtp高频绑在一起,就是因为这两个本来就是一体的:DTLS负责“我们俩都可信”,SRTP负责“后续音视频包不可偷看”。如果不走DTLS,也有MIKEY、ZRTP或者静态预置密钥,但WebRTC生态和大多数开源网关都默认DTLS-SRTP。
1.3 srtp.rar文件名到底透露了什么
再看srtp.rar_srtp_srtp open_srtp r_srtp开源库这串词,拆开看就清晰了。
srtp.rar:说明这是一份打包好、需要解压的源码压缩包。srtp open:大概率来自两处,一是老版libsrtp里创建会话上下文的函数叫srtp_open(),二是“启用SRTP”这个动作在很多程序里被叫成“open srtp”。r_srtp:这种命名一般是发布版(release)或者某个内部改造分支的代号,不是官方标准,也可能是别人在GitHub上fork后仓促命名的仓库。开源库:说明这份压缩包最终对应的是某个开源的SRTP实现,最主流的就是libsrtp。
如果你是从某个网盘或者同事那里拿到的srtp.rar,不要被文件名里的花里胡哨带偏,第一件事永远是搞清楚里面是哪个SRTP实现、哪个版本。这决定了后面的编译选项和API写法。
2. 开源SRTP库怎么选:libsrtp为什么是默认答案
2.1 主流SRTP实现盘点
我见过有人在这个阶段纠结很久,其实不用。目前能落到工程里用的就这几个:
| 实现 | 维护方 | 特点 | 适用场景 |
|---|---|---|---|
| libsrtp | Cisco维护 | WebRTC参考实现,纯C,API稳定,文档全 | 绝大多数音视频、WebRTC网关 |
| OpenSSL内置SRTP支持 | OpenSSL | 不直接提供SRTP封装,只提供DTLS-SRTP密钥导出和加密原语 | 配合libsrtp或其他SRTP栈使用 |
| PJSIP的SRTP模块 | Teluu | 集成在PJSIP栈里,用底层libsrtp或内部实现 | PJSUA/PJSIP业务 |
| GStreamer的srtp插件 | GStreamer | 基于libsrtp封装成GStreamer元素 | 管道式流媒体处理 |
除非你的项目本身就是PJSIP或者GStreamer体系,否则从零接入SRTP,直接选libsrtp。WebRTC里的SRTP栈基本就是libsrtp,Chrome、Firefox、各种网关都能对上。它提供完整的加密算法、认证算法、重放保护、密钥导出工具,而且API分成两层:一层是srtp_*会话层,一层是crypto_*加密内核层,日常用会话层就够了。
2.2 libsrtp版本演进:srtp_open到srtp_create
网上很多老文章的代码片段里写的是srtp_open(),现在的API文档里你几乎找不到这个函数名,因为它在新版本里改名了。
libsrtp 1.x时代,创建会话上下文用的是srtp_create()和srtp_open()并存,老接口叫srtp_open()。到libsrtp 2.x以后,官方统一成两套:
srtp_create(&srtp_ctx, &policy):创建并初始化SRTP会话srtp_dealloc(srtp_ctx):释放会话srtp_protect(srtp_ctx, rtp_buffer, &len):加密RTP包srtp_unprotect(srtp_ctx, rtp_buffer, &len):解密并校验RTP包
所以你在老代码里看到srtp_open特别正常,但新项目一律用srtp_create。这也是srtp open这个词迷惑性最大的地方——它不一定是“打开SRTP功能”的意思,可能只是API函数名。
2.3 看到r_srtp这类命名时怎么判断
收到r_srtp这种目录名时,不要直接编译完就拿来集成。我的习惯是先做三件事:
- 看版本号:打开源码根目录的
configure.ac或CMakeLists.txt,看SRTP_MAJOR_VERSION和SRTP_MINOR_VERSION。如果是2.x,直接按新API写;如果是1.x,注意接口区别。 - 看README和ChangeLog:确认是不是官方标签打出来的release包,还是被别人改过内部加密逻辑。搞过安防网关的人应该懂,很多厂家在libsrtp上改过算法轮数或者密钥派生逻辑,这种定制包一定要留着对应文档。
- 看测试用例:跑一下官方的test程序,如果自带的
rtpw_test能通过,说明这包基本是完整的,如果失败,先说包有问题,别急着怪自己代码。
3. 拿到srtp.rar之后:解压、认目录、编译一套走
3.1 用rar解压软件解开压缩包
文件名是srtp.rar,第一步肯定是解压。这套在Linux开发机、Windows上都能处理。
Windows下用WinRAR或者7-Zip就能解,右键直接解开。Linux下如果没装rar工具,先去装个unrar:
sudo apt install unrar # Debian/Ubuntu sudo yum install unrar # CentOS/RHEL 需要先开EPEL解压命令:
unrar x srtp.rar或者用7-Zip:
7z x srtp.rar解压后如果看到的是根目录下直接一堆.c、.h文件,那说明打出来的包没包好,建议统一建一个工作目录再放进去;如果看到的是一个以libsrtp-2.x.x或者r_srtp命名的目录,那直接进目录。
这里要说一个热词里提到的“rar密码移除”。如果有人给你的srtp.rar是带密码的,标准做法是找打包人索取密码,或者让对方重新打成不带密码的包。网上那些所谓的“rar密码移除”工具,我在实际项目里不建议碰——绝大多数是捆绑垃圾软件甚至木马,而且能破解的密码强度本身就说明这个分发包来源不可信。安全第一,宁可重新下载一份官方源码包。
3.2 源码目录结构和核心文件
解压完不要急着编译,先花五分钟把目录结构看一遍。以libsrtp 2.x为例子:
libsrtp/ ├── include/ │ ├── srtp.h # 主头文件,所有API都在这里 │ └── crypto_math.h # 加密数学工具函数 ├── crypto/ │ ├── cipher/ # AES等加密算法实现 │ ├── hash/ # HMAC-SHA1等认证算法 │ └── kernel/ # 加密内核配置 ├── src/ │ ├── srtp.c # 核心SRTP逻辑 │ ├── rtp.c # RTP包解析封装 │ └── dtls_srtp.c # DTLS-SRTP密钥导出实现 ├── test/ │ ├── rtpw_test.c # 官方测试程序 │ └── srtp_driver.c # 驱动级测试 ├── configure.ac ├── CMakeLists.txt └── Makefile.in对业务代码最关键的只有两个:include/srtp.h和src/srtp.c。前者是头文件,后者是核心实现。dtls_srtp.c在做WebRTC对接时很有用,密钥导出那一截就在这。
3.3 configure编译参数与常见选项
libsrtp的构建有两种方式,传统的是autotools,新一点的也支持CMake。我日常用autotools多一点,编译命令非常稳定:
./configure --enable-openssl --prefix=/usr/local make -j$(nproc) sudo make install--enable-openssl这个参数很重要。它让libsrtp使用OpenSSL的加密函数库,而不是自带的那个纯C实现。自带实现在AES-CM场景下还能用,但性能一般,而且在GCM模式下没优势。实际生产环境链接系统OpenSSL是更稳的选择,还能借助硬件加速。
如果要用AES-GCM系列算法,老版本需要显式开:
./configure --enable-openssl --enable-gcm --prefix=/usr/local新版默认就把GCM编进来了,但我还是习惯显式加上,防止版本行为不一致。编译完以后,检查一下生成的目标文件,确认有libsrtp2.so(或.a),同时看一下pkg-config能不能正常找到:
pkg-config --libs --cflags libsrtp2如果你的项目用CMake,也可以这样:
cmake -B build -DENABLE_OPENSSL=ON cmake --build build cmake --install build3.4 交叉编译到嵌入式平台
做音视频终端、IPC、边缘网关时,免不了交叉编译。libsrtp作为纯C库,交叉编译难度不大,核心就是给configure传对工具链参数。
./configure \ --host=aarch64-linux-gnu \ --enable-openssl \ --with-openssl-dir=/your/sdk/usr \ --prefix=/usr/local \ CC=aarch64-linux-gnu-gcc \ CXX=aarch64-linux-gnu-g++ make -j$(nproc) make install DESTDIR=/your/sysroot注意点只有两个:
--with-openssl-dir一定要指到交叉编译环境里的OpenSSL目录,不能指到宿主机。不然链接阶段会出现“架构不匹配”的报错。- 如果最终产物要打包成静态库给第三方,加一个
--with-pic,位置无关代码对后续打进so还是可执行文件都有好处。
4. 核心API实操:从srtp_init到加解密完整链路
4.1 初始化与Policy配置
几乎所有使用libsrtp的代码,第一步都是:
#include <srtp.h> srtp_init();SRTP_init在libsrtp 2.x里是个宏或实际函数,用于初始化库内部状态、加密内核、随机数源。如果你没有调用它就直接srtp_create,会返回srtp_err_status_init,这是新手最常见的错误。
接下来配置policy。policy是整个SRTP最核心的数据结构,它决定了用什么加密算法、用什么认证算法、密钥是什么、SSRC行为、重放窗口大小等。一个典型的RTP加密配置长这样:
srtp_policy_t policy; srtp_t srtp_ctx; srtp_err_status_t err; /* 1. 清零,避免脏数据 */ memset(&policy, 0, sizeof(policy)); /* 2. 选择加密和认证策略,这里用最常见的AES-CM-128 + HMAC-SHA1-80 */ srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&policy.rtp); srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&policy.rtcp); /* 3. 设置随机派生的密钥 */ uint8_t key[30]; // 16字节key + 14字节salt rand_bytes(key, sizeof(key)); // 真实项目里这个key来自DTLS-SRTP导出 /* 4. 设置SSRC行为 */ policy.ssrc.type = ssrc_any_outbound; // 出向使用具体SSRC,或者inbound用any policy.ssrc.value = 0; // ssrc_any_outbound时可以为0 /* 5. 重放窗口和重复发送开关 */ policy.window_size = 128; policy.allow_repeat_tx = 1; // 允许重传相同包(比如DTMF重复包) /* 6. key和链表指针 */ policy.key = key; policy.next = NULL; /* 7. 创建会话 */ err = srtp_create(&srtp_ctx, &policy); if (err != srtp_err_status_ok) { printf("srtp_create failed: %d\n", err); }如果选择GCM,把第二步换成:
srtp_crypto_policy_set_aes_gcm_128_16_auth(&policy.rtp); srtp_crypto_policy_set_aes_gcm_128_16_auth(&policy.rtcp);GCM模式下的密钥长度也会变化:AES-128-GCM是16字节key + 12字节salt,所以key缓冲区大小是28字节。
4.2 srtp_protect和srtp_unprotect实战
密钥协商完成后,发包前调用srtp_protect,收包后调用srtp_unprotect,这是SRTP最核心的两个动作。
看一个完整的发送端例子:
uint8_t rtp_packet[2048]; size_t len; /* 构造一个RTP包:在rtp_packet里填好RTP头,然后拷贝音视频载荷 */ /* ... 省略RTP头填充过程 ... */ len = rtp_payload_len + rtp_header_len; err = srtp_protect(srtp_ctx, rtp_packet, &len); if (err != srtp_err_status_ok) { printf("srtp_protect failed, err=%d\n", err); return; } /* 注意:len在srtp_protect之后会变成SRTP包的完整长度,包含认证标签 */ sendto(sockfd, rtp_packet, len, 0, (struct sockaddr*)&dest, sizeof(dest));接收端的逻辑基本镜像:
uint8_t recv_buffer[2048]; size_t recv_len = recvfrom(sockfd, recv_buffer, sizeof(recv_buffer), 0, ...); err = srtp_unprotect(srtp_ctx, recv_buffer, &recv_len); if (err != srtp_err_status_ok) { /* auth_fail表示密钥不匹配或数据被篡改 */ printf("srtp_unprotect failed, err=%d\n", err); return; } /* recv_len在unprotect后是原始RTP包长度,继续按RTP解析 */这里有两个实际工程里容易踩的坑:
第一,len在srtp_protect之后会变大。HMAC-SHA1-80会追加10字节认证标签,GCM-128-16会追加16字节认证标签。如果缓冲区大小不够尾部追加,就会溢出。所以我喊一句:发送缓冲区和接收缓冲区都至少比最大RTP包多预留32字节。
第二,srtp_protect是在原地把RTP头、扩展头里的内容做了处理,SSRC、序列号这些字段可能被改写,所以不要在调用后还拿原始RTP包当参照。正确做法是先把RTP包留着,真正要发的时候再拷贝到发送缓冲区里调srtp_protect。
4.3 从DTLS握手导出SRTP密钥
真实WebRTC或SIP over DTLS场景里,SRTP密钥不是自己随机生成的,而是从DTLS握手结果里导出的。RFC 5764规定了一个术语“EXTRACTOR-dtls_srtp”标签,握手完成后从TLS主密钥中提取出密钥材料。
如果用libsrtp提供的API:
#include <srtp.h> #define DTLS_SRTP_KEYING_MATERIAL_LEN 60 // AES_CM_128_HMAC_SHA1_80:2*(16+14)=60 uint8_t keying_material[60]; srtp_dtls_keying_material_t dtls_keys; srtp_err_status_t err; /* 经过DTLS握手后,从TLS栈获得keying_material(例如在OpenSSL里通过 SSL_export_keying_material(material, 60, "EXTRACTOR-dtls_srtp", ...)) */ /* ... */ err = srtp_dtls_keying_material_export( keying_material, NULL, /* salt,传NULL表示不额外加盐 */ &dtls_keys); if (err != srtp_err_status_ok) { printf("srtp_dtls_keying_material_export failed\n"); return; } /* dtls_keys.master_key现在指向16字节主密钥, dtls_keys.master_salt指向14字节salt */ memcpy(key_buffer, dtls_keys.master_key, 16); memcpy(key_buffer + 16, dtls_keys.master_salt, 14);如果你不想依赖libsrtp这个导出函数,也可以自己拆。规则很简单:客户端取keying_material的前30字节,服务端取后30字节,前16字节是加密key,后14字节是salt,组合起来填入policy.key。很多auth_fail问题都是这段取反了,后面排查部分会再提。
4.4 多流和RTCP的扩展玩法
一个会话里处理多个媒体流,是WebRTC的常态。音频一个SSRC,视频一个SSRC,甚至屏幕共享再来一个SSRC。
libsrtp的policy链表机制支持多流:一个流一个policy节点,通过policy.next串起来,一次srtp_create就能创建多个加密上下文。代码上就是把上面那段policy配置复制成多份,各自设置不同的ssrc.value,然后用next连起来。
RTCP的加密走的是另一套policy字段:policy.rtcp。虽然RTCP包我们一般不加密(很多场景只做认证),但标准要求必须设置。如果不想加密RTCP,可以用srtp_crypto_policy_set_null_cipher_hmac_sha1_80()这种null cipher策略,只认证不加密,这样可以省一点CPU,同时保证控制报文可信。
5. 常见问题与排查技巧实录
5.1 压缩包层面的问题
- rar解压损坏:下载过程中文件损坏很常见,尤其是一个几十MB的
srtp.rar。用unrar t srtp.rar测试压缩包完整性,如果提示损坏,直接重新下载,不要尝试修复,强行修复出来的源码可能在编译时出现诡异报错。 - 带密码的压缩包:优先找来源方要密码。用破解工具不靠谱,而且你无法验证破解后的代码有没有被替换过。安全项目里这是铁律。
- 打包方式混乱:有的包解开后不是标准目录,而是直接把一堆
.c.h文件平铺在根目录。这种多半是Windows上右键压缩的产物,不影响编译,但建议自己重建目录结构再动手。
5.2 srtp_create/open失败这类init问题
| 错误码 | 场景 | 处理方式 |
|---|---|---|
srtp_err_status_init | 没有先调srtp_init() | 程序最开始调用一次srtp_init() |
srtp_err_status_fail | policy参数非法、key为空 | 检查policy.key、policy.cipher_type、policy.auth_type是否都已经设置 |
srtp_err_status_no_ctx | 传了NULL的srtp_t* | 检查srtp_create第一个参数是否指向有效的srtp_t变量 |
srtp_err_status_alloc_fail | 内存不足,或者栈碎片严重 | 嵌入式环境尤其常见,检查堆空间 |
policy没清零是非常隐蔽的错误。结构体里有不少保留字段和内部标志,如果不memset清零,里面有随机值,srtp_create在解析时就会出现莫名其妙的问题,报错都不是固定的。这就是我写policy配置时永远先memset的原因。
5.3 加解密失败与auth_fail
auth_fail是SRTP排查里最高频的错误。它明确说明认证标签不对,也就是对端拿到的数据和我方发的数据不一致。大部分原因按照出现频率排序:
- 密钥材料分裂错误:DTLS导出的60字节,客户端和服务端各用一半。两边如果都取了前30字节,必然auth_fail。代码里用
dtls_keys.master_key直接建policy的一般没事,手工拆的人最容易错。 - SSRC没有对齐:SRTP的加密IV依赖SSRC。发送端用
ssrc_specific,接收端用了ssrc_any_outbound且实际流的SSRC对不上,解密出来就是乱码或者auth_fail。调试时可以把两端的SSRC打印出来比对。 - 丢包顺序问题:SRTP重放保护默认窗口是128,如果网络丢包超过窗口范围,老包会被当重放包丢弃,然后大量auth_fail。这种情况把
policy.window_size调大,比如1024,同时检查UDP链路为什么要丢这么多。 - RTP扩展头没有协商好:如果你的RTP包带扩展头,SRTP处理扩展头时两边对扩展头的理解不一致,认证标签就会对不上。这个在WebRTC接入SIP网关时容易遇到,先关掉RTP扩展头试一下。
我自己的排查习惯是:先打印错误码,再有条件的话在srtp_unprotect失败时同时打印SSRC、序列号、包长和认证标签的前4字节,四个值一对比,80%的auth_fail都能定位。
5.4 重放保护、丢包与调优建议
SRTP的重放保护看似简单,实际对音视频实时性影响很大。默认窗口大小是128,意思是SRTP允许序列号回退最多128个包再丢。对于正常网络这够用,但在WiFi切换、弱网传输、FEC重传的时候,包可能延后比较久才到,超过窗口就会被丢弃。
我的建议是:
- 局域网内或者强调实时性的场景,窗口不用太大,128够用。
- 跨公网、弱网、有FEC重传机制的场景,窗口调到512或者1024,配合对端的重发策略。
- 千万不要为了排查方便把窗口设成0,那等于完全关闭重放保护,生产环境裸奔,认证和重放防御全失效。
另外,GCM模式下包长度和CPU占用都比CBC/HMAC模式低,如果两端都是libsrtp 2.x且启用了OpenSSL,尽量用AEAD_AES_128_GCM。我用同样的机器对比过,同样10000包,AES-CM+HMAC和AES-128-GCM的加解密耗时大概能差出25%~35%,在移动端上这个差距更明显。
再补充一个和MTU相关的小经验。SRTP会给每个RTP包增加认证标签长度,HMAC-SHA1-80是10字节,GCM是16字节。如果你原来每个RTP包是1400字节(UDP载荷),加密后变成1410/1416字节。在运营商或者路由器MTU比较紧的网络里,超一点就会分片或者被丢。所以做媒体层时,给RTP载荷留一点裕量,宁可少放几个字节的音频帧,也要保证加密后UDP包不超过MTU。
我在实际项目里折腾libsrtp,最深的体会是这库本身很简洁,但它的坑全在周边:密钥材料从哪来、policy配没配对、缓冲区留没留够。很多人卡在auth_fail卡两天,最后发现就是客户端服务端取反了half材料,或者RTP扩展头没对齐。如果你也是刚拿到一份srtp.rar准备集成,建议按这个顺序走:先解压确认版本,再跑通官方test把基础环境验证好,接着用自己的DTLS栈把密钥导出到libsrtp,最后再接入你的RTP收发循环。每一步单独验证,会比自己一口气写完再调省非常多时间。
最后分享一个调试小技巧:在把srtp_protect接进正式媒体链路之前,先用一个简单的UDP socket把音频包发到对端,看看能不能正常srtp_unprotect还原。这一步能隔离出问题是出在RTP构造、DTLS协商还是SRTP库本身,别把三件事混在一起查,不然一个auth_fail能把人查崩溃。
本文还有配套的精品资源,点击获取