简介这是一份面向VoIP、视频会议等实时通信场景的SRTP开源库压缩包适合需要集成安全实时传输能力的C开发者。资源内含已经用VC7Visual Studio 2005编译好的工程文件可直接打开编译与调试省去自行配置构建环境的麻烦库中涵盖AES加密、AES-ICM流密码、SHA-1完整性校验、密钥管理与RTP封装等核心模块便于理解SRTP协议在加解密、防篡改与认证方面的完整实现。压缩包共168个文件主体为C源文件45个与头文件34个另有Visual Studio工程配置、Makefile、configure脚本及说明文档等包体仅547KB轻量易用。资源整体代码结构清晰既有可运行的驱动与测试程序也保留了构建所需的基础设施适合用于学习协议细节、二次开发或移植到其他平台。已有196人学习浏览是一份值得参考的SRTP入门与工程实践资料。1. srtp开源库到底在防什么RTP明文流的加密封装拿到“srtp开源库”常见发行名是libsrtp标题里的srtp.rar、srtp open、r_srtp大多是同一套代码的不同打包或命名形式之后首先要搞清楚它的定位RTP协议在设计时没有考虑任何保密性语音、视频、RTCP控制信息在网络上全部是明文抓包就能直接还原通话内容。srtp开源库就是在不改动RTP头部结构的前提下对负载做加密和完整性认证同时防重放。如果你正在做WebRTC服务端、音视频网关、SIP媒体中继这类项目这个库几乎是绕不开的。下面从协议机制讲到编译、接入、排错再到验证全链路走一遍。2. SRTP协议关键机制与libsrtp选型加密、认证、防重放怎么协同2.1 RTP为什么需要单独做安全层RTP包分为头部和负载。头部携带sequence number、timestamp、SSRC这些字段必须保持明文因为路由器、对端、丢包统计逻辑都要读它。负载是编码后的音视频帧。问题在于负载本身也是明文。SRTP的思路是头部不动只加密负载同时在包尾部追加一个认证标签。认证标签覆盖头部和密文让接收方能够发现包被篡改或伪造。RTCP同样需要保护。SRTCP是RTCP的加密版本复用同一套密钥和派生逻辑只是包索引的维护方式不同。设计上四种包类型分别是SRTP、SRTCP、以及未加密但带认证的扩展头模式。引入SRTP后一个明显的好处是中间设备照常工作NAT、QoS、流量统计都不受影响代价是每个包多出若干字节的认证标签以及加解密带来的CPU开销。libsrtp把所有复杂细节封装在protect和unprotect两个函数里。protect输入明文RTP包输出SRTP包unprotect输入SRTP包输出明文RTP包。密钥管理、ROC维护、重放窗口都在内部完成。这对应用层来说是黑匣子但黑匣子一旦出错最难排查所以后面会有一整章专门讲坑。2.2 加密、认证、密钥派生三个子系统的参数默认加密算法是AES-CTR。CTR模式本质上是把计数器值加密后与明文异或属于流密码不需要填充按字节对齐性能高且可并行。默认认证算法是HMAC-SHA1输出80位标签很多场景会裁到32位来省带宽。2017年之后的libsrtp完整支持AES-GCM推荐优先用AES-GCM它把加密和认证合并成一个算法延迟更低且认证标签可以设成8字节。密钥派生是另一个容易理解错的部分。系统里有一个master key16字节或32字节和master salt14字节两者不能直接用于加密。真正的session key要根据当前包的SSRC和包索引用PRF派生出来。SRTP的包索引是48位的高32位叫ROC低16位就是RTP头里的sequence number。ROC只有在seq从65535回绕到0时才加一所以接收端允许一定程度的丢包而不会把索引算错。来看一个常用的策略对照表策略名称加密算法认证算法标签长度适用场景AES_CM_128_HMAC_SHA1_80AES-CTR 128位HMAC-SHA180位WebRTC历史兼容AES_CM_128_HMAC_SHA1_32AES-CTR 128位HMAC-SHA132位带宽敏感场景AES_GCM_128_8_AUTHAES-GCM 128位GCM内置8字节新项目首选AES_GCM_256_8_AUTHAES-GCM 256位GCM内置8字节高安全等级需求选型上目前主流的开源实现就是libsrtp。理由有三点一是WebRTC生态里大量服务端基于它接口经过充分验证二是自带完整的RFC标准测试向量和测试工具三是许可证对商业集成友好。标题里的r_srtp、srtp open在GitHub上大多是同一代码仓库的fork或旧版打包功能上等价建议统一跟libsrtp主线走避免遇到无人维护的编译问题。2.3 重放窗口与ROC必须理解的两个边界重放窗口是一个滑动位图接收方记录最近收到的包索引对超过窗口的旧包直接拒绝。libsrtp里用policy.window_size控制单位是个包。窗口设得越大容忍乱序能力越强内存也越多。设成128到256在实际音视频网络中足够如果设成0等于是严格有序模式在互联网上几乎必崩。ROC的维护在发送端和接收端是不同步的。发送端持续递增自己的索引接收端则要根据收到的seq跳跃来估算ROC。libsrtp内部做了这个估算但如果应用层自己拼包、改包、转发包很容易破坏ROC的推演逻辑。这也是多流处理中翻车最多的原因后面第4章会专门展开。3. 编译libsrtp并跑通最小protect/unprotect从srtp.rar到可运行demo3.1 从源码包到可运行库的编译步骤假设你已经拿到srtp.rar并解压到srtp目录根目录下就是configure脚本和源码树。libsrtp使用autotools构建习惯上我不用系统默认路径而是指定独立prefix方便后续卸载和切换版本。先确认系统有gcc、make和libssl-dev。依赖OpenSSL不是硬性的libsrtp自带crypto实现但开启OpenSSL后AES-GCM在x86上能获得明显加速。cd srtp ./configure --prefix/usr/local/srtp --enable-openssl make -j4 make install逻辑说明第一行进入解压后的源码根目录。configure会检测平台、编译器、OpenSSL头文件位置生成Makefile。--enable-openssl让AES-GCM和AES-CTR都走OpenSSL的加密实现在桌面和服务平台上是推荐做法。make -j4并行编译4表示同时用4个任务机器核多可以调大。make install把头文件和库装到/usr/local/srtp下。参数说明--prefix指定安装目录改成/usr/local或$HOME/srtp都可以关键是让后续编译时找得到。--enable-openssl需要系统装了libssl-dev如果目标平台是嵌入式且没有OpenSSL去掉这个开关即可内置crypto照样支持AES-GCM。configure还提供--enable-debug调问题非常有用和--enable-stdout让测试工具往终端打日志排错阶段建议全开。编译安装完成后头文件在/usr/local/srtp/include/srtp2/库文件是libsrtp2.so。写代码时include路径写srtp2/srtp.h链接参数为-L/usr/local/srtp/lib -lsrtp2。3.2 最小加密与解密demoprotect和unprotect的完整调用下面的demo模拟两端通信一端用固定密钥加密一个模拟RTP包另一端用相同密钥解出明文并校验完整性。密钥固定写死省去信令协商的干扰方便你验证明白加解密链路本身是通的。#include stdio.h #include string.h #include srtp2/srtp.h // master_key(16字节) master_salt(14字节) 30字节 static uint8_t test_key[30] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d }; int main(void) { srtp_t srtp_send NULL, srtp_recv NULL; srtp_policy_t policy; crypto_policy_t crypto; srtp_init(); // 使用AES-CM 80位HMAC-SHA1WebRTC默认策略 crypto_policy_set_aes_cm_128_hmac_sha1_80(crypto); memset(policy, 0, sizeof(srtp_policy_t)); policy.ssrc.type ssrc_any_inbound; // 接受任意SSRC policy.ssrc.value 0; policy.key test_key; policy.rtp crypto; policy.rtcp crypto; policy.window_size 128; policy.allow_repeat_tx 0; policy.enc_xtn_hdr NULL; policy.enc_xtn_hdr_count 0; policy.next NULL; srtp_create(srtp_send, policy); srtp_create(srtp_recv, policy); // 模拟一个RTP包12字节头 10字节payload uint8_t rtp[22]; uint8_t enc[64]; int len sizeof(rtp); memset(rtp, 0, sizeof(rtp)); rtp[0] 0x80; // RTP版本2无扩展头 memcpy(rtp[8], 1234, 4); // SSRC仅作演示 memcpy(rtp[10], helloSRTP, 9); // 负载 memcpy(enc, rtp, sizeof(rtp)); srtp_protect(srtp_send, enc, len); printf(protect: len%d\n, len); srtp_unprotect(srtp_recv, enc, len); printf(unprotect: len%d payload%.10s\n, len, (char*)enc[12]); srtp_dealloc(srtp_send); srtp_dealloc(srtp_recv); return 0; }逻辑说明srtp_create把策略复制到会话内部所以policy变量可以复用。srtp_protect把密文和认证标签写回原缓冲区并把len更新为加密后的总长度。本例原包22字节80位认证标签追加10字节加密后len变成32。srtp_unprotect先做完整性认证认证通过才解密失败会返回srtp_err_status_auth_failure此时缓冲区内容不可信不能直接用。两个会话使用相同key真实系统里不要这么做这只是本地自测。参数说明policy.key必须是恰好30字节多一个少一个都会在srtp_create时返回错误。ssrc_any_inbound表示接受任意SSRC的入站包如果固定为某个值其他SSRC的包会被拒绝。window_size128就是重放窗口允许最多128个包的乱序。allow_repeat_tx控制是否允许重复包发送RTCP场景可能用到RTP一般设0。crypto_policy_set_aes_cm_128_hmac_sha1_80选的是80位标签想省带宽换crypto_policy_set_aes_cm_128_hmac_sha1_32。编译命令为gcc demo.c -I/usr/local/srtp/include -L/usr/local/srtp/lib -lsrtp2 -o demo运行前可能需要export LD_LIBRARY_PATH/usr/local/srtp/lib。3.3 从单包到一个循环连续处理多包的注意点实际场景不会只加解密一个包。一个典型循环是上层RTP模块填好包头和负载然后调用protect发送接收端对每个收到的UDP包直接调unprotect。需要留意的是libsrtp不会帮你管理RTP包的seq字段它只读取seq并维护内部索引。所以发送端每发一个包seq要自己递增并写进包头这和普通RTP发送逻辑完全一样。还有一个细节protect只加密负载和指定的扩展头12字节基础RTP头保持原样。这也意味着如果上层在protect之后又去改头部的seq或SSRC认证标签就失效了。我见过有人先把包发出统计打点后又改了字段导致对端持续auth_failure排查了一整天才发现是修改了已经加密的包。4. 把SRTP接进真实RTP流SSRC会话管理、ROC维护与DTLS-SRTP对接4.1 单流场景固定SSRC的转发最简单的场景是单一SSRC的RTP转发比如一个音视频网关只处理一路通话。创建会话时可以指定policy.ssrc.type ssrc_specific并填上固定值。这样libsrtp内部可以对SSRC做校验防止串流。但实际接入中更推荐用ssrc_any_inbound因为WebRTC会话里的视频流、音频流SSRC通常是协商出来的不一定会在初始化前就知道。单流转发逻辑如下UDP收包后如果包类型是RTP调用srtp_unprotect还原明文明文包交给后面的媒体处理逻辑要发出时再对明文RTP调用srtp_protect。整个过程阻塞在单个会话上不需要考虑锁和会话切换性能最容易做到极致。4.2 多流与ROC的坑不能把多路流灌进同一个会话多流场景下每一路SSRC必须对应独立的srtp_t实例。原因在于ROC是按流维护的如果两个不同流共用同一个会话包序号互相干扰ROC的推演立刻崩掉。我在做多路合流时栽过这个跟头把两个终端的RTP流都往同一个srtp会话里protect对端全部返回认证失败报错还带歧义。排查很久才发现是会话混用。正确的做法是按SSRC建立会话表static srtp_t session_table[256]; srtp_t get_session_by_ssrc(uint32_t ssrc) { // 简化实现SSRC取模映射 int slot ssrc % 256; if (session_table[slot] NULL) { srtp_policy_t policy; memset(policy, 0, sizeof(policy)); policy.ssrc.type ssrc_any_inbound; policy.key master_key; crypto_policy_set_aes_gcm_128_8_auth(policy.rtp); policy.rtcp policy.rtp; policy.window_size 128; srtp_create(session_table[slot], policy); } return session_table[slot]; }逻辑说明这段代码用SSRC取模找会话找不到就新建。srtp_create内部会复制policy所以可以复用同一个master_key。AES-GCM的认证标签是8字节比HMAC-SHA1的80位标签省2字节带宽多流场景下这个收益更明显。参数说明ssrc_any_inbound让会话接受任何SSRC入站包但发送端的srtp_protect会把自己维护的SSRC写进包。会话表要做好并发保护多个收发线程同时命中同一个slot时需要加锁或按本次会话分离。srtp_dealloc在流结束时就要调用否则长期运行会内存泄漏。还有一个细节RTCP也要走会话policy.rtcp没有单独配置时会沿用rtp策略但libsrtp对SRTCP默认使用较短的认证标签这没问题不需要改。多流场景下输出方向的选择也很关键。一个会话只能有一个发送方向和一个接收方向如果网关既要转给终端A又要转给终端B必须建两个会话密钥也各自独立。单向会话其实可以同时支持protect和unprotect但密钥都一样时两个网关之间就会互相解密出乱码。4.3 和WebRTC对接DTLS-SRTP的密钥怎么切分WebRTC的媒体面通过DTLS握手协商出密钥然后交给SRTP使用。DTLS-SRTP导出的keying material是一个长串包含client write key、server write key、client write salt、server write salt加起来对应两端各自的30字节主密钥和盐。这里最容易错的是顺序不是简单地把前30字节给发送端、后30字节给接收端而是按握手的角色来分配。// 从DTLS导出的keying material约60字节中切出两端密钥 // is_client表示本端在DTLS握手中是client角色 void split_keying_material(const uint8_t* mat, int is_client, uint8_t* send_key, uint8_t* recv_key) { // 每个方向30字节16字节key 14字节salt if (is_client) { memcpy(send_key, mat, 30); // client用client_write_key加密发送 memcpy(recv_key, mat 30, 30); // 用server_write_key解密接收 } else { memcpy(send_key, mat 30, 30); // server用server_write_key加密发送 memcpy(recv_key, mat, 30); // 用client_write_key解密接收 } }逻辑说明DTLS-SRTP的RFC 5764规定keying material从两端的PRF输出中直接切块。client write key在前server write key在后。所以client端发送密钥取前30字节server端发送密钥取后30字节。这段代码做了一个干净的封装避免业务层看到裸字节。参数说明libsrtp默认的crypto_policy是AES_CM_128_HMAC_SHA1_80对应恰好16字节key加14字节salt。如果改用AES-GCM-128key和salt长度不变但整个抓包流量特征会不一样。还有一点WebRTC实际用法是每个RTP流音频、视频、FEC等各建一个srtp会话虽然是同一把密钥切分出来的但SSRC范围限制可以更严格。5. SRTP接入避坑手册认证失败、GCM不可用和MTU截断的排查5.1 unprotect持续返回auth_failure抓包看不出问题现象接收端srtp_unprotect一直返回srtp_err_status_auth_failure抓到的SRTP包头部正常负载也是密文格式上没问题。原因大多数时候是两端密钥不一致。尤其是WebRTC接入时client/server方向的keying material切反了或者只拷贝了16字节主密钥而丢了14字节盐。还有可能是发送端在protect之后又修改了包内容。解决先固定一个测试密钥把发送和接收都初始化成同一个30字节key确认protect/unprotect回环能过。这一步过了之后再接入真实协商逻辑把两端的key通过日志打出来逐字节对比。我建议打印时对salt部分单独标注因为salt错误很隐蔽加密算法可能照常跑但认证一定失败。5.2 配置了AES-GCM却报unsupported现象configure时带了--enable-openssl代码里调用crypto_policy_set_aes_gcm_128_8_auth运行时srtp_create返回unsupported。原因libsrtp的GCM支持编译期就决定了。如果configure没有检测到OpenSSL或者OpenSSL版本低于1.1GCM会被禁用静默回退到内置加密而内置加密在旧版本中只支持AES-CM。解决检查config.log中openssl的检测结果。configure加--with-openssl/usr/local/ssl明确指定路径。如果目标平台确实没有OpenSSL用libsrtp自带crypto也能跑AES-GCM只是性能受限。5.3 大包在UDP传输中被截断现象protect之后包长度比原RTP多了10字节发送端和接收端在同一台机器上测试没问题但跨网络后负载只有前几个字节正确。原因以太网MTU是1500字节IP头加UDP头占28字节留给UDP payload最多1472字节。SRTP认证标签让原包多了10字节如果原RTP包是1470字节加密后变成1480字节超过1472在IP层被分片或者直接被网卡丢弃。解决应用层控制RTP包的上限留出SRTP标签的余量。一般我把最大RTP payload限制在1400字节这样加密后最多1410安全余量充足。如果走TURN或ICE中继还要把中继头开销也算进去。5.4 网络抖动后出现flood告警正常包被丢弃现象接收端在丢包恢复后陆续收到一批正常包但日志里全是重放检测告警媒体画面卡顿。原因重放窗口太小。window_size设成个位数时任何超过窗口的乱序包都会被当作旧包拒绝。互联网环境下RTP传输的抖动经常达到几十甚至上百毫秒对应的包数量很容易超过10个。解决窗口大小按实际抖动预算来算。假设视频30fps每帧20个包一帧的包时间跨度约33ms200ms抖动对应大约120个包。建议window_size设128到256对应约200至400ms的抖动容忍度。想省内存就把窗口压缩到业务能接受的最小值但别低于64。5.5 多会话场景下内存持续上涨现象长期运行后进程内存越来越大或者srtp_create返回session limit错误。原因每个新SSRC创建一个srtp会话但流结束时没有调用srtp_dealloc会话泄漏了。尤其在一些动态建链的场景里终端频繁进出会话表只增不减。解决给会话表加超时回收机制比如30秒没有活动包就dealloc。dealloc之后不要再调用unprotect否则是悬垂指针。习惯上我会在dealloc时顺手置NULL并加日志记录会话数方便观察泄漏趋势。6. 验证SRTP是否生效抓包确认、官方测试与性能压测6.1 用Wireshark抓包确认加密效果验证第一步是在本地回环抓包。启动demo在lo网卡上抓UDP流量过滤出目标端口。正常的SRTP包特征是RTP头保持明文PT、SSRC、seq可见但payload完全不可读包末尾多出一段认证标签。如果payload能看到字符或原始码流说明protect没有被真正调用。另外注意SRTCP包的PT字段通常是加密的Wireshark需要密钥配置才能解析出RR/SR等类型。6.2 跑官方测试向量和简单性能对比libsrtp自带的标准向量测试是一张安全网。编译时若enable了stdout可以直接跑测试程序它会验证RFC 3711里的加密结果确认当前编译版本里AES-CTR、AES-GCM和HMAC-SHA1的实现在算法层面没有偏差。性能压测可以做一个简单的循环固定1k字节的RTP包调用protect一百万次统计吞吐。经验参考是AES-CM在支持AES-NI的x86上约2到3GbpsAES-GCM约1.5Gbps起步ARM平台上差距更大。如果测出来只有几百Mbps先确认是否开了OpenSSL加速再确认CPU频率没有降。我的习惯是每改动一次密钥管理逻辑都要先跑一遍30字节固定密钥的回环demo再接入真实流量。这个习惯在三个项目里救过我每次都是业务信令一切正常、媒体就是黑屏的诡异情况。希望帮到你。本文还有配套的精品资源点击获取