You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用BPF在GENEVE封装报文的外层IP头插入时间戳选项?

GENEVE外层IP头插入IP时间戳选项的BPF实现问题

在项目开发中,需要为GENEVE封装报文的外层IP头插入时间戳选项。已尝试将BPF程序挂载到OVS的GENEVE虚拟端口出口TC钩子,但现有程序误将选项插入内层IP头,因此提出两个问题:

  1. 是否可以使用BPF向外层IP头插入IP选项?
  2. 若可行,具体该如何实现?

测试用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接口信息截图

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 21:47:11