BPF_MAP_TYPE_SOCKMAP 和 BPF_MAP_TYPE_SOCKHASH

注意

  • BPF_MAP_TYPE_SOCKMAP 在内核版本 4.14 中引入

  • BPF_MAP_TYPE_SOCKHASH 在内核版本 4.18 中引入

BPF_MAP_TYPE_SOCKMAPBPF_MAP_TYPE_SOCKHASH 映射可用于在套接字之间重定向 skb,或借助 BPF 辅助函数 bpf_sk_redirect_map()bpf_sk_redirect_hash()bpf_msg_redirect_map()bpf_msg_redirect_hash(),根据 BPF (verdict) 程序的执行结果在套接字级别应用策略。

BPF_MAP_TYPE_SOCKMAP 由一个数组支持,该数组使用整数键作为索引来查找 struct sock 的引用。映射值是套接字描述符。类似地,BPF_MAP_TYPE_SOCKHASH 是一个由哈希支持的 BPF 映射,通过套接字描述符保存对套接字的引用。

注意

值类型可以是 __u32 或 __u64;后者 (__u64) 用于支持将套接字 cookie 返回给用户空间。将映射持有的 struct sock * 返回给用户空间既不安全也无用。

这些映射可以附加 BPF 程序,具体包括解析器程序和判决程序。解析器程序确定已解析了多少数据,因此需要排队多少数据才能得出判决。判决程序本质上是重定向程序,可以返回 __SK_DROP__SK_PASS__SK_REDIRECT 的判决。

当一个套接字插入到这些映射之一时,它的套接字回调被替换,并且一个 struct sk_psock 附加到它。此外,这个 sk_psock 继承了附加到映射的程序。

一个套接字对象可以存在于多个映射中,但只能继承一个解析器或判决程序。如果将套接字对象添加到映射会导致存在多个解析器程序,则更新将返回 EBUSY 错误。

可附加到这些映射的受支持程序是

struct sk_psock_progs {
        struct bpf_prog *msg_parser;
        struct bpf_prog *stream_parser;
        struct bpf_prog *stream_verdict;
        struct bpf_prog *skb_verdict;
};

注意

用户不允许将 stream_verdictskb_verdict 程序附加到同一个映射。

映射程序的附加类型是

  • msg_parser 程序 - BPF_SK_MSG_VERDICT

  • stream_parser 程序 - BPF_SK_SKB_STREAM_PARSER

  • stream_verdict 程序 - BPF_SK_SKB_STREAM_VERDICT

  • skb_verdict 程序 - BPF_SK_SKB_VERDICT

还有一些额外的辅助函数可用于解析器和判决程序:bpf_msg_apply_bytes()bpf_msg_cork_bytes()。通过 bpf_msg_apply_bytes(),BPF 程序可以告诉基础设施给定的判决应适用于多少字节。辅助函数 bpf_msg_cork_bytes() 处理另一种情况,即 BPF 程序在收到更多字节之前无法对消息作出判决,并且程序不希望在数据已知为良好之前转发数据包。

最后,辅助函数 bpf_msg_pull_data()bpf_msg_push_data() 可用于 BPF_PROG_TYPE_SK_MSG BPF 程序,以拉取数据并将起始和结束指针设置为给定值,或者向 struct sk_msg_buff *msg 添加元数据。

所有这些辅助函数将在下面详细描述。

用法

内核 BPF

bpf_msg_redirect_map()

long bpf_msg_redirect_map(struct sk_msg_buff *msg, struct bpf_map *map, u32 key, u64 flags)

此辅助函数用于在套接字级别实现策略的程序中。如果消息 msg 被允许通过(即,如果判决 BPF 程序返回 SK_PASS),则将其重定向到由 map(类型为 BPF_MAP_TYPE_SOCKMAP)在索引 key 处引用的套接字。入口和出口接口都可以用于重定向。flags 中的 BPF_F_INGRESS 值用于选择入口路径,否则选择出口路径。这是目前唯一支持的标志。

成功时返回 SK_PASS,错误时返回 SK_DROP

bpf_sk_redirect_map()

long bpf_sk_redirect_map(struct sk_buff *skb, struct bpf_map *map, u32 key u64 flags)

将数据包重定向到由 map(类型为 BPF_MAP_TYPE_SOCKMAP)在索引 key 处引用的套接字。入口和出口接口都可以用于重定向。flags 中的 BPF_F_INGRESS 值用于选择入口路径,否则选择出口路径。这是目前唯一支持的标志。

成功时返回 SK_PASS,错误时返回 SK_DROP

bpf_map_lookup_elem()

void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)

类型为 struct sock * 的套接字条目可以使用 bpf_map_lookup_elem() 辅助函数检索。

bpf_sock_map_update()

long bpf_sock_map_update(struct bpf_sock_ops *skops, struct bpf_map *map, void *key, u64 flags)

向引用套接字的 map 添加或更新条目。skops 用作与 key 相关联的条目的新值。flags 参数可以是以下之一:

  • BPF_ANY: 创建新元素或更新现有元素。

  • BPF_NOEXIST: 仅在元素不存在时创建新元素。

  • BPF_EXIST: 更新现有元素。

