tcptraceebpfkprobe

TCP Packet Payload Sniffing with eBPF Kprobe Attachment to tcp_sendmsg


I am trying to read the payloads of outgoing TCP packets sent by the redis-cli utility to a remote Redis database via eBPF as a test of eBPF's capabilities. My plan is to filter all outgoing TCP packets by their destination port(accomplished), as accessed by the first parameter of tcp_sendmsg(), sock* sk, and then obtain their payloads by accessing the iov_base field through &(msghdr->msg_iter.iov)->iov_base, where msghdr is the second parameter.

However, the copied data appears to be corrupt and bears no relation to any payload of any TCP packet(I am tracking the true payloads through Wireshark). Here is the code:

struct redis_sys_packet {
    u16 lport;
    u16 dport;
    u32 nr_segments;
    u32 packet_length;
    char packet[128];
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, struct redis_sys_packet);
    __type(value, u64);
} redis_sys_write_calls SEC(".maps");

//... other code

// kprobe handler for tcp_sendmsg 
SEC("kprobe/tcp_sendmsg")
int kprobe__tcp_sendmsg(struct pt_regs *ctx) {
    if (!ctx || ctx == NULL) return 0;
    struct sock *sk = (struct sock *)(ctx->di);
    if (!sk || sk == NULL) return 0;
    u16 lport = 0;
    u16 dport = 0;


    bpf_probe_read_kernel(&lport, sizeof(lport), &sk->__sk_common.skc_num);
    bpf_probe_read_kernel(&dport, sizeof(dport), &sk->__sk_common.skc_dport);

    struct redis_sys_packet pack = {};
    pack.lport = lport;
    pack.dport = bpf_ntohs(dport);
    if (pack.dport != 19795) return 0; // filter by port

    // second parameter
    struct msghdr* msg = (struct msghdr *)(ctx->si);
    if (!msg || msg == NULL) return 0;
    // Get the pointer to the iov (IO vector) array that holds the payload
    struct iov_iter *iter = &(msg->msg_iter);
    if (!iter || iter == NULL) return 0;
    bpf_probe_read(&pack.nr_segments, sizeof(unsigned long), &(iter->nr_segs));
    const struct iovec *iov = (const struct iovec *)&(iter->iov);

    // Check if we can access the first iovec
    if (!iov)
        return 0;


    // Get the base address of the payload
    void *iovbase;
    bpf_probe_read(&iovbase, sizeof(void *), &iov[0].iov_base);
    unsigned long iov_len;
    // read from count instead of iov->iov_len - for some reason this always returns 1, not the true length
    bpf_probe_read(&iov_len, sizeof(size_t), &(iter->count));
    pack.packet_length = (u32)iov_len;
    u32 copy_length = (u32)iov_len;
    if (copy_length > 128) {
        copy_length = 128;
    } else if (copy_length <= 0) {
        return 0;
    }


    bpf_probe_read_kernel(pack.packet, copy_length, iovbase);
    increment_map(&redis_sys_write_calls, &pack, 1); // returns irrelevant data

    return 0;
}

If there is any other tracepoint/function to which I can attach a kprobe/uprobe to track outgoing TCP packets and sniff the payload, I would also be very thankful. I have tried attaching to xdp already, but it did not work because of unrelated faults.

I am carrying out all these on Ubuntu 22.04 on top of WSL2 with a kernel of 5.15.153.1-microsoft-standard-WSL2.


Solution

  • You can use socket filters, TC (Traffic Control), or XDP (eXpress Data Path) to capture TCP, UDP, and ICMP payloads without resorting to kprobe, uprobe, or tracepoint.

    Here are some example implementations you can explore: