ebpf:在 lo 接口上的套接字过滤器程序中丢弃 ICMP 数据包
ebpf: drop ICMP packet in socket filter program on lo interface
考虑 BPF_PROG_TYPE_SOCKET_FILTER
类型的非常简单的 ebpf
代码:
struct bpf_insn prog[] = {
BPF_MOV64_IMM(BPF_REG_0, -1),
BPF_EXIT_INSN(),
};
下面来自 net/core/filter.c
和 net/core/sock/c
的代码片段显示了如何调用过滤器:
static inline int pskb_trim(struct sk_buff *skb, unsigned int len)
{
return (len < skb->len) ? __pskb_trim(skb, len) : 0;
}
...
int sk_filter_trim_cap(struct sock *sk, struct sk_buff *skb, unsigned int cap)
{
int err;
...
if (filter) {
pkt_len = bpf_prog_run_save_cb(filter->prog, skb);
skb->sk = save_sk;
err = pkt_len ? pskb_trim(skb, max(cap, pkt_len)) : -EPERM;
}
...
return err;
}
...
static inline int sk_filter(struct sock *sk, struct sk_buff *skb)
{
return sk_filter_trim_cap(sk, skb, 1);
}
最终 sk_filter()
将被 sock_queue_rcv_skb()
调用,即如果 sk_filter()
returns 0,到达套接字的数据包将被过滤器处理并排队。
如果我正确理解这段代码,在这种情况下(return 代码 0xffffffff)将导致数据包被丢弃。但是,我的简单 ebpf 代码在附加到 AF_PACKET
原始套接字(绑定到 lo
接口)时不会丢弃通过环回接口发送的 icmp
数据包。它与环回接口上的 eBPF 或 ICMP 行为有什么关系吗?
更新
正如 pchaigno
所指出的,套接字过滤器程序处理 份 的数据包。在我的例子中,ebpf 应用程序基本上创建了一个 tap
套接字(AF_PACKET
原始套接字),它将在 在 将它们传递到协议层之前传递入口数据包.我在内核代码中做了一些调查,发现接收到的数据包最终会到达 __netif_receive_skb_core() 函数,该函数执行以下操作:
list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
这会将数据包传递给 AF_PACKET
处理程序,最终将 运行 ebpf 过滤。
作为一种确认 ebpf
过滤器实际上过滤原始 AF_PACKET 套接字的方法,可以按如下方式转储统计信息:
struct tpacket_stats stats;
...
len = sizeof(stats);
err = getsockopt(sock, SOL_PACKET, PACKET_STATISTICS, &stats, &len);
此统计信息将指示过滤器行为。
反之:如果返回 0,它将丢弃数据包。来自代码:
* sk_filter_trim_cap - run a packet through a socket filter
* [...]
*
* Run the eBPF program and then cut skb->data to correct size returned by
* the program. If pkt_len is 0 we toss packet. If skb->len is smaller
* than pkt_len we keep whole skb->data. [...]
pchaigno 的回答是正确的,内核代码始终是真实的最终来源,但在那种情况下,我也会重定向到套接字的手册页,man 7 socket
。这是描述将 BPF 过滤器附加到套接字的选项的地方,它说:
SO_ATTACH_FILTER (since Linux 2.2), SO_ATTACH_BPF (since Linux
3.19)
Attach a classic BPF (SO_ATTACH_FILTER) or an extended BPF
(SO_ATTACH_BPF) program to the socket for use as a filter
of incoming packets. A packet will be dropped if the
filter program returns zero. If the filter program
returns a nonzero value which is less than the packet's
data length, the packet will be truncated to the length
returned. If the value returned by the filter is greater
than or equal to the packet's data length, the packet is
allowed to proceed unmodified.
所以:
0
丢弃数据包(“截断为长度 0”)。
- 小值截断数据包(到与该值对应的长度)。
- 大值传包
这对 cBPF 和 eBPF 都有效。
套接字过滤器 BPF 接收数据包的副本;因此,此 BPF 过滤器会丢弃或截断数据包的副本,而不是原始数据包。原始数据包通过内核不受过滤器影响。
套接字过滤器 eBPF 程序的 return 值实际上只影响链接在其后或插入在其自身之后的 BPF 过滤器。
如果你想要 eBPF 功能并且需要丢弃数据包,你应该选择不同的内核挂钩点,例如Netfilter Hooks、流量控制等
流量控制是ingress/egress,过滤器附加到单个网络接口,以流量控制BPF(tc-eBPF)的形式支持BPF。 Netfilter Hooks 是 IP-level,支持 Iptables、Nftables、Nfqueue,并且易于在内核模块中使用。与在超早期 (ingress/egress) 阶段轻松修改或丢弃数据包的流量控制相比,BPF 支持不如流量控制好。 Iptables-extensions BPF (xt-bpf) 可以丢弃数据包,将数据包信息存储在地图中,但数据包修改是有限的。
但是对于 ICMP 数据包丢弃,单个 Iptables 规则就足够了。
考虑 BPF_PROG_TYPE_SOCKET_FILTER
类型的非常简单的 ebpf
代码:
struct bpf_insn prog[] = {
BPF_MOV64_IMM(BPF_REG_0, -1),
BPF_EXIT_INSN(),
};
下面来自 net/core/filter.c
和 net/core/sock/c
的代码片段显示了如何调用过滤器:
static inline int pskb_trim(struct sk_buff *skb, unsigned int len)
{
return (len < skb->len) ? __pskb_trim(skb, len) : 0;
}
...
int sk_filter_trim_cap(struct sock *sk, struct sk_buff *skb, unsigned int cap)
{
int err;
...
if (filter) {
pkt_len = bpf_prog_run_save_cb(filter->prog, skb);
skb->sk = save_sk;
err = pkt_len ? pskb_trim(skb, max(cap, pkt_len)) : -EPERM;
}
...
return err;
}
...
static inline int sk_filter(struct sock *sk, struct sk_buff *skb)
{
return sk_filter_trim_cap(sk, skb, 1);
}
最终 sk_filter()
将被 sock_queue_rcv_skb()
调用,即如果 sk_filter()
returns 0,到达套接字的数据包将被过滤器处理并排队。
如果我正确理解这段代码,在这种情况下(return 代码 0xffffffff)将导致数据包被丢弃。但是,我的简单 ebpf 代码在附加到 AF_PACKET
原始套接字(绑定到 lo
接口)时不会丢弃通过环回接口发送的 icmp
数据包。它与环回接口上的 eBPF 或 ICMP 行为有什么关系吗?
更新
正如 pchaigno
所指出的,套接字过滤器程序处理 份 的数据包。在我的例子中,ebpf 应用程序基本上创建了一个 tap
套接字(AF_PACKET
原始套接字),它将在 在 将它们传递到协议层之前传递入口数据包.我在内核代码中做了一些调查,发现接收到的数据包最终会到达 __netif_receive_skb_core() 函数,该函数执行以下操作:
list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
这会将数据包传递给 AF_PACKET
处理程序,最终将 运行 ebpf 过滤。
作为一种确认 ebpf
过滤器实际上过滤原始 AF_PACKET 套接字的方法,可以按如下方式转储统计信息:
struct tpacket_stats stats;
...
len = sizeof(stats);
err = getsockopt(sock, SOL_PACKET, PACKET_STATISTICS, &stats, &len);
此统计信息将指示过滤器行为。
反之:如果返回 0,它将丢弃数据包。来自代码:
* sk_filter_trim_cap - run a packet through a socket filter
* [...]
*
* Run the eBPF program and then cut skb->data to correct size returned by
* the program. If pkt_len is 0 we toss packet. If skb->len is smaller
* than pkt_len we keep whole skb->data. [...]
pchaigno 的回答是正确的,内核代码始终是真实的最终来源,但在那种情况下,我也会重定向到套接字的手册页,man 7 socket
。这是描述将 BPF 过滤器附加到套接字的选项的地方,它说:
SO_ATTACH_FILTER (since Linux 2.2), SO_ATTACH_BPF (since Linux
3.19)
Attach a classic BPF (SO_ATTACH_FILTER) or an extended BPF
(SO_ATTACH_BPF) program to the socket for use as a filter
of incoming packets. A packet will be dropped if the
filter program returns zero. If the filter program
returns a nonzero value which is less than the packet's
data length, the packet will be truncated to the length
returned. If the value returned by the filter is greater
than or equal to the packet's data length, the packet is
allowed to proceed unmodified.
所以:
0
丢弃数据包(“截断为长度 0”)。- 小值截断数据包(到与该值对应的长度)。
- 大值传包
这对 cBPF 和 eBPF 都有效。
套接字过滤器 BPF 接收数据包的副本;因此,此 BPF 过滤器会丢弃或截断数据包的副本,而不是原始数据包。原始数据包通过内核不受过滤器影响。
套接字过滤器 eBPF 程序的 return 值实际上只影响链接在其后或插入在其自身之后的 BPF 过滤器。
如果你想要 eBPF 功能并且需要丢弃数据包,你应该选择不同的内核挂钩点,例如Netfilter Hooks、流量控制等
流量控制是ingress/egress,过滤器附加到单个网络接口,以流量控制BPF(tc-eBPF)的形式支持BPF。 Netfilter Hooks 是 IP-level,支持 Iptables、Nftables、Nfqueue,并且易于在内核模块中使用。与在超早期 (ingress/egress) 阶段轻松修改或丢弃数据包的流量控制相比,BPF 支持不如流量控制好。 Iptables-extensions BPF (xt-bpf) 可以丢弃数据包,将数据包信息存储在地图中,但数据包修改是有限的。
但是对于 ICMP 数据包丢弃,单个 Iptables 规则就足够了。