如果 map 具有 BPF 程序(解析器和判决),则这些程序将被添加的套接字继承。如果套接字已附加到 BPF 程序,则会导致错误。

成功时返回 0,失败时返回负错误码。

bpf_sock_hash_update()

long bpf_sock_hash_update(struct bpf_sock_ops *skops, struct bpf_map *map, void *key, u64 flags)

向引用套接字的 sockhash map 添加或更新条目。skops 用作与 key 相关联的条目的新值。

flags 参数可以是以下之一

  • BPF_ANY: 创建新元素或更新现有元素。

  • BPF_NOEXIST: 仅在元素不存在时创建新元素。

  • BPF_EXIST: 更新现有元素。

如果 map 具有 BPF 程序(解析器和判决),则这些程序将被添加的套接字继承。如果套接字已附加到 BPF 程序,则会导致错误。

成功时返回 0,失败时返回负错误码。

bpf_msg_redirect_hash()

long bpf_msg_redirect_hash(struct sk_msg_buff *msg, struct bpf_map *map, void *key, u64 flags)

此辅助函数用于在套接字级别实现策略的程序中。如果消息 msg 被允许通过(即,如果判决 BPF 程序返回 SK_PASS),则使用哈希 key 将其重定向到由 map(类型为 BPF_MAP_TYPE_SOCKHASH)引用的套接字。入口和出口接口都可以用于重定向。flags 中的 BPF_F_INGRESS 值用于选择入口路径,否则选择出口路径。这是目前唯一支持的标志。

成功时返回 SK_PASS,错误时返回 SK_DROP

bpf_sk_redirect_hash()

long bpf_sk_redirect_hash(struct sk_buff *skb, struct bpf_map *map, void *key, u64 flags)

此辅助函数用于在 skb 套接字级别实现策略的程序中。如果 sk_buff skb 被允许通过(即,如果判决 BPF 程序返回 SK_PASS),则使用哈希 key 将其重定向到由 map(类型为 BPF_MAP_TYPE_SOCKHASH)引用的套接字。入口和出口接口都可以用于重定向。flags 中的 BPF_F_INGRESS 值用于选择入口路径,否则选择出口路径。这是目前唯一支持的标志。

成功时返回 SK_PASS,错误时返回 SK_DROP

bpf_msg_apply_bytes()

long bpf_msg_apply_bytes(struct sk_msg_buff *msg, u32 bytes)

对于套接字策略,将 BPF 程序的判决应用于消息 msg 的接下来(bytes 数)字节。例如,此辅助函数可用于以下情况:

  • 一个单独的 sendmsg()sendfile() 系统调用包含多个逻辑消息,BPF 程序应读取这些消息并对其应用判决。

  • BPF 程序只关心读取 msg 的前 bytes 字节。如果消息具有很大的有效载荷,那么为所有字节重复设置和调用 BPF 程序,即使判决已知,也会产生不必要的开销。

返回 0

bpf_msg_cork_bytes()

long bpf_msg_cork_bytes(struct sk_msg_buff *msg, u32 bytes)

对于套接字策略,阻止对消息 msg 执行判决 BPF 程序,直到已累积了指定数量的 bytes

当需要在指定数量的字节之后才能分配判决时,即使数据跨越多个 sendmsg()sendfile() 调用,也可以使用此功能。

返回 0

bpf_msg_pull_data()

long bpf_msg_pull_data(struct sk_msg_buff *msg, u32 start, u32 end, u64 flags)

对于套接字策略,从用户空间拉取 msg 的非线性数据,并将指针 msg->datamsg->data_end 分别设置为 msgstartend 字节偏移量。

如果类型为 BPF_PROG_TYPE_SK_MSG 的程序在 msg 上运行,它只能解析 (data, data_end) 指针已消耗的数据。对于 sendmsg() 钩子,这可能是第一个 scatterlist 元素。但对于依赖 MSG_SPLICE_PAGES 的调用(例如 sendfile()),这将是范围 (0, 0),因为数据与用户空间共享,并且默认目标是避免在 BPF 判决决定期间(或之后)允许用户空间修改数据。此辅助函数可用于拉取数据并将起始和结束指针设置为给定值。必要时将复制数据(即,如果数据不是线性的,并且起始和结束指针未指向同一块)。

调用此辅助函数可能会改变底层的包缓冲区。因此,在加载时,验证器之前对指针进行的所有检查都将失效,如果辅助函数与直接数据包访问结合使用,则必须重新执行。

flags 的所有值都保留供将来使用,并且必须保持为零。

成功时返回 0,失败时返回负错误码。

bpf_map_lookup_elem()

void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)

在 sockmap 或 sockhash 映射中查找套接字条目。

返回与 key 关联的套接字条目,如果未找到条目则返回 NULL。

bpf_map_update_elem()

long bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)

在 sockmap 或 sockhash 中添加或更新套接字条目。

