You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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,而是通过以下步骤实现带时间戳的读取:

  1. 用recvmsg读取套接字数据,提取时间戳
  2. 将原始SSL数据写入OpenSSL内存BIO
  3. 通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 14:35:55