eBPF Talk: XDP 解析所有 TCP options 里,已经做到了使用 XDP 解析所有 TCP options 的功能。

不过,其中使用了 percpu map 在 XDP 和 freplace 之间传递 offset;那么,是否有办法将该 percpu map 优化掉呢?

TL;DR 历尽艰难,将 percpu map 优化掉后,可以通过函数参数传递 offset 了。优化后的源代码:learn-by-example topt.c

第一步:干掉 percpu map

基于原来的源代码,干掉 percpu map,然后将参数 offset 的类型设置为 __u8:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
diff --git a/ebpf/tcpoptions/tcp.c b/ebpf/tcpoptions/tcp.c
index d61db80..955487d 100644
--- a/ebpf/tcpoptions/tcp.c
+++ b/ebpf/tcpoptions/tcp.c
@@ -29,11 +14,12 @@ __check(void *data, void *data_end, int length)
 }

 __noinline int
-option_parser(struct xdp_md *xdp)
+option_parser(struct xdp_md *xdp, __u8 offset)
 {
     int ret = 0;

     barrier_var(ret);
+    barrier_var(offset);
     return xdp ? 1 : ret;
 }

@@ -41,23 +27,20 @@ static void
 __parse_options(struct xdp_md *xdp, struct tcphdr *tcph)
 {
     int length = (tcph->doff << 2) - sizeof(struct tcphdr);
-
-    __u32 *offset = get_buf();
-    if (!offset)
-        return;
+    __u8 offset;

     /* Initialize offset to tcp options part. */
-    *offset = (void *) (tcph + 1) - ctx_ptr(xdp, data);;
+    offset = (void *) (tcph + 1) - ctx_ptr(xdp, data);;

     for (int i = 0; i < ((1<<4 /* bits number of doff */)<<2)-sizeof(struct tcphdr); i++) {
         if (length <= 0)
             break;

-        int ret = option_parser(xdp);
+        int ret = option_parser(xdp, offset);
         if (ret <= 0)
             break;

-        *offset += ret;
+        offset += ret;
         length -= ret;
     }
 }
diff --git a/ebpf/tcpoptions/topt.c b/ebpf/tcpoptions/topt.c
index 216d363..76265fc 100644
--- a/ebpf/tcpoptions/topt.c
+++ b/ebpf/tcpoptions/topt.c
@@ -55,21 +55,6 @@ static volatile const __u32 TARGET_OPVAL_LEN = 0; // including the suffix '\0'

 #define TCPOLEN_MARK                    255

-struct {
-    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
-    __type(key, int);
-    __type(value, __u32);
-    __uint(max_entries, 1);
-} buf SEC(".maps");
-
-static __always_inline __u32 *
-get_buf(void)
-{
-    int key = 0;
-
-    return bpf_map_lookup_elem(&buf, &key);
-}
-
 struct tcp_option {
     __u8 opsize;
     char opname[35];
@@ -155,7 +140,7 @@ modify_option(void *data, void *data_end, __u8 opsize)
 }

 static int
-parse_option(struct xdp_md *xdp, __u8 /* should not be __u32 */ offset)
+parse_option(struct xdp_md *xdp, __u8 offset)
 {
     void *data = ctx_ptr(xdp, data) + offset;
     void *data_end = ctx_ptr(xdp, data_end);
@@ -242,11 +227,7 @@ parse_option(struct xdp_md *xdp, __u8 /* should not be __u32 */ offset)
 }

 SEC("freplace/option_parser")
-int topt(struct xdp_md *xdp)
+int topt(struct xdp_md *xdp, __u8 offset)
 {
-    __u32 *offset = get_buf();
-    if (!offset)
-        return -1;
-
-    return parse_option(xdp, *offset);
+    return parse_option(xdp, offset);
 }

main.go 也一并改了。

