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 辅助函数 bpf_sk_redirect_map(), bpf_sk_redirect_hash(), bpf_msg_redirect_map()bpf_msg_redirect_hash()

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)

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

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 级别实现策略的程序中。如果允许 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)

对于套接字策略,防止在累积了 bytes 数量之前执行消息 msg 的判决 BPF 程序。

当需要在分配判决之前需要特定数量的字节时,即使数据跨越多个 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 字节偏移量。

如果在 msg 上运行 BPF_PROG_TYPE_SK_MSG 类型的程序,它只能解析 (data, data_end) 指针已经消耗的数据。对于 sendmsg() 钩子,这很可能是第一个散列表元素。但是对于依赖 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)

可以使用 bpf_map_update_elem() 函数添加或更新 Sockmap 条目。 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)

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

注意

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

bpf_map_delete_elem()

int bpf_map_delete_elem(int fd, const void *key)

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

成功时返回 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;
}

参考