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

使用eBPF Kprobe读取TCP包payload时iov_iter数据异常求解

如何在eBPF Kprobe中正确读取TCP收发数据包的payload?

问题描述

我尝试通过libbpf使用eBPF Kprobe挂载到tcp_sendmsg和tcp_recvmsg函数,解析TCP收发数据包的payload。根据认知,payload存储在第二个参数struct msghdr *msg的msg_iter(struct iov_iter*)指向的iov(struct iovec*)的iov_base中。但读取iovec的iov_length时结果为1,而msg_iter->count却是正确的payload长度;从iov_base读取数据时,数据出现损坏。推测数据需完全刷入iov_iter后才能读取,现咨询如何在Kprobe中正确读取TCP包payload?

复现代码(tcp_sendmsg的eBPF Kprobe)

#include"vmlinux.h"
#include<bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

const char fmt_info1[] = "segs: %d, offset: %d ,count:%d\n";
const char fmt_info2[] = "iov_length: %d, read_length: %d, data-readsuccess: %d\n";
const char fmt_notify_data[] = "iter_type: %d, Data: ";
const char fmt_data[] = "%02x ";
const char fmt_end[] = "\n";
unsigned char databuf[256];

SEC("kprobe/tcp_sendmsg")
int probe_tcp_sendmsg(struct pt_regs *ctx){
    struct msghdr *msg = (struct msghdr *)PT_REGS_PARM2(ctx);
    struct iov_iter *iter= &(msg->msg_iter);
    struct iovec *iter_data = (struct iovec *)&(iter->iov);
    int read_success=-1;
    long unsigned int iov_seg_count;//count of iovec's
    unsigned char iov_type;//0 is iter_iovec
    unsigned long iov_count;//total data length in iter
    unsigned long iov_length;//length of current iovec
    unsigned long iov_offset;//iovec's to skip in iter
    unsigned long read_length;//final length in the data buffer

    read_success=bpf_probe_read(&iov_type, sizeof(unsigned char), &(iter->iter_type));
    if(read_success!=0) {
        return 0;
    } 
    read_success=bpf_probe_read(&iov_seg_count, sizeof(long unsigned int), &(iter->nr_segs));
    if(read_success!=0) {
        return 0;
    } 
   read_success=bpf_probe_read(&iov_count, sizeof(unsigned long), &(iter->count));
    if(read_success!=0) {
        return 0;
    } 
    read_success=bpf_probe_read(&iov_offset, sizeof(unsigned long), &(iter->iov_offset));
    if(read_success!=0) {
        return 0;
    } 

    void* startaddress;
    read_success=bpf_probe_read(&startaddress, sizeof(void*), &(iter_data->iov_base));
    if(read_success!=0){
        return 0;
    }
    
    
    read_success=bpf_probe_read(&iov_length, sizeof(size_t), &(iter_data->iov_len));
    if(read_success!=0){
        return 0;
    }
    
    if(iov_length< 256){//change against iov_count
        read_length=iov_length;//change against iov_count
    }else{
        read_length=256;
    } 
    read_success=bpf_probe_read(databuf, read_length, startaddress);
    bpf_trace_printk(fmt_info1, sizeof(fmt_info1),iov_seg_count, iov_offset, iov_count);
    bpf_trace_printk(fmt_info2, sizeof(fmt_info2),iov_length, read_length, read_success);
    bpf_trace_printk(fmt_notify_data, sizeof(fmt_notify_data), iov_type);
    for(unsigned long i=0;i<read_length;++i){
        bpf_trace_printk(fmt_data,sizeof(fmt_data), databuf[i]);
    } 
    bpf_trace_printk(fmt_end, sizeof(fmt_end));

    return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

编译与加载步骤

  • 生成vmlinux.h:bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
  • 编译eBPF程序:clang -g -O3 -target bpf -D__TARGET_ARCH_x86 -c bpf_ip_tcp.c -o bpf_ip_tcp.o
  • 生成骨架文件:bpftool gen skeleton bpf_ip_tcp.o name bpf_ip_tcp > bpf_ip_tcp.h
  • 编译加载器:clang -Wall -Wextra -g3 main.c -o ebpf-example -lbpf

加载器代码

#include <sys/resource.h>
#include "bpf_ip_tcp.h"
int main(){
    struct rlimit rlim_new = {
        .rlim_cur   = RLIM_INFINITY,
        .rlim_max   = RLIM_INFINITY,
    };
    if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) {
        exit(1);
    }
    struct bpf_ip_tcp *skel = bpf_ip_tcp__open();
    bpf_ip_tcp__load(skel);
    bpf_ip_tcp__attach(skel); 
    while(1==1){}; 
    return 0;
}