flags 参数可以是以下之一

  • BPF_ANY: 创建新元素或更新现有元素。

  • BPF_NOEXIST: 仅在元素不存在时创建新元素。

  • BPF_EXIST: 更新现有元素。

成功时返回 0,失败时返回负错误码。

bpf_map_delete_elem()

long bpf_map_delete_elem(struct bpf_map *map, const void *key)

从 sockmap 或 sockhash 中删除套接字条目。

成功时返回 0,失败时返回负错误码。

用户空间

bpf_map_update_elem()

int bpf_map_update_elem(int fd, const void *key, const void *value, __u64 flags)

Sockmap 条目可以使用 bpf_map_update_elem() 函数添加或更新。key 参数是 sockmap 数组的索引值。而 value 参数是该套接字的 FD 值。

在底层,sockmap 更新函数使用套接字 FD 值来检索关联的套接字及其附加的 psock。

flags 参数可以是以下之一

  • BPF_ANY: 创建新元素或更新现有元素。

  • BPF_NOEXIST: 仅在元素不存在时创建新元素。

  • BPF_EXIST: 更新现有元素。

bpf_map_lookup_elem()

int bpf_map_lookup_elem(int fd, const void *key, void *value)

Sockmap 条目可以使用 bpf_map_lookup_elem() 函数检索。

注意

返回的条目是套接字 cookie 而不是套接字本身。

bpf_map_delete_elem()

int bpf_map_delete_elem(int fd, const void *key)

Sockmap 条目可以使用 bpf_map_delete_elem() 函数删除。

成功时返回 0,失败时返回负错误码。

示例

内核 BPF

可以在以下位置找到使用 sockmap API 的几个示例:

以下代码片段展示了如何声明一个 sockmap。

struct {
        __uint(type, BPF_MAP_TYPE_SOCKMAP);
        __uint(max_entries, 1);
        __type(key, __u32);
        __type(value, __u64);
} sock_map_rx SEC(".maps");

以下代码片段展示了一个示例解析器程序。

SEC("sk_skb/stream_parser")
int bpf_prog_parser(struct __sk_buff *skb)
{
        return skb->len;
}

以下代码片段展示了一个简单的判决程序,它与 sockmap 交互,根据本地端口将流量重定向到另一个套接字。

SEC("sk_skb/stream_verdict")
int bpf_prog_verdict(struct __sk_buff *skb)
{
        __u32 lport = skb->local_port;
        __u32 idx = 0;

        if (lport == 10000)
                return bpf_sk_redirect_map(skb, &sock_map_rx, idx, 0);

        return SK_PASS;
}

以下代码片段展示了如何声明一个 sockhash 映射。

struct socket_key {
        __u32 src_ip;
        __u32 dst_ip;
        __u32 src_port;
        __u32 dst_port;
};

struct {
        __uint(type, BPF_MAP_TYPE_SOCKHASH);
        __uint(max_entries, 1);
        __type(key, struct socket_key);
        __type(value, __u64);
} sock_hash_rx SEC(".maps");

以下代码片段展示了一个简单的判决程序,它与 sockhash 交互,根据一些 skb 参数的哈希值将流量重定向到另一个套接字。

static inline
void extract_socket_key(struct __sk_buff *skb, struct socket_key *key)
{
        key->src_ip = skb->remote_ip4;
        key->dst_ip = skb->local_ip4;
        key->src_port = skb->remote_port >> 16;
        key->dst_port = (bpf_htonl(skb->local_port)) >> 16;
}

SEC("sk_skb/stream_verdict")
int bpf_prog_verdict(struct __sk_buff *skb)
{
        struct socket_key key;

        extract_socket_key(skb, &key);

        return bpf_sk_redirect_hash(skb, &sock_hash_rx, &key, 0);
}

用户空间

可以在以下位置找到使用 sockmap API 的几个示例:

以下代码示例展示了如何创建一个 sockmap,附加解析器和判决程序,以及添加一个套接字条目。

int create_sample_sockmap(int sock, int parse_prog_fd, int verdict_prog_fd)
{
        int index = 0;
        int map, err;

        map = bpf_map_create(BPF_MAP_TYPE_SOCKMAP, NULL, sizeof(int), sizeof(int), 1, NULL);
        if (map < 0) {
                fprintf(stderr, "Failed to create sockmap: %s\n", strerror(errno));
                return -1;
        }

        err = bpf_prog_attach(parse_prog_fd, map, BPF_SK_SKB_STREAM_PARSER, 0);
        if (err){
                fprintf(stderr, "Failed to attach_parser_prog_to_map: %s\n", strerror(errno));
                goto out;
        }

        err = bpf_prog_attach(verdict_prog_fd, map, BPF_SK_SKB_STREAM_VERDICT, 0);
        if (err){
                fprintf(stderr, "Failed to attach_verdict_prog_to_map: %s\n", strerror(errno));
                goto out;
        }

        err = bpf_map_update_elem(map, &index, &sock, BPF_NOEXIST);
        if (err) {
                fprintf(stderr, "Failed to update sockmap: %s\n", strerror(errno));
                goto out;
        }

out:
        close(map);
        return err;
}

参考文献