如何用BPF在GENEVE封装报文的外层IP头插入时间戳选项?
GENEVE外层IP头插入IP时间戳选项的BPF实现问题
在项目开发中,需要为GENEVE封装报文的外层IP头插入时间戳选项。已尝试将BPF程序挂载到OVS的GENEVE虚拟端口出口TC钩子,但现有程序误将选项插入内层IP头,因此提出两个问题:
- 是否可以使用BPF向外层IP头插入IP选项?
- 若可行,具体该如何实现?
测试用BPF程序代码
第一段BPF代码
#include <linux/pkt_cls.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <arpa/inet.h> #ifndef __section # define __section(NAME) __attribute__((section(NAME), used)) #endif #ifndef __inline # define __inline inline __attribute__((always_inline)) #endif #ifndef lock_xadd # define lock_xadd(ptr, val) ((void)__sync_fetch_and_add(ptr, val)) #endif #ifndef BPF_FUNC # define BPF_FUNC(NAME, ...) (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME #endif #define OPT_LEN 4 #define IP_HDL 20 static int BPF_FUNC(skb_adjust_room, struct __sk_buff *skb, __s32 len_diff, __u32 mode, __u64 flags); //static int BPF_FUNC(skb_vlan_push, struct __sk_buff *skb, uint16_t proto, uint16_t vlan_tci); __section("egress") int push_tun_opt(struct __sk_buff *skb) { struct ethhdr *eth; struct iphdr *iph; __be32 *opt; volatile void *data, *data_end; int ret = TC_ACT_OK; //skb_vlan_push(skb,0x8100,0x1); skb_adjust_room(skb, size(OPT_LEN), BPF_ADJ_ROOM_NET, BPF_F_ADJ_ROOM_ENCAP_L3_IPV4); data = (void *)(long)skb->data; data_end = (void *)(long)skb->data_end; eth = (void *)data; iph = (void *)(eth+1); opt = (void *)(iph+1); if ((void *)(opt+1) > data_end) goto out; *opt = 0x1234abcd; out: return ret; } char __license[] __section("license") = "GPL";
第二段无flags的BPF代码
#include <linux/pkt_cls.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <arpa/inet.h> #include <uapi/linux/bpf.h> #ifndef __section # define __section(NAME) __attribute__((section(NAME), used)) #endif #ifndef __inline # define __inline inline __attribute__((always_inline)) #endif #ifndef lock_xadd # define lock_xadd(ptr, val) ((void)__sync_fetch_and_add(ptr, val)) #endif #ifndef BPF_FUNC # define BPF_FUNC(NAME, ...) (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME #endif #define OPT_LEN 4 #define IP_HDL 20 static int BPF_FUNC(skb_adjust_room, struct __sk_buff *skb, __s32 len_diff, __u32 mode, __u64 flags); //static int BPF_FUNC(skb_vlan_push, struct __sk_buff *skb, uint16_t proto, uint16_t vlan_tci); static int BPF_FUNC(skb_store_bytes, struct __sk_buff *skb, __s32 offset, const void *from, __u32 len, __u64 flags); __section("egress") int push_tun_opt(struct __sk_buff *skb) { struct ethhdr *eth; struct iphdr *iph; __be32 *opt; volatile void *data, *data_end; int ret = TC_ACT_OK; //skb_vlan_push(skb,0x8100,0x1); skb_adjust_room(skb, OPT_LEN, BPF_ADJ_ROOM_NET, 0); data = (void *)(long)skb->data; data_end = (void *)(long)skb->data_end; eth = (void *)data; iph = (void *)(eth+1); opt = (void *)(iph+1); if ((void *)(opt+1) > data_end) goto out; *opt = 0x1234abcd; //skb_store_bytes(skb, sizeof(struct ethhdr) + sizeof(struct iphdr), &opt, OPT_LEN, BPF_F_RECOMPUTE_CSUM); out: return ret; } char __license[] __section("license") = "GPL";
报文捕获截图
- 插入前:

- 插入后:

GENEVE接口信息截图

