Published at 2021-02-21 | Last Update 2021-02-21
本文翻译自 2020 年 Quentin Monnet 的一篇英文博客: Understanding tc “direct action” mode for BPF。
Quentin Monnet 是 Cilium 开发者之一。
如作者所说,da
模式不仅是使用 tc ebpf 程序的推荐方式,而且(据他所知,截至本文
写作时)也是唯一方式。所以,很多人一直在使用它(包括通过 Cilium 间接使用),却没
有深挖过它到底是什么意思 —— 这样用就行了。
本文结合 tc/ebpf 开发史,介绍了 da
模式的来龙去脉,并给出了例子、内核及 iproute2/tc 中的实现。
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
clsact
Linux 的流量控制子系统(Traffic Control, TC)已经在内核中存在多年,并仍处于活跃开发之中。
Kernel 4.1
的一个重要变化是:添加了一些新的 hook,并支持将 eBPF 程序作为
tc classifier(也称为 filter) 或 tc action 加载到这些 hook 点。大概六个月之后,
kernel 4.4
发布时,iproute2 引入了一个 direct-action
模式,但关于这个模式的文档甚少。
本文初稿时,除了 commit log 之外,没有关于
direct-action
的其他文档。如今 Cilium Guide 及tc-bpf(8)
中 都有了一些简要描述,说这个模式 “instructs eBPF classifier to not invoke external TC actions, instead use the TC actions return codes (TC_ACT_OK
,TC_ACT_SHOT
etc.) for classifiers.”
在介绍 direct-action
之前,需要先回顾一下 Linux TC 的经典使用场景和使用方式。
流量控制最终是在内核中完成的:tc 模块根据不同算法对网络设备上的流量进行控制
(限速、设置优先级等等)。用户一般通过 iproute2 中的 tc
工具完成配置 —— 这是与
内核 TC 子系统相对应的用户侧工具 —— 二者之间(大部分情况下)通过
Netlink 消息通信。
TC 是一个强大但复杂的框架(且文档较少)。 它的几个核心概念:
组合以上概念,下面是对某个网络设备上的流量进行分类和限速时,所需完成的大致步骤:
为网络设备创建一个 qdisc。
创建流量类别(class),并 attach 到 qdisc。
创建 filter(classifier),并 attach 到 qdisc。
filters 用于对网络设备上的流量进行分类,并将包分发(dispatch)到前面定义的不同 class。
filter 会对每个包进行过滤,返回下列值之一:
0
:表示 mismatch。如果后面还有其他 filters,则继续对这个包应用下一个 filter。-1
:表示这个 filter 上配置的默认 classid。另外,可以给 filter 添加 action。例如,将选中的包丢弃(drop),或者将流量镜像到另一个网络设备等等。
除此之外,qdisc 和 class 还可以循环嵌套,即: class 里加入新 qdisc,然后新 qdisc 里又可以继续添加新 class, 最终形成的是一个以 root qdisc 为根的树。但对于本文接下来的内容,我们不需要了解这么多。
下面是一个例子,(参考了 HTB shaper 文档):
# x:y 格式:
# * x 表示 qdisc, y 表示这个 qdisc 内的某个 class
# * 1: 是 1:0 的简写
#
# "default 11":any traffic that is not otherwise classified will be assigned to class 1:11
$ tc qdisc add dev eth0 root handle 1: htb default 11
$ tc class add dev eth0 parent 1: classid 1:1 htb rate 100kbps ceil 100kbps
$ tc class add dev eth0 parent 1:1 classid 1:10 htb rate 30kbps ceil 100kbps
$ tc class add dev eth0 parent 1:1 classid 1:11 htb rate 10kbps ceil 100kbps
$ tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \
match ip src 1.2.3.4 match ip dport 80 0xffff flowid 1:10
$ tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \
match ip src 1.2.3.4 action drop
以上设置表示以下顺序逻辑:
src_ip==1.2.3.4 && dst_port==80
,则将其送到第一个队列。这个队列对应的 class 目标速率是 30kbps
;否则,src_ip==1.2.3.4
,则将其 drop;10kbps
。有了以上基础,现在可以讨论 eBPF 了。
本质上,eBPF 是一种类汇编语言,能编写运行在内核的、安全的程序。 eBPF 程序能 attach 到内核中的若干 hook 点,其中大部分 hook 点 都是用于包处理(packet processing)和监控(monitoring)目的的。
这些 hook 中有两个与 TC 相关:从内核 4.1 开始,eBPF 程序能作为 tc classifier 或 tc action 附着(attach)到这两个 hook 点。
作为分类器使用时,eBPF 能使处理过程更灵活,甚至还能实现有状态处理,或者与用户 态交互(通过名为 map 的特殊数据结构)。
但这种场景下的 eBPF 程序本质上还是一个分类器,因此返回值与普通分类器并无二致:
0
:mismatch-1
:match,表示当前 filter 的默认 classid用作 action 时,eBPF 程序的返回值
提示系统接下来对这个包执行什么动作(action),下面的内容来自 tc-bpf(2)
:
TC_ACT_UNSPEC (-1)
:使用 tc 的默认 action(与 classifier/filter 返回 -1
时类似)。TC_ACT_OK (0)
:结束处理过程,放行(allows the packet to proceed)。TC_ACT_RECLASSIFY (1)
:从头开始,重新执行分类过程。TC_ACT_SHOT (2)
:丢弃包。TC_ACT_PIPE (3)
:如果有下一个 action,执行之。有了以上基础,现在可以讨论 direct-action 了。
上面看到,
所以,如果要实现”匹配+执行动作“的目的 —— 例如,如果源 IP 是 10.1.1.1
,则 drop 这
个包 —— 就需要两个步骤:一个 classifier 和一个 action,即 classfifier+action
模式。
虽然 eBPF 有一些限制,例如单个程序的指令数是有上限的、只允许有限循环等等,但 它提供了一种数据包处理的强大语言。这带来的结果之一是:对于很多场景,eBPF classifier 已经有足够的能力完成完成任务处理,无需再 attach 额外的 qdisc 或 class 了,对于 tc 层的数据包过滤(pass/drop/etc)场景尤其如此。
所以,为了
针对 eBPF classifier,社区为 TC 引入了一个新的 flag:direct-action
,简写 da
。
这个 flag 用在 filter 的 attach time,告诉系统:
filter(classifier)的返回值应当被解读为 action 类型的返回值
(即前面提到的 TC_ACT_XXX
;本来的话,应当被解读为 classid。)。
这意味着,一个作为 tc classifier 加载的 eBPF 程序,现在可以返回
TC_ACT_SHOT
, TC_ACT_OK
等 tc action 的返回值了。换句话说,现在不需要另一个专门的
tc action 对象来 drop 或 mirror 相应的包了。
direct-action
flag 也是最简单的、最快的,是现在的推荐方式。那么,TC eBPF action 能完成类似功能吗?也就是说,能用 action 模块来完成处理包+返回 “pass” 或 “drop” 吗?答案是不行: actions 并没有直接 attach 到某个 qdisc,它们只能用于包从某个 classifier 出来的地方, 这也就意味着:无论如何都得有个 classifier/filter。
另一个问题:这意味着 TC eBPF actions 毫无用处了吗?也不是。 eBPF action 仍然还可以用在其他 filters 后面。例如下面这个场景,
在这个 filter 后面再加一个 ebpf action(做进一步过滤)
因为 ebpf action 中可以实现逻辑处理,因此可以在这里做额外判断,如果包满 足某些额外的条件,就返回 drop。
以上就是 ebpf action 可以使用的场景之一。但坦白说,我见过的场景都是 eBPF 程序同 时负责 filtering 和返回 action,而不需要额外的 filters。
正常 classifier 返回的是 classid,提示系统接下来应该把包送到哪个 class 做进一步处理。
而现在, tc ebpf classifier direct-action
模式返回的是 action 结果。
这是否意味着 eBPF classifier 丢失了 classid 信息?
答案是:NO,我们仍然可以从其他地方获得这个 classid 信息。传递给 filter 程序
的参数是 struct __skb_buff
,其中有个 tc_classid
字段,存储的就是返回的
classid。后面介绍内核实现时会看到。
direct-action
模式引入内核和 iproute2 之后几个月,
内核 Linux 4.5
添加了一个新的 qdisc 类型: clsact
。
clsact
与 ingress
qdisc 类似,能够以 direct-action
模式 attach eBPF 程序,
其特点是不会执行任何排队(does not perform any queuing)。clsact
是 ingress
的超集,因为它还支持在 egress 上以 direct-action
模式 attach eBPF 程序,而在此之前我们是无法做到这一点的。更多关于 clsact
qdisc 信息见
commit log
和 Cilium Guide。
下面展示如何编写一个 tc ebpf filter (classifier),以及如何编译、加载、附着到内核 。
下面这段程序根据包的大小和协议类型进行处理,可能会 drop、allow 或对包执行其他操 作。
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/pkt_cls.h>
#include <linux/swab.h>
int classifier(struct __sk_buff *skb)
{
void *data_end = (void *)(unsigned long long)skb->data_end;
void *data = (void *)(unsigned long long)skb->data;
struct ethhdr *eth = data;
if (data + sizeof(struct ethhdr) > data_end)
return TC_ACT_SHOT;
if (eth->h_proto == ___constant_swab16(ETH_P_IP))
/*
* Packet processing is not implemented in this sample. Parse
* IPv4 header, possibly push/pop encapsulation headers, update
* header fields, drop or transmit based on network policy,
* collect statistics and store them in a eBPF map...
*/
return process_packet(skb);
else
return TC_ACT_OK;
}
使用 clang/LLVM 将我们的 ebpf filter 程序编译为编译成目标文件:
$ clang -O2 -emit-llvm -c foo.c -o - | \
llc -march=bpf -mcpu=probe -filetype=obj -o foo.o
首先需要创建一个 qdisc(因为 filter 必须 attach 到某个 qdisc):
$ tc qdisc add dev eth0 clsact
然后将我们的 filter 程序 attach 到 qdisc:
$ tc filter add dev eth0 ingress bpf direct-action obj foo.o sec .text
查看:
$ tc filter show dev eth0
$ tc filter show dev eth0 ingress
filter protocol all pref 49152 bpf chain 0
filter protocol all pref 49152 bpf chain 0 handle 0x1 foo.o:[.text] direct-action not_in_hw id 11 tag ebe28a8e9a2e747f
可以看到 foo.o
中的 filter 已经 attach 到 ingress 路径,并且使用了 direct-action
模式。
现在这段对流量进行分类+执行动作(classification and action selection)程序已经开始工作了。
$ tc qdisc del dev eth0 clsact
内核对 direct-action 模式的支持出现在 045efa82ff56, commit log 如下(排版略有调整):
cls_bpf: introduce integrated actions
Often cls_bpf classifier is used with single action drop attached. Optimize this use case and let cls_bpf return both classid and action. For backwards compatibility reasons enable this feature under TCA_BPF_FLAG_ACT_DIRECT flag.
Then more interesting programs like the following are easier to write:
int cls_bpf_prog(struct __sk_buff *skb) { /* classify arp, ip, ipv6 into different traffic classes and drop all other packets */ switch (skb->protocol) { case htons(ETH_P_ARP): skb->tc_classid = 1; break; case htons(ETH_P_IP): skb->tc_classid = 2; break; case htons(ETH_P_IPV6): skb->tc_classid = 3; break; default: return TC_ACT_SHOT; } return TC_ACT_OK; }
尤其值得一提的是下面这段逻辑,
做一点解释:
filter_res = BPF_PROG_RUN(prog->filter, skb);
这个函数执行 eBPF 程序(classifier/filter),并将返回值存到 filter_res,filter_res !=0 && filter_res != -1
,那 res->classid = filter_res;
ret = tcf_exts_exec(skb, &prog->exts, res);
,这会调用到相关的 action 模块,对包执行 actionprog->exts_integrated
为 true
时表示 direct-action
)。此时,
classid
是从 qdisc_skb_cb(skb)->tc_classid
获取的,其中 struct __sk_buff *skb
是传递给 eBPF 程序的上下文ret = cls_bpf_exec_opcode(filter_res);
(而非调用外部 action 模块),然后退出循环相应的 iproute2 commit faa8a463002f,
添加了对 tc da|direct-action
的支持。
本文介绍了 tc ebpf 中 da
模式的来龙去脉,并给出了详细的使用案例。
截至本文发表时,da
模式不仅是使用 tc ebpf 的推荐方式,而且
据我所知也是唯一方式。