不过,运行起来后,却过不了 verifier:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ sudo ./tcpoptions
2024/09/04 14:28:35 Verifier error: load program: invalid argument:
    Validating topt() func#0...
    0: R1=ctx() R2=scalar() R10=fp0
    ; void *data = ctx_ptr(xdp, data) + offset;
    0: (61) r8 = *(u32 *)(r1 +0)          ; R1=ctx() R8_w=pkt(r=0)
    ; void *data = ctx_ptr(xdp, data) + offset;
    1: (0f) r8 += r2
    math between pkt pointer and register with unbounded min value is not allowed
    processed 2 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
2024/09/04 14:28:35 Failed to load topt objects: field Topt: program topt: load program: invalid argument: math between pkt pointer and register with unbounded min value is not allowed (7 line(s) omitted)

topt.cvoid *data = ctx_ptr(xdp, data) + offset; 出错了,错误提示是:pkt 指针不能跟不限制最小值的寄存器进行数学计算。

看明白这个错误就好,接下来就尝试修复吧。

第二步:修复导致 verifier 过不了的问题

parse_option() 里使用 if 判断最小值试试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
static int
parse_option(struct xdp_md *xdp, __u8 offset)
{
    void *data_end = ctx_ptr(xdp, data_end);
    void *data = ctx_ptr(xdp, data);
    struct tcp_option *topt;
    __u8 opcode, opsize;

    if (offset < 20)
        return -1;

    data += offset;
    if (!__check(data, data_end, 1))
        return -1;

    opcode = *(__u8 *) data;
    data++;

    // ...
}

不过,还是出错了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ sudo ./tcpoptions
2024/09/04 14:36:52 Verifier error: load program: invalid argument:
    Validating topt() func#0...
    0: R1=ctx() R2=scalar() R10=fp0
    ; int topt(struct xdp_md *xdp, __u8 offset)
    0: (18) r0 = 0xffffffff               ; R0_w=0xffffffff
    ; void *data_end = ctx_ptr(xdp, data_end);
    2: (61) r6 = *(u32 *)(r1 +4)          ; R1=ctx() R6_w=pkt_end()
    3: (b7) r3 = 20                       ; R3_w=20
    ; if (offset < 20)
    4: (2d) if r3 > r2 goto pc+9          ; R2=scalar(umin=20) R3_w=20
    ; void *data = ctx_ptr(xdp, data);
    5: (61) r8 = *(u32 *)(r1 +0)          ; R1=ctx() R8_w=pkt(r=0)
    ; data += offset;
    6: (0f) r8 += r2
    math between pkt pointer and register with unbounded min value is not allowed
    processed 6 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
2024/09/08 07:57:26 Failed to load topt objects: field Topt: program topt: load program: invalid argument: math between pkt pointer and register with unbounded min value is not allowed (14 line(s) omitted)

还是同样的问题。该怎么办呢?

直接上解决办法吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
static int
parse_option(struct xdp_md *xdp, int offset)
{
    void *data_end = ctx_ptr(xdp, data_end);
    void *data = ctx_ptr(xdp, data);
    struct tcp_option *topt;
    __u8 opcode, opsize;

    offset &= 255;
    data += offset;
    if (!__check(data, data_end, 1))
        return -1;

    opcode = *(__u8 *) data;
    data++;

    // ...
}

解决办法是将参数 offset 的类型改为 int,并在 data += offset; 前加一行 offset &= 255;

为什么要这么改呢?

  1. verifier 不知道寄存器对应的变量的类型信息,因为已经被 clang 掩盖掉了。
  2. 使用按位与对变量进行数值范围限制,是为了更好地告知 verifier 寄存器的数值范围;并且能避免 if 臃肿、且可能避免被 clang 优化。

总结

通过这次尝试,学会了使用按位与对变量进行数值范围限制。

所以,当 verifier 提示跟数值范围有关的错误时,可以尝试:

  1. 使用 if 限制最大值、最小值。
  2. 使用 & 直接限制数值范围。

同时,可以推断:clang 知道变量类型信息,但 verifier 无法从寄存器推断出当前寄存器的类型信息 (数值范围);所以,需要绕开 clang 的优化, 给 verifier 提供更精确的数值范围。