在 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[1]
第一步:干掉 percpu map
基于原来的源代码,干掉 percpu map,然后将参数 offset
的类型设置为 __u8
:
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:
$ 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.c
里 void *data = ctx_ptr(xdp, data) + offset;
出错了,错误提示是:pkt
指针不能跟不限制最小值的寄存器进行数学计算。
看明白这个错误就好,接下来就尝试修复吧。
第二步:修复导致 verifier 过不了的问题
在 parse_option()
里使用 if
判断最小值试试:
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++;
// ...
}
不过,还是出错了:
$ 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)
还是同样的问题。该怎么办呢?
...
直接上解决办法吧:
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;
。
为什么要这么改呢?
verifier 不知道寄存器对应的变量的类型信息,因为已经被 clang
掩盖掉了。使用按位与对变量进行数值范围限制,是为了更好地告知 verifier 寄存器的数值范围;并且能避免 if
臃肿、且可能避免被clang
优化。
总结
通过这次尝试,学会了使用按位与对变量进行数值范围限制。
所以,当 verifier 提示跟数值范围有关的错误时,可以尝试:
使用 if
限制最大值、最小值。使用 &
直接限制数值范围。
同时,可以推断:clang
知道变量类型信息,但 verifier 无法从寄存器推断出当前寄存器的类型信息 (数值范围);所以,需要绕开 clang
的优化,
给 verifier 提供更精确的数值范围。
learn-by-example topt.c: https://github.com/Asphaltt/learn-by-example/blob/main/ebpf/tcpoptions/topt.c