前言
本文翻译自 2020 年 Quentin Monnet 的一篇英文博客:Understanding tc “direct action” mode for BPF[1]。
Quentin Monnet 是 Cilium 开发者之一。
如作者所说,da
模式不仅是使用 tc ebpf 程序的推荐方式,而且(据他所知,截至本文 写作时)也是唯一方式。所以,很多人一直在使用它(包括通过 Cilium 间接使用),却没 有深挖过它到底是什么意思 —— 这样用就行了。
本文结合 tc/ebpf 开发史,介绍了 da
模式的来龙去脉,并给出了例子、内核及 iproute2/tc 中的实现。
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
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[2] 及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 是一个强大但复杂的框架(且文档[3]较少)。它的**几个核心概念**:
组合以上概念,下面是对某个网络设备上的流量进行分类和限速时,所需完成的大致步骤:
0
:表示 mismatch。如果后面还有其他 filters,则**继续对这个包应用下一个 filter**。-1
:表示这个 filter 上配置的**默认 classid**。下面是一个例子,(参考了 HTB shaper 文档[4]):
# 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 后面。例如下面这个场景,
以上就是 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。后面介绍内核实现时会看到。
clsact
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[7]和 ?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[8], 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[9], 添加了对 tc da|direct-action
的支持。
本文介绍了 tc ebpf 中 da
模式的来龙去脉,并给出了详细的使用案例。截至本文发表时,da
模式不仅是使用 tc ebpf 的推荐方式,而且 据我所知也是唯一方式。
[1]
Understanding tc “direct action” mode for BPF: https://qmonnet.github.io/whirl-offload/2020/04/11/tc-bpf-direct-action/
[2]
Cilium Guide: http://docs.cilium.io/en/latest/bpf/#tc-traffic-control
[3]
文档: https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/#about-tc
[4]
HTB shaper 文档: http://luxik.cdi.cz/~devik/qos/htb/manual/userg.htm
[5]
include/uapi/linux/pkt_cls.h: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/pkt_cls.h
[6]
BPF and XDP Reference Guide from Cilium: http://docs.cilium.io/en/latest/bpf/#tc-traffic-control
[7]
commit log: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=1f211a1b929c804100e138c5d3d656992cfd5622
[8]
045efa82ff56: https://github.com/torvalds/linux/commit/045efa82ff56
[9]
faa8a463002f: https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/commit/?id=faa8a463002f
[10]
On getting tc classifier fully programmable with cls_bpf: http://www.netdevconf.org/1.1/proceedings/slides/borkmann-tc-classifier-cls-bpf.pdf
[11]
045efa82ff56: https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=045efa82ff563cd4e656ca1c2e354fa5bf6bbda4
[12]
1f211a1b929c: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=1f211a1b929c804100e138c5d3d656992cfd5622
[13]
faa8a463002f: https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/commit/?id=faa8a463002fb9a365054dd333556e0aaa022759
[14]
8f9afdd53156: https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/commit/?id=8f9afdd531560c1534be44424669add2e19deeec
原文链接:https://arthurchiao.art/blog/understanding-tc-da-mode-zh/