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 仓库链接中。