在 dev_queue_xmit 中使用 ebpf 读取 sk_buff 会产生有问题的数据

问题描述

我正在尝试通过将 kprobe 插入 __dev_queue_xmit() 来在发送之前捕获本地主机上的传出以太网帧。 但是,我从 sk_buff 结构中提取的字节与随后捕获的数据包不匹配。

到目前为止,我只针对线性 skb 尝试过它,因为我已经在那里得到了意想不到的结果。 例如,我的 kprobe调用 __dev_queue_xmit() 时报告了以下信息:

COMM            PID      TGID     LEN        DATALEN
chronyd         1058     1058     90         0
3431c4b06a8b3c7c3f2023bd08006500d0a57f040f7f0000000000000000000000000000000000006018d11a0f7f00000100000000000000000000000000000060a67f040f7f0000000000000000000000000000000000004001

COMM调用函数的进程名, PID调用线程的 id 和 TGID线程组 id。 LEN(skb->len - skb->data_len) 的值,DATA_LENskb->data_len

接下来,程序复制了 LEN(在本例中为 90)字节,从 skb->data 开始。 由于 DATALEN 为零,因此这是一个线性 skb。因此,这些字节应该包含即将发送的帧,不是吗?

好吧,Wireshark 随后记录了这一帧:

0000   34 31 c4 b0 6a 8b 3c 7c 3f 20 23 bd 08 00 45 00
0010   00 4c 83 93 40 00 40 11 d1 a2 c0 a8 b2 18 c0 a8
0020   b2 01 c8 07 00 7b 00 38 e5 b4 23 00 06 20 00 00
0030   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0040   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0050   00 00 38 bc 17 13 12 4a 4c c0

形成以太网标头的前 14 个字节按预期完美匹配。其他一切都根本不匹配。 现在的问题是:为什么字节不匹配

(是的,我确定来自 Wireshark 的帧确实是由调用 __dev_queue_xmit() 引起的。这是因为当时只有使用网络的后台程序在运行,因此传出流量为相当小。此外,正如预期的那样,捕获的帧包含 90 个字节。此外,该帧包含一个 NTP 有效负载,这正是您对 chronyd 所期望的。)

我的内核版本是 5.12.6-200.fc33.x86_64。

如果你想自己尝试一下或者仔细看看我的程序,这里是:

from bcc import BPF

from ctypes import cast,POINTER,c_char

prog = """
#include <linux/sched.h>
#include <linux/skbuff.h>

struct xmit_event {
    u64 ts;
    u32 pid;
    u32 tgid;
    u32 len;
    u32 datalen;
    u32 packet_buf_ptr;
    char comm[TASK_COMM_LEN];
    
    u64 head;
    u64 data;
    u64 tail;
    u64 end;
};
BPF_PERF_OUTPUT(xmits);

#define PACKET_BUF_SIZE 32768
# define PACKET_BUFS_PER_cpu 15

struct packet_buf {
    char data[PACKET_BUF_SIZE];
};
BPF_PERcpu_ARRAY(packet_buf,struct packet_buf,PACKET_BUFS_PER_cpu);
BPF_PERcpu_ARRAY(packet_buf_head,u32,1);

int kprobe____dev_queue_xmit(struct pt_regs *ctx,struct sk_buff *skb,void *accel_priv) {
    if (skb == NULL || skb->data == NULL)
        return 0;
    struct xmit_event data = { };
    u64 both = bpf_get_current_pid_tgid();

    data.pid = both;
    if (data.pid == 0)
        return 0;
    data.tgid = both >> 32;
    data.ts = bpf_ktime_get_ns();
    bpf_get_current_comm(&data.comm,sizeof(data.comm));
    data.len = skb->len;
    
    // copy packet contents
    int slot = 0;
    u32 *packet_buf_ptr = packet_buf_head.lookup(&slot);
    if (packet_buf_ptr == NULL)
        return 0;
    u32 buf_head = *packet_buf_ptr;
    u32 next_buf_head = (buf_head + 1) % PACKET_BUFS_PER_cpu;
    packet_buf_head.update(&slot,&next_buf_head);
    
    struct packet_buf *ringbuf = packet_buf.lookup(&buf_head);
    if (ringbuf == NULL)
        return 0;
    
    u32 skb_data_len = skb->data_len;
    u32 headlen = data.len - skb_data_len;
    headlen &= 0xffffff; // Useless,but validator demands it because "this unsigned(!) variable Could otherwise be negative"
    bpf_probe_read_kernel(ringbuf->data,headlen < PACKET_BUF_SIZE ? headlen : PACKET_BUF_SIZE,skb->data);
    data.packet_buf_ptr = buf_head;
    
    data.len = headlen;
    data.datalen = skb_data_len;
    
    data.head = (u64) skb->head;
    data.data = (u64) skb->data;
    data.tail = (u64) skb->tail;
    data.end = (u64) skb->end;
    
    xmits.perf_submit(ctx,&data,sizeof(data));

    return 0;
}

"""

global b

def xmit_received(cpu,data,size):
    global b
    global py_packet_buf
    ev = b["xmits"].event(data)
    print("%-18d %-25s %-8d %-8d %-10d %-10d %-12d %-12d %-12d %-12d" % (ev.ts,ev.comm.decode(),ev.pid,ev.tgid,ev.len,ev.datalen,ev.head,ev.data,ev.tail,ev.end))
    bs = cast(py_packet_buf[ev.packet_buf_ptr][cpu].data,POINTER(c_char))[:ev.len]
    c = bytes(bs)
    print(c.hex())


def observe_kernel():
    # load BPF program
    global b
    b = BPF(text=prog)

    print("%-18s %-25s %-8s %-8s %-10s %-10s %-12s %-12s %-12s %-12s" % ("TS","COMM","PID","TGID","LEN","DATALEN","HEAD","DATA","TAIL","END"))

    b["xmits"].open_perf_buffer(xmit_received)
    global py_packet_buf
    py_packet_buf = b["packet_buf"]

    try:
        while True:
            b.perf_buffer_poll()
    except KeyboardInterrupt:
        print("Kernel observer thread stopped.")

observe_kernel()

解决方法

发现问题。 我需要更换

struct packet_buf {
    char data[PACKET_BUF_SIZE];
};

struct packet_buf {
    unsigned char data[PACKET_BUF_SIZE];
};

然而,当我不使用这些数据进行比较或算术运算时,我不明白符号性如何产生影响。