问题解答
1. 是否可以使用BPF向外层IP头插入IP选项?
可以,但需要注意挂载点的选择以及BPF程序对隧道封装层次的正确识别。
2. 具体实现方案
关键问题分析
现有代码误修改内层IP头的核心原因:挂载到GENEVE虚拟端口出口时,报文尚未完成GENEVE隧道封装,skb->data指向的是内层报文的以太网头,因此操作的是内层IP头。需要在隧道封装完成后再处理报文。
实现步骤
(1)选择正确的TC挂载点
将BPF程序挂载到物理网卡的出口TC钩子,而非GENEVE虚拟端口。此时报文已完成GENEVE封装,外层IP头已存在,可直接操作。
(2)修改BPF程序以定位外层IP头
以下是修正后的BPF程序,可正确向外层IP头插入时间戳选项:
#include <linux/pkt_cls.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/geneve.h> #include <arpa/inet.h> #include <uapi/linux/bpf.h> #ifndef __section # define __section(NAME) __attribute__((section(NAME), used)) #endif #ifndef __inline # define __inline inline __attribute__((always_inline)) #endif #ifndef lock_xadd # define lock_xadd(ptr, val) ((void)__sync_fetch_and_add(ptr, val)) #endif #ifndef BPF_FUNC # define BPF_FUNC(NAME, ...) (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME #endif #define OPT_LEN 4 // 时间戳选项:类型(0x44) + 长度(0x04) + 示例时间戳值 #define IP_OPT_TIMESTAMP 0x44041234abcdULL static int BPF_FUNC(skb_adjust_room, struct __sk_buff *skb, __s32 len_diff, __u32 mode, __u64 flags); static int BPF_FUNC(skb_store_bytes, struct __sk_buff *skb, __s32 offset, const void *from, __u32 len, __u64 flags); static int BPF_FUNC(bpf_l3_csum_replace, struct __sk_buff *skb, __u32 off, __u32 old, __u32 new, __u32 flags); __section("egress") int push_outer_ip_opt(struct __sk_buff *skb) { struct ethhdr *eth; struct iphdr *iph; __u32 ip_hdr_len; volatile void *data, *data_end; int ret = TC_ACT_OK; data = (void *)(long)skb->data; data_end = (void *)(long)skb->data_end; // 定位外层以太网头 eth = data; if ((void *)(eth + 1) > data_end) goto out; // 仅处理IPv4封装的GENEVE报文 if (eth->h_proto != htons(ETH_P_IP)) goto out; // 定位外层IP头 iph = (void *)(eth + 1); if ((void *)(iph + 1) > data_end) goto out; // 检查IP头剩余空间(IP头最大60字节) ip_hdr_len = iph->ihl * 4; if (ip_hdr_len + OPT_LEN > 60) goto out; // 为IP选项调整skb空间 if (skb_adjust_room(skb, OPT_LEN, BPF_ADJ_ROOM_NET, BPF_F_ADJ_ROOM_ENCAP_L3_IPV4) != 0) goto out; // 调整后重新获取指针 data = (void *)(long)skb->data; data_end = (void *)(long)skb->data_end; eth = data; iph = (void *)(eth + 1); if ((void *)(iph + 1) > data_end) goto out; // 移动IP头后原有内容,腾出选项空间 __u32 opt_offset = sizeof(struct ethhdr) + ip_hdr_len; if (skb_store_bytes(skb, opt_offset + OPT_LEN, (void *)(iph + ip_hdr_len/4), skb->len - opt_offset, BPF_F_RECOMPUTE_CSUM) != 0) goto out; // 写入时间戳选项 __u64 opt_val = IP_OPT_TIMESTAMP; if (skb_store_bytes(skb, opt_offset, &opt_val, OPT_LEN, BPF_F_RECOMPUTE_CSUM) != 0) goto out; // 更新IP头字段:ihl、总长度 __u8 old_ihl = iph->ihl; iph->ihl += OPT_LEN / 4; __be16 old_tot_len = iph->tot_len; iph->tot_len = htons(ntohs(iph->tot_len) + OPT_LEN); // 重新计算IP校验和 bpf_l3_csum_replace(skb, sizeof(struct ethhdr) + offsetof(struct iphdr, check), htons(old_ihl * 4 + ntohs(old_tot_len)), htons(iph->ihl * 4 + ntohs(iph->tot_len)), 0); out: return ret; } char __license[] __section("license") = "GPL";
(3)挂载BPF程序到物理网卡
执行以下命令:
# 为物理网卡添加clsact队列规则 tc qdisc add dev <物理网卡名> clsact # 挂载BPF程序到出口钩子 tc filter add dev <物理网卡名> egress bpf da obj <编译后的BPF程序.o> sec egress
(4)验证
使用tcpdump在物理网卡捕获报文,查看外层IP头是否包含时间戳选项:
tcpdump -i <物理网卡名> ip proto 47 -vvv
内容的提问来源于stack exchange,提问作者Hai Pham
相关产品推荐
相关产品推荐

