
简介一份安全实时传输协议SRTP开源库资源包用于在实时传输中增加加密与完整性保护面向需要实现VoIP、视频会议等安全通信场景的C开发者。压缩包共有168个文件大小约547KB主体为45个C源码文件与34个头文件覆盖加密算法、SRTP协议处理、内核调度等核心模块另有4个配置、2个工程文件及解决方案、构建脚本和说明文档其中readme与pdf可帮助理解协议设计configure与install-sh便于配置安装整体可在Visual Studio 2005VC7环境中直接编译使用。目前已有196人学习下载。借助这份资料开发者无需从头搭建编译环境可快速理解密钥管理、数据包加解密流程并参考现成工程结构将SRTP安全能力集成到自有项目中有效缩短协议封装与调试周期是学习实时安全传输实现的便捷素材也有助于掌握协议栈的安全设计思路。1. srtp 开源库实时音视频加密的地基WebRTC 里绕不开的那层做音视频通话、WebRTC 网关、SIP 软电话的工程师迟早会在抓包里看到明文 RTP——只要中间有人能抓包通话内容就能被直接还原。SRTPSecure Real-time Transport ProtocolRFC 3711给 RTP 套一层认证加密负载加密、整包完整性校验同时保留 RTP 头的可路由性。libsrtp 这套 srtp 开源库是生产环境最常见的 SRTP 实现之一大量 WebRTC 和 SIP 栈直接依赖它。它解决的问题很具体不加额外握手延迟在收发每个媒体包时完成加解密并容忍丢包乱序。适合谁已经能收发 RTP 但找不到加密入口的人以及想在自研音视频模块里接入 SRTP 的 C/C 工程师。下面用一个最小 demo把协议、参数和坑一次走通。2. SRTP 协议与 libsrtp 架构加密到底发生在 RTP 的哪一层2.1 SRTP 不是另一个 TLS认证加密的对象是 RTP 负载很多人第一次接触 SRTP 会拿它和 TLS 类比这个类比放到包结构上就翻车。TLS 跑在可靠字节流上有握手、有会话状态RTP 跑在 UDP 上包会丢、会乱、会重复。SRTP 的核心设计是“每个 RTP 包独立加解密”发送方拿到一个明文 RTP 包原地把它变成 SRTP 包接收方拿到 SRTP 包原地还原成 RTP 包。包与包之间没有 TLS 那种记录层依赖这是它能用在实时链路上的前提。加密的范围也很有讲究。RTP 头版本、PT、seq、timestamp、SSRC保持明文负载全部加密认证却覆盖整个包。为什么头要明文因为路由、组播、QoS 标记和丢包统计都要读头为什么认证要覆盖整个包因为改 timestamp 和 seq 同样能破坏通话。libsrtp 把这一进一出封装成两个函数发送侧srtp_protect()接收侧srtp_unprotect()两者都在原缓冲区上操作不需要申请新内存。另一个常见误用以为设置了加密RTP 扩展头比如一帧里的 video orientation、transport-cc feedback也会自动被保护。事实是 RTP 扩展头默认明文只有把 policy 里的encrypt_rtp_header_extensions置 1 才会连同扩展头一起加密对端也必须用同样的设置否则解出来就是错位。设置这个字段前先确认对端和中间设备对扩展头的读取方式。2.2 libsrtp 的核心对象与源码树srtp_t、policy、SSRC 流在 libsrtp 里srtp_t是会话上下文一个 context 就是一套“用哪把 key、用什么算法、保护哪些流”的配置集合。创建时你要填一个srtp_policy_t里面最关键的是加密套件、master key、SSRC 范围和重放窗口。之后每次收发包把同一个srtp_t传给 protect/unprotect 就行应用层不用关心逐包的会话密钥怎么派生。经典 libsrtp 源码树的结构很固定include 目录放srtp.hsrc 下面分 crypto 和 srtp 两个实现目录test 目录里有大量自测程序其实是最好的使用范例。你拿到srtp.rar解压后先ls看根目录是不是这套结构确认头文件在include/srtp.h还是srtp/srtp.h不同打包方式路径会有差异以实际为准。源码树的 API 分两个时代。老版本里 crypto policy 的函数不带srtp_前缀比如crypto_policy_set_aes_cm_128_hmac_sha1_80libsrtp 2.x 改成了srtp_crypto_policy_set_...前缀。编译报“未声明”时先看头文件里实际函数名这种命名差异是新手编译失败的最常见原因不是代码写错。2.3 ROC 与 MKI为什么实时协议要专门处理序号翻转RTP 的 sequence number 只有 16 位。视频 50 帧每秒65536 个包大概 22 分钟就翻转一轮一个长通话翻转几十次很正常。SRTP 如果直接用这个 16 位 seq 参与加密翻转后加密输出的前几个字节就会和上次循环重复等价于相同的明文得到相同的密文前缀这是安全上的大忌。所以 RFC 3711 引入 32 位 ROCRollover Counter和 seq 拼成 48 位包索引加密与认证都基于这个索引。libsrtp 在 context 内部自动维护每条流的 ROC发送侧靠观察包里的 seq 翻转来进位接收侧靠重放窗口和包序推算。这意味着 ROC 是上下文里的内存状态——进程重启、context 重建ROC 就从 0 重新算。这就是后面避坑章里“重启后全部解不开”的根因理解这一点能省很多排错时间。MKIMaster Key Identifier是给密钥轮换用的同一对端可以同时持有几把主密钥发方在包里带一个 MKI 告诉收方“这包用的是哪把”。大多数 WebRTC 和 VoIP 场景用 DTLS-SRTP 完成一次密钥协商后并不轮换MKI 保持默认 0 即可项目初期不用花时间研究它。3. 把 libsrtp 跑起来从解压 srtp.rar 到一次 RTP 加解密3.1 解压与构建先确认拿到的是不是 libsrtp 源码树# 拿到的是 rar 压缩包时用 unrar 或 7z 解压二选一 unrar x srtp.rar # 7z x srtp.rar # 解压后先看根目录确认是不是 libsrtp 标准源码树 cd srtp ls -l # 标准构建方式configure make ./configure --prefix/usr/local --disable-debug make -j$(nproc) sudo make install解压后如果根目录有configure或autogen.sh以及 include、src、test 这几个目录基本就是 libsrtp 源码树如果解出来只是一堆散落的 .c/.h 文件就先用grep -r srtp_init *.h定位头文件再往下走。--disable-debug去掉断言和调试符号适合直接编进生产模块想看底层加密实现细节时去掉这个参数重新编一份即可。新版源码也支持 CMakecmake -B build cmake --build build和 configure 路线二选一不必都跑。编完自己模块时用pkg-config --cflags --libs libsrtp拿编译参数比手写-I路径靠谱。3.2 发送端30 字节静态密钥做一次原地加密#include srtp/srtp.h #include stdio.h #include string.h #include stdint.h /* 构造一个最小 RTP 头12 字节头 8 字节负载 20 字节 */ static void build_rtp(uint8_t *pkt, uint16_t seq, uint32_t ssrc) { pkt[0] 0x80; /* v2, 无 padding, 无扩展 */ pkt[1] 96; /* 动态 payload type */ pkt[2] (seq 8) 0xff; pkt[3] seq 0xff; pkt[4] pkt[5] pkt[6] pkt[7] 0; /* timestamp 先填 0 */ pkt[8] (ssrc 24) 0xff; pkt[9] (ssrc 16) 0xff; pkt[10] (ssrc 8) 0xff; pkt[11] ssrc 0xff; } int main(void) { uint8_t buf[64]; int len 20; srtp_t ctx; srtp_policy_t policy; /* 进程内只调用一次的全局初始化 */ srtp_init(); memset(policy, 0, sizeof(policy)); srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(policy.rtp); srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(policy.rtcp); policy.ssrc.type ssrc_specific; policy.ssrc.value 0x01020304; /* 只保护这一个 SSRC */ policy.key (uint8_t *)0123456789abcdef0123456789abcd; /* 30 字节 */ policy.next_seq 0x1234; /* 和首包 seq 保持一致 */ policy.allow_repeat_tx 0; policy.window_size 128; policy.rtp.sec_serv sec_serv_conf_and_auth; policy.rtcp.sec_serv sec_serv_conf_and_auth; if (srtp_create(ctx, policy) ! srtp_err_status_ok) { printf(srtp_create failed\n); return -1; } build_rtp(buf, 0x1234, 0x01020304); memset(buf 12, 0xaa, 8); /* 负载 8 字节全填 AA */ /* 原地加密入口 len20出口 len30多 10 字节认证标签 */ srtp_err_status_t rc srtp_protect(ctx, buf, len); if (rc ! srtp_err_status_ok) { printf(protect failed: %d\n, rc); return -1; } printf(protect ok, len%d\n, len); return 0; }这段代码是 SRTP 集成的最小路径。因为 AES-CM 是流式加密负载长度不变len 从 20 变成 30 多出来的 10 字节是 HMAC-SHA1 截断到 80 bit 的认证标签追加在包尾。policy.key的 30 字节不是随便拼的前 16 字节是 128 位 master key后 14 字节是 112 位 master saltRFC 3711 的会话密钥派生全靠这个组合。写静态字符串做演示可以生产环境绝不能这么干。next_seq建议设成首包的 seqlibsrtp 靠它推算初始 ROCdemo 里首包 seq 是 0x1234两者对不上时加密可能成功但接收端推包索引会错位。加密成功后把 buf 交给 UDP socket 发给对端即可头部的 seq、SSRC 原样可见但负载已经不可读。3.3 接收端unprotect 返回值和负载校验/* 接收端和发送端放在同一个文件里便于回环测试 */ int main(void) { uint8_t buf[64]; int len; srtp_t rcv; srtp_policy_t policy; srtp_init(); memset(policy, 0, sizeof(policy)); srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(policy.rtp); srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(policy.rtcp); policy.ssrc.type ssrc_any_inbound; /* 接收侧可不限定 SSRC */ policy.key (uint8_t *)0123456789abcdef0123456789abcd; policy.window_size 128; policy.rtp.sec_serv sec_serv_conf_and_auth; policy.rtcp.sec_serv sec_serv_conf_and_auth; if (srtp_create(rcv, policy) ! srtp_err_status_ok) return -1; /* 假设 buf 里是 3.2 节 protect 之后的那 30 字节数据 */ len 30; srtp_err_status_t rc srtp_unprotect(rcv, buf, len); if (rc ! srtp_err_status_ok) { printf(unprotect failed, code%d\n, rc); return -1; } /* 解密成功后 len 应回到 20buf[12..19] 应全是 0xaa */ for (int i 12; i 20; i) { if (buf[i] ! 0xaa) { printf(payload mismatch at %d\n, i); return -1; } } printf(unprotect ok, len%d, payload verified\n, len); return 0; }接收侧不需要next_seq包序号从 RTP 头里读libsrtp 收到包后先算包索引、查重放窗口、验证 HMAC最后才解密负载。unprotect 失败时返回的错误码是最直观的排错入口比较常见的是srtp_err_status_replay_old被重放窗口判定为旧包和srtp_err_status_auth_failkey 或套件不一致导致认证失败。成功标准很简单调用返回 oklen 变回 20负载恢复 AA。这个回环测试是所有 SRTP 集成的第一关过不了这一关就谈不上接 WebRTC。4. 加密套件与策略参数从默认值到生产级配置4.1 三种 crypto suite 怎么选80、32 还是 GCM套件DTLS-SRTP profile 名加密算法认证算法认证标签长度典型场景SRTP_AES128_CM_HMAC_SHA1_80AES-128 CTRHMAC-SHA1 截断10 字节WebRTC 默认、兼容性最好SRTP_AES128_CM_HMAC_SHA1_32AES-128 CTRHMAC-SHA1 截断4 字节老移动端省带宽SRTP_AEAD_AES_128_GCMAES-128 GCMAEADGCM 自带16 字节新自研链路推荐80 后缀表示认证标签截断到 80 bit也就是 10 字节32 后缀是 4 字节安全余量很低没有带宽压力不要选。GCM 是 AEAD 结构加密同时完成认证标签 16 字节AES-NI 指令集下性能通常比 CMSHA1 的组合更好。选型原则要和现网老终端互通只能跟对端协商的 profile 走优先 80两端代码都是你自己的直接上 GCM。libsrtp 里 80 和 32 分别对应srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80和..._32GCM 对应srtp_crypto_policy_set_aes_gcm_128_16_auth之类的接口具体函数名以本地头文件为准搜aes_gcm就能定位。4.2 srtp_policy_t 关键字段逐个拆解srtp_policy_t policy; memset(policy, 0, sizeof(policy)); policy.ssrc.type ssrc_specific; /* ssrc_specific / ssrc_any_inbound / ssrc_any_outbound */ policy.ssrc.value 0x01020304; /* type 为 specific 时生效和包头 SSRC 必须一致 */ policy.key master_key; /* 30 字节16 key 14 salt */ policy.next_seq 0x1234; /* 发送端首包 seq接收端不用填 */ policy.window_size 128; /* 接收端重放滑动窗口大小 */ policy.allow_repeat_tx 0; /* 发送端是否允许重复 seq默认 0 */ policy.encrypt_rtp_header_extensions 0; /* 是否连 RTP 扩展头一起加密 */ policy.rtp.sec_serv sec_serv_conf_and_auth; /* RTP 加密 认证 */ policy.rtcp.sec_serv sec_serv_conf_and_auth; /* RTCP 单独一套别漏 */ssrc.type用ssrc_specific时context 只处理指定 SSRC 的流初学阶段最推荐接收侧图省事可以用ssrc_any_inboundlibsrtp 会按 SSRC 动态拆分内部 stream代价是排错时多一层“这条流到底挂在哪”的认知负担。window_size只在接收端有意义它决定能容忍多大乱序默认 128 足够绝大多数链路抖动极大时调到 256但别无限放大窗口越大越容易漏掉重放攻击。allow_repeat_tx在 RTP 场景默认 0 就好个别重传扩展需要发重复 seq 的包时再打开。encrypt_rtp_header_extensions开启后对端必须同样支持否则扩展头解出来是乱码联调时优先关掉先确认 payload 通了再说。4.3 生产参数建议静态密钥只配出现在测试环境生产环境的第一条铁律密钥来源必须走 DTLS-SRTP 握手WebRTC 标准做法或者至少通过信令安全通道协商不允许把 30 字节静态 key 写死在代码和配置文件里。第二条每次会话重建都要换新 key。这不仅是安全问题也是状态问题——旧 key 对应旧的 ROC 和重放窗口重启后的 context 和旧 key 根本无法对齐硬用旧 key 只会得到一堆 auth_fail 日志。实际部署时我一般这样定参数RTP 和 RTCP 都用同一套 profilertp 和 rtcp 的sec_serv都设conf_and_auth重放窗口 128如果对端是移动网络且抖动明显窗口升到 256。认证标签保持 80 bit 不动别为了省那 6 字节在带宽上冒险。RTCP 的保护经常被漏掉创建 context 时把policy.rtcp一并配好接收侧统一走srtp_protect_rtcp / srtp_unprotect_rtcp。5. 常见问题与排查五类翻车现场从 ROC 到重放窗口下面五条是接入 libsrtp 最常踩的坑每条按“现象 → 原因 → 解决”写排错时对号入座。5.1 unprotect 返回 replay_old新包被判成旧包现象收到的包明明是新包srtp_unprotect一直返回srtp_err_status_replay_old。 原因重放窗口的推进依赖每条流的最高包索引。发送端 context 重建后从低 seq 重新发包或者接收端window_size太小、乱序距离超过窗口长度都会被判定为旧包。 解决先确认发送端没有因为重传机制重复发旧包再把window_size从 128 调到 256 做对照测试。如果两边都重启过问题大概率不在窗口而是 ROC 状态丢失走 5.3 的重协商流程。5.2 auth_fail 且双方 key 看起来一模一样字节没对齐现象两端都用同一个字符串当 key解密就是srtp_err_status_auth_fail堪称玄学。 原因最常见的是 key 组织错位。SRTP 的 master key 是“16 字节 AES 密钥 14 字节 salt”拼在一起有人只复制了前 16 字节把后面 14 字节当成无用的 padding第二种常见原因是两端一个配了 80-bit tag一个配了 32-bit tag认证长度不一致HMAC 截断位置对不上。 解决把两端的 key 逐字节打十六进制打印出来比对别只看字符串前缀相同确认两端调用的 crypto policy 函数名完全一致80 和 32 的函数千万不要混用。5.3 进程重启后所有包解不开ROC 状态不能指望后悔药现象服务端升级重启客户端还在按老会话发包重启后 unprotect 全部失败密钥明明没换。 原因重启后 context 里的 ROC 归零包索引默认从 0 重新算对端还在用重启前的 48 位索引加密数据两边对不上。静态 key 场景更惨因为没有握手机制来重新对齐索引状态。 解决生产环境把 SRTP 会话和信令会话绑在一起重连时重新做 DTLS-SRTP 握手换新 key、建新 context。不要试图手动恢复 ROCSRTP 协议里没有“续传”的概念。5.4 RTCP 只保护了一半policy 配了函数却没用对现象RTP 加解密正常RTCP 要么解不开、要么抓包一看是明文。 原因libsrtp 的 RTP 和 RTCP 是两套独立接口保护 RTCP 要调用srtp_protect_rtcp / srtp_unprotect_rtcp。policy.rtcp配了conf_and_auth但发送端忘了调对应的 rtcp 函数明文 RTCP 到了对端unprotect_rtcp手里必然失败。 解决要么 RTP/RTCP 双路都保护发送侧统一走srtp_protect_rtcp要么明确把policy.rtcp.sec_serv设成sec_serv_none别半吊子。只用 RTP 保护的配置在抓包时会发现 RTCP 里的关键信息全裸奔。5.5 多路流串台SSRC 对不上就互相踩现象一个 context 处理两个 SSRCA 流正常B 流随机 auth_fail。 原因创建 context 时policy.ssrc.type用了ssrc_specificvalue 却填的是 A 的 SSRCB 的包进来后被当成 A 流处理重放窗口和 ROC 全部错位。反过来用ssrc_any_inbound时libsrtp 会按 SSRC 自动拆 stream但前提是各流使用相同的 key 和套件。 解决初学阶段固定一个 SSRC 做回环网关场景如果每个远端的 key 不同就给每个 SSRC 单独建 context。调试时先把多路合一改成单路确认单路全绿再放量。6. 进阶验证抓包确认负载加密再把 RTCP 双路配齐6.1 用 tshark 验证负载不再是明文# 抓本机回环 5004 端口上的媒体包 tshark -i lo -f udp port 5004 -w srtp_test.pcap # 回放时只看 rtp.payload 字段 tshark -r srtp_test.pcap -Y rtp -T fields -e rtp.payload | head -5如果没加密负载是 G.711 或 VP8 时会有明显的编码特征比如 G.711 的波形字节直接可读、VP8 的关键帧头有固定 pattern加密后rtp.payload是一串不可读的十六进制而且每个包比明文多 10 字节80-bit tag。Wireshark 也支持在协议设置里填 SRTP master key 做解密验证注意要填完整的 30 字节 key salt只填前 16 字节解不开——这又是一次“密钥字节数”的教训和 5.2 的翻车是同源的。6.2 把 RTCP 双路配齐再上生产/* 发送侧RTCP 包在发送前原地加密 */ srtp_protect_rtcp(ctx, rtcp_buf, rtcp_len); /* 接收侧对应调用 RTCP 变体解密 */ srtp_unprotect_rtcp(rcv, rtcp_buf, rtcp_len);RTCP 的包索引和 RTP 是分开的所以绝对不能把普通 protect 函数套到 RTCP 包上对端也必须用对应的 rtcp 变体。配齐后抓包看到的 RTCP 只有裸的头部复合包 payload 不可读sender report 里的 NTP 时间戳、丢包率这些元数据也不会泄露给旁路抓包的人。我现在的习惯是任何 SRTP 集成都从 30 字节静态 key 的回环 demo 开始protect/unprotect 跑绿了再去接 DTLS-SRTP 的密钥流。别小看这一步省掉的排错时间足够写十遍 demo。以前我直接把这个库塞进一个现成网关结果联调时全是 auth_fail日志只给错误码根本分不清是 salt 少拷贝了 14 字节还是 tag 长度不一致——后来老老实实回退到最小回环才定位到是 RTCP 那路 policy 忘了配。做实时媒体这一行能用最小复现解决的问题就别在真机链路上猜。希望帮到你。本文还有配套的精品资源点击获取