如何在OpenSSL中获取网卡/Linux内核时间戳?
如何在OpenSSL SSL连接中获取Linux网络时间戳
核心思路
OpenSSL的SSL对象依赖底层套接字的BIO组件,我们可以绕过直接调用SSL_read的方式,自行通过recvmsg读取套接字数据并提取时间戳,再将原始SSL数据喂入OpenSSL的内存BIO中,让SSL层完成解密流程。这种方案无需修改OpenSSL源码,就能同时实现SSL数据读取和时间戳获取。
具体实现步骤
1. 配置套接字时间戳选项
首先要为底层套接字开启接收时间戳的功能,配置方式与普通recvmsg场景一致:
int enable = 1; // 可组合多种时间戳类型,比如硬件+软件时间戳,根据网卡支持情况调整 int ts_flags = SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_SOFTWARE | SOF_TIMESTAMPING_RAW_HARDWARE; setsockopt(sockfd, SOL_SOCKET, SO_TIMESTAMPING, &ts_flags, sizeof(ts_flags));
2. 自定义SSL读取流程
不再直接调用SSL_read,而是通过以下步骤实现带时间戳的读取:
- 用
recvmsg读取套接字数据,提取时间戳 - 将原始SSL数据写入OpenSSL内存BIO
- 通过
SSL_read从BIO中读取解密后的应用层数据
代码示例片段
#include <openssl/ssl.h> #include <sys/socket.h> #include <time.h> #include <errno.h> // 关联到SSL对象的内存BIO,用于缓存原始SSL数据 static BIO *ssl_mem_bio = NULL; // 初始化:创建内存BIO并绑定到SSL对象 void ssl_ts_init(SSL *ssl) { ssl_mem_bio = BIO_new(BIO_s_mem()); // 将内存BIO设置为SSL的读写BIO SSL_set_bio(ssl, ssl_mem_bio, ssl_mem_bio); } // 带时间戳的SSL读取函数 ssize_t ssl_read_with_ts(SSL *ssl, int sockfd, void *buf, size_t buf_len, struct timespec *out_ts) { struct msghdr msg = {0}; struct iovec iov[1]; char control_buf[CMSG_SPACE(sizeof(struct timespec) * 3)]; // 内核最多返回3个时间戳 struct cmsghdr *cmsg; char raw_ssl_buf[4096]; // 缓存原始SSL帧,大小可根据MTU调整 ssize_t recv_len; int ssl_ret; // 初始化msghdr结构 iov[0].iov_base = raw_ssl_buf; iov[0].iov_len = sizeof(raw_ssl_buf); msg.msg_iov = iov; msg.msg_iovlen = 1; msg.msg_control = control_buf; msg.msg_controllen = sizeof(control_buf); // 用recvmsg读取数据和时间戳控制信息 recv_len = recvmsg(sockfd, &msg, 0); if (recv_len <= 0) { return recv_len; } // 提取时间戳:优先取硬件时间戳,无则回退到软件时间戳 *out_ts = (struct timespec){0}; for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SO_TIMESTAMPING) { struct timespec *stamps = (struct timespec *)CMSG_DATA(cmsg); // stamps[0]:软件接收时间;stamps[1]:硬件捕获时间;stamps[2]:软件转发时间 if (stamps[1].tv_sec != 0) { *out_ts = stamps[1]; } else { *out_ts = stamps[0]; } break; } } // 将原始SSL数据写入内存BIO BIO_write(ssl_mem_bio, raw_ssl_buf, recv_len); // 从SSL对象读取解密后的应用层数据 ssl_ret = SSL_read(ssl, buf, buf_len); if (ssl_ret <= 0) { // 处理SSL层错误,比如需要更多数据时继续读取 int ssl_err = SSL_get_error(ssl, ssl_ret); if (ssl_err == SSL_ERROR_WANT_READ) { errno = EAGAIN; return -1; } } return ssl_ret; } // 清理资源 void ssl_ts_cleanup() { if (ssl_mem_bio) { BIO_free(ssl_mem_bio); ssl_mem_bio = NULL; } }
3. 关键注意事项
- BIO生命周期管理:内存BIO需与SSL对象生命周期绑定,在SSL对象销毁前要调用
BIO_free释放资源。 - 非阻塞模式适配:如果套接字是异步非阻塞的,
recvmsg和SSL_read都可能返回EAGAIN,需要在循环中重试。 - 时间戳有效性验证:部分老旧网卡不支持硬件时间戳,需提前通过
ethtool等工具验证网卡能力。 - 数据完整性:确保每次
recvmsg读取的原始SSL数据全部写入内存BIO,避免丢包导致SSL解密失败。
进阶优化方案
如果需要更优雅的封装,可以实现自定义套接字BIO:在BIO的读取回调中集成recvmsg获取时间戳,并将时间戳存储在自定义上下文结构中。上层代码依然可以直接调用SSL_read,同时通过上下文接口获取对应数据包的时间戳。这种方式需要熟悉OpenSSL的BIO扩展机制,适合长期维护的项目。
内容的提问来源于stack exchange,提问作者Kevin Meier
相关产品推荐
相关产品推荐

