【问题标题】:ebpf packet filter on payload matching有效负载匹配上的 ebpf 数据包过滤器
【发布时间】:2020-05-26 23:11:12
【问题描述】:

我是 ebpf 和 xdp 主题的新手,想学习它。我的问题是如何使用 ebpf 过滤器过滤特定有效负载匹配的数据包?例如,如果数据包的数据(有效负载)为 1234,则将其传递给网络堆栈,否则将阻止该数据包。我达到了有效载荷长度。例如,如果我想匹配消息有效负载长度,它可以正常工作,但是当我开始匹配有效负载字符时出现错误。这是我的代码:

int ret_val;
unsigned long payload_offset;
unsigned long payload_size;
const char *payload = "test";
struct ethhdr *eth = data;

if ((void*)eth + sizeof(*eth) <= data_end) {
    struct iphdr *ip = data + sizeof(*eth);
    if ((void*)ip + sizeof(*ip) <= data_end) {
        if (ip->protocol == IPPROTO_UDP ) {
            struct udphdr *udp = (void*)ip + sizeof(*ip);
            if ((void*)udp + sizeof(*udp) <= data_end) {
                if (udp->dest == ntohs(5005)) {
                    payload_offset = sizeof(struct udphdr);
                    payload_size = ntohs(udp->len) - sizeof(struct udphdr);
                    unsigned char *s = (unsigned char *)&payload_size;

                    if (ret_val == __builtin_memcmp(s,payload,4) == 0) {
                        return XDP_DROP;
                    }
                }
            }
        }
    }
}

错误已删除但无法比较有效负载...我正在从 python 套接字代码发送 UDP 消息。如果我比较有效载荷长度,它工作正常。

【问题讨论】:

  • 尝试将memcmp() 替换为__builtin_memcmp()(如果您只需要比较两个字节,则将其完全删除,只需使用==)。如果它不起作用,提供更大的代码块会很有帮助。请注意,unsigned char *s=(unsigned char *)&amp;payload_size; 表示 s 指向保存有效负载大小的变量,这是您想要的吗?并且不确定您如何初始化payload。或者你如何检查长度。
  • ret_val == __builtin_memcmp(s,payload,4) 看起来不正确,您可能需要一个 = 这里。关于代码风格,我会避免在if () 条件下进行比较,如果可以的话,我会更频繁地使用早期返回并避免嵌套所有ifs。
  • 我尝试了 if (ret_val = __builtin_memcmp(s,payload,4) == 0) 但仍然无法匹配有效负载。
  • Good ol'bpf_trace_printk() 来拯救你,你必须调试你的程序:)。我会先查看*s*payload 中的内容,看看您是否满足了您的期望。如果没有完整的代码和观察到错误等,我不确定如何提供更多帮助。

标签: linux linux-kernel bpf xdp-bpf


【解决方案1】:

你尝试了什么?您可能应该阅读更多关于 eBPF 的内容以尝试了解如何处理数据包,您提供的基本示例听起来并不复杂。

基本上,您必须解析标头以查看有效负载的开始位置。 Simple BPF parsing examples 可能会帮助您理解这些原则:

  1. 从报头的开头开始(例如,首先是以太网)
  2. 检查数据包的长度足以容纳标头(否则在尝试访问上层时可能会出现越界访问)
  3. 添加标头长度以获得下一个标头的偏移量(例如 IPv4,然后例如 TCP...)
  4. 冲洗并重复。

在您的情况下,您将处理所有标头,直到获得数据有效负载的偏移量。请注意,如果您尝试匹配的流量始终具有相同的标头(例如始终是 IPv4 和 UDP),这很简单,但如果存在混合(IPv4 + IPv6、封装、IPv4 选项... )。

一旦您有了数据的偏移量,只需将该偏移量处的数据与您的模式进行比较(您可以在 BPF 程序中进行硬编码或从 BPF 映射中获取,具体取决于您的用例) .请注意,do not have access to strcmp(),但如果您需要比较超过 64 位,则可以使用__builtin_memcmp()

(当然,以上所有内容都适用于 C 程序,您可以将其编译为包含带有 LLVM 后端的 eBPF 指令的目标文件。)

如果您要在有效负载中的任意偏移处搜索字符串,请知道 eBPF 从内核 5.3 开始支持(有界)循环(如果我没记错的话)。

