IP防火墙 -- XDP实现
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, ð_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 仓库链接中。