BPF_MAP_TYPE_SOCKMAP 和 BPF_MAP_TYPE_SOCKHASH¶
注意
BPF_MAP_TYPE_SOCKMAP在内核版本 4.14 中引入BPF_MAP_TYPE_SOCKHASH在内核版本 4.18 中引入
BPF_MAP_TYPE_SOCKMAP 和 BPF_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_verdict 和 skb_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->data 和 msg->data_end 分别设置为 msg 中 start 和 end 字节偏移量。
如果类型为 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;
}