147、RDMA与TLS(传输层安全):加密RDMA通信 好的我们继续。这是专栏的第147篇。今天聊一个让不少RDMA老兵都头疼的话题TLS加密。别急着划走我知道很多人第一反应是“RDMA要的是极低延迟加TLS那不是自废武功吗”——没错两年前我也是这么想的直到我在一个金融交易系统的POC现场被客户怼得哑口无言“你们RDMA裸数据在InfiniBand链路上跑万一被物理层抓包了呢合规过不去。”那一次我不得不把TLS和RDMA硬凑到一起踩了无数坑才有了今天这篇笔记。从一次“抓包现形记”说起那是一个做极速行情分发的小型集群用的Mellanox ConnectX-5RoCE v2协议。开发阶段一切完美延迟1.2微秒吞吐量接近线速。结果安全审计一上来直接亮红灯所有通过RoCE v2传输的行情数据在交换机端口镜像抓包后Wireshark里明文可见——连TCP payload都不用解析直接看到Order Book的增量数据。客户安全官当场要求要么加密要么项目暂停。我当时的第一反应是上IPsec。但IPsec在RoCE场景下有个致命问题——它工作在IP层而RoCE的传输层是InfiniBand的RC可靠连接服务IPsec的加密会破坏RoCE的硬件卸载能力导致CPU飙升延迟直接翻倍到3微秒以上。更坑的是某些网卡固件对IPsec offload的支持只针对TCP/UDP对RoCE的QP队列对报文根本不认。于是我被迫转向TLS。但RDMA的通信模型和TCP完全不同它没有内核socket没有send/recv系统调用数据直接在用户态通过WQE工作队列元素提交给网卡硬件。传统的OpenSSL库根本没法直接挂上去。怎么办只能走“应用层加密”这条路——在数据进入RDMA发送缓冲区之前先用TLS加密接收端再解密。听起来简单但实现起来全是坑。别把TLS握手塞进RDMA数据路径第一个坑TLS握手。有人会想既然RDMA是可靠连接那我在建立QP之后直接在QP上跑TLS握手不就行了别这样写。RDMA的QP本质上是硬件状态机它不关心你传的是TLS record还是原始二进制流。如果你把TLS握手报文比如ClientHello当作普通RDMA消息发送接收端需要自己解析并响应——这意味着你必须在用户态实现一个完整的TLS状态机而且这个状态机不能阻塞RDMA的发送/接收轮询。我见过一个团队这么干结果握手阶段就超时因为RDMA的WR工作请求提交和完成轮询是异步的TLS握手需要同步的请求-响应交互两者天然冲突。我的做法是TLS握手走传统的TCP通道只在数据面用RDMA。具体来说在应用启动时先建立一个TCP连接或者用socket在这个TCP连接上完成TLS握手交换对称密钥。握手完成后关闭TCP连接只保留协商好的密钥材料比如AES-GCM的key和IV。然后把这个密钥通过ibv_post_send的“内联数据”或者直接写入共享内存传递给RDMA的发送端。接收端同理。这样RDMA数据路径上只做对称加密/解密不做握手。加密粒度别对整个消息加密对“载荷”加密第二个坑加密粒度。RDMA的典型用法是发送一个大的连续缓冲区比如一个4KB的行情快照。如果你对整个4KB加密接收端必须解密后才能解析。但问题在于RDMA的接收端通常需要知道消息边界——比如通过“立即数”或者“分段”来区分多个消息。如果你加密了整个缓冲区接收端在解密之前无法读取任何头部信息也就无法知道这个缓冲区里包含几个消息。这里踩过坑我最初把整个RDMA消息包括自定义的头部和载荷一起加密结果接收端解密后才发现头部被破坏了——因为头部里包含的序列号、长度字段在加密后变成了乱码接收端无法校验。正确的做法是只加密载荷头部保持明文。头部里可以放一个“加密标志位”和一个“IV索引”告诉接收端这个载荷是用哪个IV加密的。接收端先读取头部根据IV索引找到对应的密钥和IV再解密载荷。这样头部本身不敏感即使被嗅探也泄露不了业务数据。性能优化别让加密成为瓶颈第三个坑性能。AES-GCM是TLS 1.3推荐的加密套件支持硬件加速。但问题在于RDMA的发送端通常用CPU把数据拷贝到注册的内存区域MR然后网卡DMA读取。如果你在拷贝之前做加密加密操作本身会占用CPU周期。在100Gbps的链路上每微秒要处理约12.5KB数据如果加密速度跟不上就会形成瓶颈。我的优化思路是利用CPU的AES-NI指令集并且把加密和内存拷贝合并。具体来说在将应用数据拷贝到RDMA发送缓冲区时同时进行AES-GCM加密。这需要手动实现一个“加密拷贝”函数而不是先拷贝再加密。另外对于小消息比如小于256字节可以用内联数据inline data直接嵌入WQE避免额外的DMA操作。但注意内联数据本身不加密你需要在内联之前就完成加密。我写过一个宏专门处理这种场景// 内联发送时先加密到栈上临时缓冲区再提交WQE// 别直接加密原始数据否则原始数据会被修改#defineRDMA_SEND_INLINE_ENCRYPT(send_wr,data,len,key,iv)do{\uint8_ttmp_buf[256];\aes_gcm_encrypt(data,len,key,iv,tmp_buf);\send_wr-sg_list-addr(uint64_t)tmp_buf;\send_wr-sg_list-lengthlen;\send_wr-send_flags|IBV_SEND_INLINE;\}while(0)注意这个宏假设len不超过256字节且tmp_buf在WQE提交完成前必须有效。实际工程中你需要确保WQE完成轮询后才释放tmp_buf否则网卡可能读到栈上被覆盖的数据。这里踩过坑我忘了加内存屏障结果在弱内存序的ARM服务器上网卡读到了加密前的数据。密钥轮换别在QP上做第四个坑密钥轮换。TLS标准要求定期更换密钥防止长期使用同一密钥被破解。但在RDMA场景下密钥轮换不能通过QP本身来做——因为QP的通信是连续的你没法在中间插入一个“密钥更新消息”而不破坏数据流。我的做法是在应用层维护一个密钥版本号每个RDMA消息的头部都携带这个版本号。接收端根据版本号查找对应的密钥。当需要轮换时发送端先停止发送新消息通过一个标志位等待所有在途消息被确认通过RDMA的完成事件然后通过带外通道比如共享内存或另一个QP下发新密钥。接收端确认收到新密钥后发送端再恢复发送。这个过程中QP本身不需要重建只是应用层的密钥表更新了。个人经验性建议如果你现在要做一个需要加密的RDMA系统我的建议是先评估是否真的需要TLS级别的加密。很多场景下InfiniBand的物理层隔离比如分区密钥、子网管理器访问控制已经足够加上TLS反而引入复杂度和延迟。但如果合规要求必须端到端加密比如金融、医疗那么别试图在RDMA上跑完整的TLS协议栈——那会把你拖入状态机的泥潭。用“带外握手带内对称加密”的方案把加密逻辑放在应用层但一定要用硬件加速的AES-GCM并且注意内存拷贝和加密的合并。另外测试时别只测小包延迟。大包吞吐量下加密的CPU开销会暴露无遗。我见过一个系统64字节消息延迟只增加0.5微秒但4KB消息吞吐量直接腰斩——因为加密成了瓶颈。最后别忘了在接收端做解密后的完整性校验。RDMA的CRC只保证链路层不丢包不保证应用层数据没被篡改。AES-GCM的认证标签authentication tag就是干这个的别为了省那16字节而省略它——否则你加密了个寂寞。好了这篇就到这里。下一期我们聊聊RDMA与DPDK的“爱恨情仇”——如何在同一个网卡上同时跑RDMA和DPDK而不让它们打架。