网络数据包计数:无法从 BPF 套接字过滤器读取数据包数据

Network packet counting: Failure to read packet data from a BPF socket filter

我想计算传入的网络数据包,每个 TOS 值的长度以字节为单位。我创建了两个映射,第一个包含 256 个条目,其中包含每个 TOS 值的数据包计数,第二个包含数据包字节。所以我写了下面的 eBPF 套接字过滤器:

struct bpf_insn prog[]{
  BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),

  //we use dgram socket, so packet starts directly from IP header
  // BPF_LD_ABS(BPF_H, offsetof(struct ethhdr, h_proto)), // r0 = header type
  // BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, ETH_P_IP, 2),        // if (r0 == IPv4) skip 2
  // BPF_MOV64_IMM(BPF_REG_0, 0),                         // r0 = 0
  // BPF_EXIT_INSN(),                                     // return

  //check for IP version, we only interested in v4
  BPF_LD_ABS(BPF_B, 0),                           // R0 = ip->vers: offsetof(struct iphdr, version)
  BPF_ALU64_IMM(BPF_AND, BPF_REG_0, 0xF0),        // r0 = r0 & 0xF0
  BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0x40, 2),       // if (r0 == 0x40) goto pc+2
  BPF_MOV64_IMM(BPF_REG_0, 0),                    // r0 = 0
  BPF_EXIT_INSN(),                                // return

  // load packet TOS value
  BPF_LD_ABS(BPF_B, offsetof(struct iphdr, tos)), // R0 = ip->tos
  BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4),  // *(u32 *)(fp - 4) = r0
  BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),           // r2 = fp
  BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),          // r2 = fp - 4
  //first map with packet counters
  BPF_LD_MAP_FD(BPF_REG_1, map_cnt_fd),           // r1 = map_fd
  BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0,
               BPF_FUNC_map_lookup_elem),         // r0 = map_lookup(r1, r2)
  BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2),          // if (r0 == 0) goto pc+2
  BPF_MOV64_IMM(BPF_REG_1, 1),                    // r1 = 1
  BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW,
               BPF_REG_0, BPF_REG_1, 0, 0),       // xadd r0 += r1

  BPF_LD_ABS(BPF_B, offsetof(struct iphdr, tos)), // R0 = ip->tos
  BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4),  // *(u32 *)(fp - 4) = r0
  BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
  BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),          // r2 = fp - 4
  //second map with packet bytes
  BPF_LD_MAP_FD(BPF_REG_1, map_bytes_fd),         // r1 = map_fd
  BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0,
               BPF_FUNC_map_lookup_elem),         // r0 = map_lookup(r1, r2)
  BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2),          // if (r0 == 0) goto pc+2
  // FIXME big endian
  BPF_LDX_MEM(BPF_H, BPF_REG_1, BPF_REG_6,
              offsetof(struct iphdr, tot_len)),   // r1 = tot_len
  BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW,
               BPF_REG_0, BPF_REG_1, 0, 0),       // xadd r0 += r1

  BPF_MOV64_IMM(BPF_REG_0, 0),                    // r0 = 0
  BPF_EXIT_INSN(),
};

映射创建没有错误,套接字过滤器程序创建也很好,数据包计数器部分正常工作。但是字节计数器始终为 0。该代码有什么问题?

我试着写了一个简单的example. To compile you just need to include bpf_insn.h

问题:从套接字缓冲区读取

程序启动前放在BPF_REG_1的上下文不是指向数据开头的指针。相反,它是指向 struct __sk_buff 定义的 in the UAPI headers 的指针,如下所示:

struct __sk_buff {
    __u32 len;
    ...
}

因此,当您尝试从您的 IP header 读取数据时:

  BPF_LDX_MEM(BPF_H, BPF_REG_1, BPF_REG_6, offsetof(struct iphdr, tot_len)),

您实际上是在从 struct __sk_buff 的偏移量 2 处读取两个字节(我们称其为指针 skb)。因为您的系统是小尾数法,所以这对应于 skb->len 的最高有效位,除非您的数据包大于 2^16 字节(不太可能),否则为 0。

这里有两种可能的解决方案。

解决方案 1:使用绝对负载

我们可以更新您的程序以在正确的位置读取 IP 长度。我相信这对于 BPF_LDX_MEM() 是不可能的,因为套接字过滤器 do not permit direct packet access。解决方法是改用绝对负载。您的程序将变为:

struct bpf_insn prog[]{
  BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),

  // ... packet number counter, skipped for brevity

  // Read IP length and store to r7 (preserved during helper calls)
  BPF_LD_ABS(BPF_H,
             offsetof(struct iphdr, tot_len)),    // r0 = tot_len
  BPF_MOV64_REG(BPF_REG_7, BPF_REG_0),            // r7 = r0

  // No need to parse ToS a second time here, skipped
  BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
  BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),          // r2 = fp - 4
  //second map with packet bytes
  BPF_LD_MAP_FD(BPF_REG_1, map_bytes_fd),         // r1 = map_fd
  BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0,
               BPF_FUNC_map_lookup_elem),         // r0 = map_lookup(r1, r2)
  BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2),          // if (r0 == 0) goto pc+2

  // Now add length to the counter
  BPF_STX_XADD(BPF_DW, BPF_REG_0, BPF_REG_7, 0),  // xadd r0 += r7

  BPF_MOV64_IMM(BPF_REG_0, 0),                    // r0 = 0
  BPF_EXIT_INSN(),
};

解决方案 2:只需使用 skb->len

另一个解决方案是从 skb 中获取长度,因为内核已经为我们计算了它。这只是固定负载的偏移量和长度的问题,您的 BPF_STX_MEM(), BPF_XADD() 将变为:

  BPF_LDX_MEM(BPF_W, BPF_REG_1, BPF_REG_6,
              offsetof(struct __sk_buff, len)),         // r1 = skb->len
  BPF_STX_XADD(BPF_DW, BPF_REG_0, BPF_REG_1, 0),        // xadd r0 += r1