测试结果(执行wget时cat /sys/kernel/tracing/trace_pipe输出)

...
wget-22674   [006] d...1 46410.754999: bpf_trace_printk: segs: 1, offset: 0 ,count:400
wget-22674   [006] d...1 46410.755059: bpf_trace_printk: iov_length: 1, read_length: 1, data-readsuccess: 0
wget-22674   [006] d...1 46410.755061: bpf_trace_printk: iter_type: 0, Data: 
wget-22674   [006] d...1 46410.755061: bpf_trace_printk: 40
...

期望结果:iov_length与count相等,或存在多个段。

系统环境

  • Ubuntu 22.04 LTS
  • 内核版本5.15.0

解决方案

1. 理解问题根源

挂载tcp_sendmsg的Kprobe时,内核可能还在处理iov_iter的拆分或初始化过程:

  • msg_iter->count是payload的总长度,但iter->iov仅代表当前正在处理的单个iovec片段,因此iov_len只会返回当前片段的长度,而非总长度。
  • 内核使用iov_iter迭代机制逐步处理数据时,直接读取iov_base可能拿到未完全准备好的内存,导致数据损坏。

2. 使用bpf_iter_read读取完整数据

内核5.14及以上版本支持bpf_iter_read helper,它能自动处理多段iovec的拼接,直接从iov_iter读取完整数据。修改后的代码示例:

#include"vmlinux.h"
#include<bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

const char fmt_info[] = "total length: %lu, read length: %lu\n";
const char fmt_data[] = "%02x ";
const char fmt_end[] = "\n";
unsigned char databuf[256];

SEC("kprobe/tcp_sendmsg")
int probe_tcp_sendmsg(struct pt_regs *ctx){
    struct msghdr *msg = (struct msghdr *)PT_REGS_PARM2(ctx);
    struct iov_iter *iter = &msg->msg_iter;
    unsigned long total_len = iter->count;
    unsigned long read_len = total_len > 256 ? 256 : total_len;
    long ret;

    // 使用bpf_iter_read读取iov_iter中的完整数据
    ret = bpf_iter_read(iter, databuf, read_len);
    if (ret <= 0) {
        bpf_trace_printk("read failed: %ld\n", ret);
        return 0;
    }

    bpf_trace_printk(fmt_info, sizeof(fmt_info), total_len, ret);
    for (unsigned long i = 0; i < ret; ++i) {
        bpf_trace_printk(fmt_data, sizeof(fmt_data), databuf[i]);
    }
    bpf_trace_printk(fmt_end, sizeof(fmt_end));

    return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

注意:bpf_iter_read会修改iov_iter的偏移量,若需保留原迭代器状态,建议先通过bpf_probe_read拷贝一份iov_iter结构(需注意结构大小与内核版本兼容性),再对拷贝的结构进行读取操作。

3. 低内核版本备选方案:挂载Kretprobe

如果内核版本低于5.14不支持bpf_iter_read,可以改为挂载tcp_sendmsg的Kretprobe。此时内核已处理完数据,iov_iter状态更稳定,但需注意:payload可能已被发送,部分用户空间内存可能已被释放,读取前需确认内存有效性。


内容的提问来源于stack exchange,提问作者Nicolai Schmitt

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 20:59:50