【讨论】:

    【解决方案2】:

    您的编辑几乎是一个新问题,所以这里是一个更新的答案。请考虑在未来提出一个新问题。

    您的程序中有很多问题。特别是:

    1|    payload_offset = sizeof(struct udphdr);
    2|    payload_size = ntohs(udp->len) - sizeof(struct udphdr);
    3|    unsigned char *s = (unsigned char *)&payload_size;
    4|
    5|    if (ret_val == __builtin_memcmp(s, payload, 4) == 0) {
    6|        return XDP_DROP;
    7|    }
    
    • 在第 1 行,payload_offset 变量不是偏移量,它只包含 UDP 标头的长度。您需要将其添加到 UDP 标头的开头以获得实际的有效负载偏移量。
    • 第 2 行没问题。
    • 第 3 行没有任何意义!您让s(稍后您将其与您的模式进行比较)指向有效负载的大小? (又名“我在 cmets 中告诉过你!:)”)。相反,它应该指向......有效载荷的开始,也许?所以,基本上,data + payload_offset(一旦偏移量固定)。
    • 在第 3 行和第 5 行之间,缺少对有效负载长度的检查。当您尝试访问s (__builtin_memcmp(s, payload, 4)) 中的有效载荷时,您尝试比较四个字节的数据包数据;您必须确保数据包足够长以读取这四个字节(就像您每次从以太网、IP 或 UDP 标头字段读取之前检查长度一样)。
    • 同时,我们还可以检查有效负载的长度是否等于要匹配的模式的长度,如果它们不同则退出,而无需比较字节。
    • 第 5 行有一个 == 而不是 =,如 cmets 中所述。易于修复。但是,对于您的程序,我对 __builtin_memcmp() 没有运气,似乎 LLVM 不想内联它并将其变成一个失败的函数调用。没关系,没有它我们也可以工作。对于您的示例,您可以转换为 int 并直接比较四字节长值。对于较长的模式和最近的内核(或者在模式大小固定时展开),我们可以使用有界循环。

    这是您的程序的修改版本,适用于我的设置。

    #include <arpa/inet.h>
    #include <linux/bpf.h>
    #include <linux/if_ether.h>
    #include <linux/ip.h>
    #include <linux/udp.h>
    
    int xdp_func(struct xdp_md *ctx)
    {
        void *data_end = (void *)(long)ctx->data_end;
        void *data = (void *)(long)ctx->data;
        char match_pattern[] = "test";
        unsigned int payload_size, i;
        struct ethhdr *eth = data;
        unsigned char *payload;
        struct udphdr *udp;
        struct iphdr *ip;
    
        if ((void *)eth + sizeof(*eth) > data_end)
            return XDP_PASS;
    
        ip = data + sizeof(*eth);
        if ((void *)ip + sizeof(*ip) > data_end)
            return XDP_PASS;
    
        if (ip->protocol != IPPROTO_UDP)
            return XDP_PASS;
    
        udp = (void *)ip + sizeof(*ip);
        if ((void *)udp + sizeof(*udp) > data_end)
            return XDP_PASS;
    
        if (udp->dest != ntohs(5005))
            return XDP_PASS;
    
        payload_size = ntohs(udp->len) - sizeof(*udp);
        // Here we use "size - 1" to account for the final '\0' in "test".
        // This '\0' may or may not be in your payload, adjust if necessary.
        if (payload_size != sizeof(match_pattern) - 1) 
            return XDP_PASS;
    
        // Point to start of payload.
        payload = (unsigned char *)udp + sizeof(*udp);
        if ((void *)payload + payload_size > data_end)
            return XDP_PASS;
    
        // Compare each byte, exit if a difference is found.
        for (i = 0; i < payload_size; i++)
            if (payload[i] != match_pattern[i])
                return XDP_PASS;
    
        // Same payload, drop.
        return XDP_DROP;
    }
    

    【讨论】:

    • 如果我想将它与更多模式进行比较,我应该怎么做?我不能使用 strcmp 来比较每个模式。有什么办法吗?
    • 一次比较和多次比较有什么区别?只是更多用于将模式与函数进行比较的代码(内联与否,取决于您的内核要求)?
    猜你喜欢
    • 2020-04-23
    • 2012-02-24
    • 1970-01-01
    • 2011-07-01
    • 2013-06-24
    • 1970-01-01
    • 2012-04-12
    • 2013-12-20
    • 2016-05-24
    相关资源
    最近更新 更多