學習 eBPF 系列 7 - tc & BCC neighbor_sharing
接續前一篇主題 XDP
,今天我們要繼續來聊聊 eBPF 在 linux netowrk data path 上的另外一個進入點 tc
,並同樣以 bcc 的 neighbor_sharing (opens new window) 作為範例。
# Linux tc 介紹
首先我們要先聊聊 tc
是什麼東西。Traffic Control (tc) 是 linux kernel 網路系統裡面和 netfilter/iptables 同等重要的一個組件。不過 netfilter 主要著重在 packet mangling (封包修改) 和 filter (過濾)。而 tc 的重點是在調控流量,提供限速、整形等功能。
tc 的工作時機點分成 ingress tc
和 egress tc
,以 ingress tc
來說,他發生在 skb allocation 之後,進入 netfilter 之前。 ingress tc
主要用於輸入流量控制, egress tc
則用於流量優先級、QoS 的功能。在傳統使用上,tc 更主要是用在 egress tc
, ingress tc
本身有比較大的功能限制。
在 tc
裡面有三個主要的概念, qdisc
、 class
和 filter (classifier)
。
tc 的基礎是 queue,封包要進出主機時,會先進入 queue,根據特定的策略重新排序、刪除、延遲後再交給網卡送出,或 netfilter 等系統收入。
qdisc
是套用在這個 queue 上面的策略規則。下列舉例一部份:
- 最基本的策略規則是 pfifo,就是一個簡單的 FIFO queue,只能設定 queue 的可儲存的封包大小和封包個數。
- 更進階的如 pfifo_fast,會根據 ip 封包內的
ToS
欄位將封包分成三個優先度,每個優先度內是走 FIFO 規則,但是會優先清空高優先度的封包。 - sfq (opens new window) 則是會根據 tcp/udp/ip 欄位 hash 的結果區分出不同的連線,將不同連線的封包放入獨立的 bucket 內,然後 bucket 間使用輪尋的方式,來讓不同連線均等的輸出。
- ingress 是專門用在 ingress tc 的 qdisc 上面的 qdisc 都歸為 classless QDisc,因為我們不能透過自定義的方式對流量進行分類,提供不同的策略。
與 classless 相反的是 classful qdisc,在 classful qdisc 內,我們可以以定義出多個 class
,針對不同的 class 設定不同的限速策略等規則。也可以將多個 class 附屬在另外一個 class 下,讓子 class 共用一個父 class 的最大總限速規則,但是子分類又獨立有限速規則等等。
而要對流量進行分類就會用到 filter
, 對於某個 qdisc (classless/classful 皆可) 或著父 class 上的封包,如果滿足 filter 的條件,就可以把封包歸到某個 class 上。除了歸類到某個 class 上,filter 也可以設置為執行某個 action,包括丟棄封包、複製封包流量到另外一個網路介面上之類的…
對於 qdisc 和 class 在建立時需指定或自動分配一個在網卡上唯一的 handle 作為識別 id,格式是 <major>:<minor>
(數字),對於 qdisc 來說只有 major 的部分 <major>:
,對 class 來說 major 必須與對應 qdisc 相同。
另外在 egress pipeline 可以有多個 qdisc,其中一個作為 root,其他的藉由 filter 從 root qdisc dispatch 過去,所以需要有 major 這個欄位。
在 linux 上面主要透過 tc
這個指令來設置 qdisc
、 class
和 filter
。
# 添加 eth0 egress 的 root qdisc,類型是 htb,後面是 htb 的參數
tc qdisc add dev enp0s3 root handle 1: htb default 30
# 添加 eth 的 ingress qdisc
tc qdisc add dev enp0s3 ingress
# 設置一個 class,速度上下限都是 20mbps,附屬於 eth0 的 root qdisc (1:) 下
tc class add dev enp0s3 partent 1: classid 1:1 htb rate 20mbit ceil 20mbit
# 當封包為 ip, dst port 80 時分類到上述分類
tc filter add dev enp0s3 protocol ip parent 1: prio 1 u32 match ip dport 80 0xffff flowid 1:1
2
3
4
5
6
7
8
9
10
# 查看 egress filter
tc filter show dev eth0
# 查看 ingress filter
tc filter show dev eth0 ingress
2
3
4
5
# eBPF 與 tc
eBPF 在 tc 系統裡面是在 filter
的部分作用,並可分成兩種模式,classifier (BPF_PROG_TYPE_SCHED_CLS) 和 action (BPF_PROG_TYPE_SCHED_ACT)。
- classifier: 前者分析封包後,決定是否 match,並可以將封包分類給透過 tc 指令預設的 classid 或著重新指定 classid。
- 0: mismatch
- 1: match, 使用 filter 預設的 classid
- 直接回傳一個 classid
- action: 作為該
filter
的 action,當 tc 設置的 filter 規則 match 後,呼叫 eBPF 程式決定 action 是 drop (2), 執行預設 action (-1) 等。下列是 action 的完整定義
#define TC_ACT_UNSPEC (-1)
#define TC_ACT_OK 0
#define TC_ACT_RECLASSIFY 1
#define TC_ACT_SHOT 2
#define TC_ACT_PIPE 3
#define TC_ACT_STOLEN 4
#define TC_ACT_QUEUED 5
#define TC_ACT_REPEAT 6
#define TC_ACT_REDIRECT 7
#define TC_ACT_JUMP 0x10000000
2
3
4
5
6
7
8
9
10
# BCC neighbor_sharing
# 介紹
這次要看的是 examples/networking/neighbor_sharing
。(原始碼 (opens new window))
這次的 eBPF 程式會提供 QoS 的服務,對經過某張網卡的針對往特定的 IP 提供不同的限速群組。
/------------\ |
neigh1 --|->->->->->->->-| | |
neigh2 --|->->->->->->->-| <-128kb-| /------\ |
neigh3 --|->->->->->->->-| | wan0 | wan | |
| ^ | br100 |-<-<-<--| sim | |
| clsfy_neigh () | | ^ \------/ |
lan1 ----|->->->->->->->-| <--1Mb--| | |
lan2 ----|->->->->->->->-| | classify_wan () |
^ \------------/ |
pass () |
2
3
4
5
6
7
8
9
10
上圖是 neighbor_sharing 自帶的網路拓譜圖,neight1-3, lan1-2, wan0 是獨立的 network namespace 擁有獨立的 IP,neighbor_sharing 會在 wansim 到 br100 的介面上建立 ingress tc
,針對 neigh1-3 的 IP 提供總共 128kb/s 的網路速度,對其他 IP 提供總共 1024kb/s 的網路速度。
首先在測試之前要先安裝 pyroute2 和 netperf,前者是 python 接接 tc 指令的 library,後者是用來測試網速的工具。另外要記得設置防火牆規則不然 br100 不會轉發封包
pip3 install pyroute2
apt install netperf
iptables -P FORWARD ACCEPT
sysctl -w net.ipv4.ip_forward=1
2
3
4
neight1-3 會被分配 172.16.1.100-102 的 IP, lan 則是 172.16.1.150-151。
sudo ip netns exec wan0 netperf -H 172.16.1.100 -l 2 -k
MIGRATED TCP STREAM TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 172.16.1.100 () port 0 AF_INET : demo
Recv Send Send
Socket Socket Message Elapsed
Size Size Size Time Throughput
bytes bytes bytes secs. 10^6bits/sec
131072 16384 16384 6.00 161.45
2
3
4
5
6
7
8
透過 netperf 可以測出來到 neight1 的封包流量被限制在約 161.45 kbits/sec。
ip netns exec wan0 netperf -H 172.16.1.150 -l 2 -f k
MIGRATED TCP STREAM TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 172.16.1.150 () port 0 AF_INET : demo
Recv Send Send
Socket Socket Message Elapsed
Size Size Size Time Throughput
bytes bytes bytes secs. 10^3bits/sec
131072 16384 16384 2.67 1065.83
2
3
4
5
6
7
8
而到 lan1 大約是 1065.83kbits/sec,接近預先設置的規則。
# python 實作
這次會先看 python 的程式碼,由於這次的程式碼包含大量用來建立測試環境的部分,所以會跳過只看相關的內容。
b = BPF (src_file="tc_neighbor_sharing.c", debug=0)
wan_fn = b.load_func ("classify_wan", BPF.SCHED_CLS)
pass_fn = b.load_func ("pass", BPF.SCHED_CLS)
neighbor_fn = b.load_func ("classify_neighbor", BPF.SCHED_CLS)
2
3
4
5
首先這次的 eBPF 程式包含三個部分,因此會分別載入,並且全部都是 classifier (BPF_PROG_TYPE_SCHED_CLS)
ipr.tc ("add", "ingress", wan_if ["index"], "ffff:")
ipr.tc ("add-filter", "bpf", wan_if ["index"], ":1", fd=wan_fn.fd,
prio=1, name=wan_fn.name, parent="ffff:", action="drop",
classid=1, rate="128kbit", burst=1024 * 32, mtu=16 * 1024)
ipr.tc ("add-filter", "bpf", wan_if ["index"], ":2", fd=pass_fn.fd,
prio=2, name=pass_fn.name, parent="ffff:", action="drop",
classid=2, rate="1024kbit", burst=1024 * 32, mtu=16 * 1024)
2
3
4
5
6
7
接著會建立 wan_if 的 ingress qdisc (wan_if 是 wan0 接到 br100 的介面),並且會 ingress qdisc 下建立兩條 filter,首先它的 type 指定為 bpf 並透過 fd=wan_fn.fd
選定 eBPF program,所以會交由 eBPF classifier 來決定是不是要 match。
classifier match 後就會執行下屬的 policing action,跟 classid 無關,且在這次的範例中並不存在 class,所以 classid 其實是無意義的,不一定要設置。
後半段 action="drop", rate="128kbit", burst=1024 * 32, mtu=16 * 1024
定義了一條 policing action,只有當封包滿足 policy 條件時才會觸發具體的 action,這邊指定是流量超出 128kbit 時執行 drop,也就達到了限制 neigh 流量的效果。
第二條同理,match pass_fn 並且流量到達 1024kbit 時執行 drop,由於 pass_fn 顧名思義是無條件 match 的意思,所以等價於所有非 neigh 的流量共用這一條的 1024kbit 流量限制。
因此總結來說,eBPF 程式 wan_fn 透過某種方式判斷封包是否是往 neigh 的 ip,是的話就 match 第一條 filter 執行 policing action 來限流,不然就 match 第二條 filter 來做限流。
ret = self._create_ns ("neighbor% d" % i, ipaddr=ipaddr,
fn=neighbor_fn, cmd=cmd)
2
接著就會看到,在建立 neigh1-3 的 namespace 時,attach 了 neighbor_fn 到網卡上,因此就很好理解了 neighbor_fn 監聽了從 neigh 發出的封包,解析拿到 neigh 的 IP 後,透過 map share 給 wan_fn,讓 wan_fn 可以根據 ip 決定要不要 match 第一條 policing action。
# eBPF 實作
到這裡其實就分析出整個程式的執行邏輯了,我們接續來看看 neighbor_sharing 的 eBPF 程式,這次的 eBPF 程式分成三個部分,首先是接在每個 neigh ingress 方向的 classify_neighbor,接著是接在 wan0 ingress 方向的 classify_wan 和 pass。
前面說到出來 classify_neighbor
要做的事情就是紀錄 neigh1-3 的 IP,提供給 classify_wan
判斷是否要 match 封包,執行 128kbits 的流量限制。
struct ipkey {
u32 client_ip;
};
BPF_HASH (learned_ips, struct ipkey, int, 1024);
2
3
4
5
首先定義了一個 hash map 用 key 來儲存所有 neigh 的 IP
int classify_neighbor(struct __sk_buff *skb) {
u8 *cursor = 0;
ethernet: {
struct ethernet_t *ethernet = cursor_advance (cursor, sizeof(*ethernet));
switch (ethernet->type) {
case ETH_P_IP: goto ip;
default: goto EOP;
}
}
ip: {
struct ip_t *ip = cursor_advance (cursor, sizeof(*ip));
u32 sip = ip->src;
struct ipkey key = {.client_ip=sip};
int val = 1;
learned_ips.insert (&key, &val);
goto EOP;
}
EOP:
return 1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
接著 classify_neighbor
就會用 cursor 解析出 source ip,將其作為 hash map 的 key 放到 learned_ips 裡面,value 則都設為 1。不論如何都會 return 1 放行封包。雖然其實這是 neighbor ingress 方向上唯一的一條 filter,所以不論回傳值為多少其實都可以,不影響執行結果。
這邊就要提到第一次學習 tc 還有 classifier 時會感到很困惑的地方了,首先 classifier 的回傳值 0 表示 mismatch, 1 表示 match 並轉移到預設的 class,其餘回傳值表示直接指定 classid 為回傳的數值。接著不論 classid 是多少,都會執行 filter 上面綁定的 action。在這次的範例中,所有的 filter 其實都不存在任何的 class,因此 return 值唯一的意義是控制是否要執行 action。這邊 classify_neighbor 綁定的 action 是 ok,表示放行封包的意思
int classify_wan(struct __sk_buff *skb) {
u8 *cursor = 0;
ethernet: {
struct ethernet_t *ethernet = cursor_advance (cursor, sizeof(*ethernet));
switch (ethernet->type) {
case ETH_P_IP: goto ip;
default: goto EOP;
}
}
ip: {
struct ip_t *ip = cursor_advance (cursor, sizeof(*ip));
u32 dip = ip->dst;
struct ipkey key = {.client_ip=dip};
int *val = learned_ips.lookup (&key);
if (val)
return *val;
goto EOP;
}
EOP:
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
接著看到 classify_wan
,他會提取封包的 dst ip address,並嘗試搜尋 learned_ips,如果找的到就表示這個是 neighbor 的 ip,回傳 map 對應的 value,前面提到所有的 value 都會設置為 1,因此表示 match 的意思,不然就跳轉到 EOP 回傳 0,表示 mismatch。同樣由於這邊不存在 class,因此 value 只要是非 0 即可,只是用來 match 執行 policing action。
int pass(struct __sk_buff *skb) {
return 1;
}
2
3
最後的 pass
其實就是一條無條件回傳 1 表示 match,來執行 wan0 方向第二條 1024kbits/sec 的限流政策用的。
# tc 與 XDP 比較
在 eBPF 裡面,XDP 和 TC 兩個功能常常被拿來一起討輪,前面有提到 eBPF 可以做為 tc actions 使用來達到封包過濾之類的效果,雖然實行效果上是比不上 XDP 的,但是 tc ingress 的 eBPF hook point 也在 kernel data path 的最早期,因此也能夠提供不錯的效能,加上 tc ebpf program 的 context 是 sk_buff
,相較於 xdp_buff
,可以直接透過 __sk_buff
取得和修改更多的 meta data,加上 tc 在 ingress 和 egress 方向都有 hook point,不像 XDP 只能作用在 ingress 方向,且 tc 完全不需要驅動支援即可工作,因此 tc 在使用彈性和靈活度上是比 XDP 更占優的。
不過 tc 其實也有提供 offload 的功能,將 eBPF 程式 offload 到網卡上面執行。
# Direct action
然而由於 tc 的 hook point 分成 classifier 和 action 因此無法透過單一個 eBPF 程式做到 match-action 的效果,然而大多數時候 eBPF tc 程式的開發並不是要利用 tc 系統的功能做限速等功能,而是要利用 tc 在 kernel path 極早期這點做 packet mangling 和 filter 等事項,再加上 tc 系統的使用學習難度相對高,因此 eBPC 在 tc 後引入了 direct-action (opens new window) 和 clsact (opens new window) 這兩個功能。
首先介紹 direct-action (da),這個是在 classifier (BPF_PROG_TYPE_SCHED_CLS) 可啟用的一個選項,如果啟用 da,classifier 的回傳值就變成是 action,和 BPF_PROG_TYPE_SCHED_ACT 相同,而原本的 classid 改成設置__skb_buff->tc_classid 來傳輸。
在 kernel code 內使用 prog->exts_integrated 標示是否啟用 da 功能
透過 da 可以透過單一個 eBPF 程式完成 classifier 和 action 的功能,降低了 tc hook point 對原本 tc 系統框架的依賴,能夠透過 eBPF 程式簡潔的完成功能。
在 da 的使用上可以參考 bcc 的範例 examples/networking/tc_perf_event.py
,使用上與普通的 classifer 幾乎無異,只要在載入時 ipr.tc ("add-filter","bpf", me,":1", fd=fn.fd, ... ,direct_action=True)
加上 direct_action 選項即可。
透過 tc 指令查看時也可以看到 direct-action
字樣。
tc filter show dev t1a
filter parent 1: protocol all pref 49152 bpf chain 0
filter parent 1: protocol all pref 49152 bpf chain 0 handle 0x1 flowid :1 hello direct-action not_in_hw id 308 tag 57cd311f2e27366b jited
action order 1: gact action pass
random type none pass val 0
index 2 ref 1 bind 1
2
3
4
5
6
# clsact
後來 tc 加入了 clsact,clsact 是一個專為 eBPF 設計的偽 qdisc。首先 clsact 同時作用在 ingress 和 egress 方向,也進一步簡化了 ebpf 程式的掛載。
tc qdisc add dev em1 clsact
tc filter add dev em1 ingress bpf da obj tc-example.o sec ingress
tc filter add dev em1 egress bpf da obj tc-example.o sec egress
2
3
同時 clsact 工作在真的 qdisc 本身的 lock 之前,因此可以避免 lock 的開銷,預先完成比較複雜繁重的封包分類,在進入到真的 queue filter 時只根據更簡單的欄位 (如 tc_index) 做分類。另外 da 本來只能使用在 ingress 方向,透過 clsact,da 可以工作在 egress 方向。
關於 eBPF tc 的部分就大致上介紹到這裡,對於 tc 這個子系統相對來說是真的蠻陌生的,因此介紹這個部分的確是有比較大的難度和說不清楚的地方。