据了解(未查证),从 clang12 开始,eBPF 代码中的变量声明不再要求写在函数体的最前方,而是可以按需声明并初始化。

写法一:一次性声明全部的变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static __always_inline void
set_output_headers(struct __sk_buff *skb, struct event *ev)
{
    struct ethhdr *eth;
    struct vlan_hdr *vh;
    struct iphdr *iph;
    struct udphdr *udph;
    struct tcphdr *tcph;
    struct icmphdr *icmph;
    int l3_off = 0, l4_off, var_off = 0, cpy_off = 0;

    // ...
}

如上代码片段,将所有需要处理的协议头对应的 struct 变量放在一起声明。如果后续需要处理其它协议,则需要在此处添加对应的 struct 变量的声明。

先声明后使用

我不喜欢这种写法,不过这种写法能在编码的时候有一个好处:使用 typeof(hdr) 获取类型、使用 sizeof(*hdr) 获取协议头大小,而无需啰嗦地再写一遍 struct xxxhdr

写法二:按需声明并初始化

 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
static __always_inline __u16
calc_l3_off(struct sk_buff *skb)
{
    __u16 skb_network_header = BPF_CORE_READ(skb, network_header);
    if (skb_network_header != (__u16)~0ul)
        return skb_network_header;

    // calculate l3_off from eth layer
    __u16 skb_mac_header = BPF_CORE_READ(skb, mac_header);
    __u16 l2_off = 0;
    if (skb_mac_header != (__u16)~0ul)
        l2_off = skb_mac_header;

    __u16 l3_off = 0;

    unsigned char *skb_head = BPF_CORE_READ(skb, head);
    struct ethhdr *eth = (struct ethhdr *)(skb_head + l2_off);
    l3_off += sizeof(*eth);

    __be16 l3_proto = BPF_CORE_READ(eth, h_proto);
    if (is_vlan_proto(l3_proto)) {
        struct vlan_hdr *vh = (struct vlan_hdr *)(eth + 1);
        l3_off += sizeof(*vh);

        l3_proto = BPF_CORE_READ(vh, h_vlan_encapsulated_proto);
    }

    if (!is_ipv4_proto(l3_proto))
        return 0;

    return l3_off;
}

如上代码片段,将需要用到的变量在需要的地方声明并初始化即可。

对比写法一,此种写法对变量的使用更加灵活,对程序员更加友好。

写法三:混合写法一和写法二

 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
static volatile const struct config __cfg = {};

static __always_inline bool
filter_skb(struct sk_buff *skb)
{
    struct config cfg = __cfg;

    if (!cfg.target_ip && !cfg.target_port && !cfg.l4_proto)
        return false;

    __u16 l3_off = calc_l3_off(skb);
    if (l3_off == 0)
        return true;

    unsigned char *skb_head = BPF_CORE_READ(skb, head);

    struct iphdr *iph;
    iph = (typeof(iph))(skb_head + l3_off);
    __u8 l4_proto = BPF_CORE_READ(iph, protocol);
    if (filter_proto(l4_proto))
        return true;

    if (cfg.l4_proto && cfg.l4_proto != l4_proto)
        return true;

    __be32 dip = BPF_CORE_READ(iph, daddr);
    if (cfg.target_ip && cfg.target_ip != dip)
        return true;

    struct udphdr *udph;
    udph = (typeof(udph))(skb_head + l3_off + calc_ipv4_hdr_size(iph));
    __be16 dport = BPF_CORE_READ(udph, dest);
    if (cfg.target_port && cfg.target_port != dport)
        return true;

    return false;
}

如上代码片段,对于协议头的变量,先声明而后使用 typeof() 初始化,其它变量则可以直接声明并初始化。

小结

编写的 eBPF 代码量逐渐增大后,逐渐地形成了自己的代码风格。

而且,有了自己的代码风格后,可以一次性地编写 eBPF 代码并编译、加载通过,而无需反 复地来回调试(为了通过编译器、eBPF verifier/校验器的检查)。