IP防火墙 – XDP实现

XDP 简介

XDP 在 linux 4.8 版本内核中引入,在位于数据包接受最早的数据点(还未分配 struct __sk_buff),可以直接对数据包改写、丢弃或转发等操作。

本文将用户层传递进来的规则进行操作(丢弃 或 允许),来实现 IP 防火墙的功能。

IP Block

实现分成两部分,用户接口部分 与 内核部分。

用户接口提供两个程序,分别是 加载器 和 IP规则修改:

ipblock-loader: XDP 加载器,将 IP Block XDP Prog 挂载到内核中:

# attach IP Block to eth2
./ipblock-loader -d eth2

# detach IP Block from eth2
./ipblock-loader -d eth2 -u

ipblock-rule: 通过 XDP 暴露的 MAP 结构,变更 IP Block 规则:

# droping IP packets for the ::ffff:c612:13/128
$ ./ipblock-rule -a ::ffff:c612:13/128 -p deny

# allow IP packets for the 192.168.31.0/24
$ ./ipblock-rule -a 192.168.31.0/24 -p allow

# delete rules
$ ./ipblock-rule -d ::ffff:c612:13/128
$ ./ipblock-rule -d 192.168.31.0/24

Map 存储结构

ipblock XDP 程序里定义 IPv4 和 IPv6 两个类型的前缀树 map,方便应用层调用 bpf helper API 进行操作。

map key 类型使用 bpf_lpm_triekey + sockaddr map value 类型为 enum xdp_action

IPv4 sockaddr 使用 uint32_t 类型存放(与 struct in_addr 类型的内存模型一致) IPv6 sockaddr 使用 struct in6_addr

如下所示:

struct lpm_v4_key {
    struct bpf_lpm_trie_key lpm;
    uint32_t                addr;
};

struct lpm_v6_key {
    struct bpf_lpm_trie_key lpm;
    struct in6_addr         addr;
};

// IPv4 map
struct {
    __uint(type, BPF_MAP_TYPE_LPM_TRIE);
    __uint(max_entries, MAX_RULES);
    __type(key, struct lpm_v4_key);
    __type(value, enum xdp_action);
    __uint(map_flags, BPF_F_NO_PREALLOC);
} ipv4_map SEC(".maps");

// IPv6 map
struct {
    __uint(type, BPF_MAP_TYPE_LPM_TRIE);
    __uint(max_entries, MAX_RULES);
    __type(key, struct lpm_v6_key);
    __type(value, enum xdp_action);
    __uint(map_flags, BPF_F_NO_PREALLOC);
} ipv6_map SEC(".maps");

XDP 实现逻辑

由于解析部分代码重复性比较多,做成了宏,简化重复的代码

#define PARSE_FUNC_DECLARATION(STRUCT)              \
static __always_inline                              \
struct STRUCT *parse_ ## STRUCT (struct cursor *c)  \
{                                                   \
    struct STRUCT *ret = c->pos;                    \
    if (c->pos + sizeof(struct STRUCT) > c->end) {  \
        return NULL;                                \
    }                                               \
    c->pos += sizeof(struct STRUCT);                \
    return ret;                                     \
}

PARSE_FUNC_DECLARATION(ethhdr)
PARSE_FUNC_DECLARATION(vlanhdr)
PARSE_FUNC_DECLARATION(iphdr)
PARSE_FUNC_DECLARATION(ipv6hdr)

struct cursor 使用保存了待解析的数据位置。

PARSE_FUNC_DECLARATION(iphdr) 宏定义展开后,生成如下代码:

static __always_inline
struct iphdr *parse_iphdr(struct cursor *c)
{
    struct iphdr *ret = c->pos;
    if (c->pos + sizeof(struct iphdr) > c->end) {
        return NULL;
    }
    c->pos += sizeof(struct iphdr);
    return ret;
}

以下是数据包处理逻辑:

SEC("xdp")
int xdp_prog(struct xdp_md *ctx)
{
    ...

    rc = XDP_PASS;

    cursor_init(&c, ctx);

    // 解析 eth header
    eth = parse_eth(&c, &eth_proto);
    if (eth == NULL) {
        goto pass;
    }

    // 解析 IP header
    if (eth_proto == bpf_htons(ETH_P_IP)) {
        iph = parse_iphdr(&c);
        if (iph == NULL) {
            goto pass;
        }

        // 从 ipv4 map 中拿到 action
        rc = ip_map_lookup_value(&ipv4_map, iph->saddr);

    } else if (eth_proto == bpf_htons(ETH_P_IPV6)) {
        ip6h = parse_ipv6hdr(&c);
        if (ip6h == NULL) {
            goto pass;
        }

        // 从 ipv6 map 中拿到 action
        rc = ip6_map_lookup_value(&ipv6_map, ip6h->saddr);
    }

pass:

    return rc;
}

ipblock-loader

ipblock-loader 是 XDP 加载器,用于将 XDP program 挂载到指定网卡中。

static int
do_load(struct options *opt, struct ipblock_bpf *skel)
{
    int             err;

    // 挂载 XDP 到指定网卡
    err = xdp_link_attach(opt->ifindex, opt->xdp_flags,
                          bpf_program__fd(skel->progs.xdp_prog));
    if (err != 0) {
        return err;
    }

    // PIN map 到 bpf fs 中
    err = pin_maps_in_bpf_object(skel);
    if (err != 0) {
        return err;
    }

    return 0;
}

挂载成功后,将 map PIN 到 bpf fs,路径分别为:

  • /sys/fs/bpf/ipblock/ipv4_map
  • /sys/fs/bpf/ipblock/ipv6_map

ipblock-rule

ipblock-rule 实现为规则控制程序,用于增删改规则

static int
do_add_cmd(options_t *opt)
{
    ...

    // 根据 IP地址类型,打开对应的 bpf map
    fd = open_bpf_map(opt->cidr.af);

    ...

    // 设置 bpf_lpm_trie_key
    lpm = malloc(sizeof(*lpm) + opt->cidr.socklen);
    lpm->prefixlen = opt->cidr.prefixlen;
    memcpy(lpm->data, &opt->cidr.sockaddr, opt->cidr.socklen);

    // BPF_ANY 增加或更新规则
    if (bpf_map_update_elem(fd, lpm, &opt->action, BPF_ANY) != 0) {
        ...
    }

    ...
}

详细代码在文末的 github 仓库链接中。

Reference

BPF and XDP Reference Guide

github libbpf

BPF Portability and CO-RE

https://github.com/cppcoffee/ipblock