深入理解 tc ebpf 的 direct-action (da) 模式(2020)


前言

本文翻譯自 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.”

1 背景知識:Linux 流量控制(tc)子系統

在介紹 direct-action 之前,需要先回顧一下 Linux TC 的經典使用場景和使用方式。

流量控制最終是在內核中完成的:tc 模塊根據不同算法對網絡設備上的流量進行控制 (限速、設置優先級等等)。用戶一般通過 iproute2 中的 tc 工具完成配置 —— 這是與 內核 TC 子系統相對應的用戶側工具 ——二者之間(大部分情況下)通過 Netlink 消息通信

1.1 tc 術語

TC 是一個強大但複雜的框架(且文檔[3]較少)。它的**幾個核心概念**:

  • queueing discipline (qdisc):排隊規則,根據某種算法完成限速、整形等功能
  • class:用戶定義的流量類別
  • classifier (也稱爲 filter):分類器,分類規則
  • action:要對包執行什麼動作

組合以上概念,下面是對某個網絡設備上的流量進行分類和限速時,所需完成的大致步驟:

  1. 爲網絡設備**創建一個 qdisc**。

  • qdisc 是一個 整流器/整形器(shaper), 可以包含多個 class,不同 class 可以應用不同的策略。
  • qdisc 需要附着(attach)到某個網絡接口(network interface),及流量方向(ingress or egress)。
  • 創建流量類別(class),並 attach 到 qdisc。

    • 例如,根據帶寬分類,創建高、中、低三個類別。
  • 創建 filter(classifier),並 attach 到 qdisc。

    filters 用於對網絡設備上的流量進行分類,並將包分發(dispatch)到前面定義的不同 class

    filter 會對每個包進行過濾,返回下列值之一:

    • 0:表示 mismatch。如果後面還有其他 filters,則** 繼續對這個包應用下一個 filter**。
    • -1:表示這個 filter 上配置的** 默認 classid**。
    • 其他值: 表示一個 classid。系統接下來應該將包送往這個指定的 class。可以看到,通過這種方式可以實現非線性分類(non-linear classification)。
  • 另外,可以給 filter 添加 action。例如,將選中的包丟棄(drop),或者將流量鏡像到另一個網絡設備等等。

  • 除此之外,qdisc 和 class 還可以循環嵌套,即:class 里加入新 qdisc,然後新 qdisc 裏又可以繼續添加新 class, 最終形成的是一個以 root qdisc 爲根的樹。但對於本文接下來的內容,我們不需要了解這麼多。

  • 1.2 tc 示例:匹配 IP 和 Port 對流量進行分類

    下面是一個例子,(參考了 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

    以上設置表示以下順序邏輯

    1. 如果包匹配 src_ip==1.2.3.4 && dst_port==80,則將其送到第一個隊列。這個隊列對應的 class 目標速率是 30kbps;否則,
    2. 如果包匹配 src_ip==1.2.3.4,則將其 drop;
    3. 所有其他包將被送到第二個隊列,對應的 class 目標速率是 10kbps

    2 tc ebpf 程序

    有了以上基礎,現在可以討論 eBPF 了。

    本質上,eBPF 是一種類彙編語言,能編寫運行在內核的、安全的程序。eBPF 程序能 attach 到內核中的若干 hook 點,其中大部分 hook 點 都是用於包處理(packet processing)和監控(monitoring)目的的。

    這些 hook 中有兩個與 TC 相關:從內核 4.1 開始,eBPF 程序能作爲 tc classifier 或 tc action 附着(attach)到這兩個 hook 點

    2.1 用作 classifier(分類器)

    作爲分類器使用時,eBPF 能使處理過程更靈活,甚至還能實現有狀態處理,或者與用戶 態交互(通過名爲 map 的特殊數據結構)。

    但這種場景下的 eBPF 程序本質上還是一個分類器,因此返回值與普通分類器並無二致

    • 0:mismatch
    • -1:match,表示當前 filter 的默認 classid
    • 其他值:表示 classid

    2.2 用作 action(動作)

    用作 action 時,eBPF 程序的返回值 提示系統接下來對這個包執行什麼動作(action),下面的內容來自 tc-bpf(2)

    1. TC_ACT_UNSPEC (-1):使用 tc 的默認 action(與 classifier/filter 返回 -1 時類似)。
    2. TC_ACT_OK (0):結束處理過程,放行(allows the packet to proceed)。
    3. TC_ACT_RECLASSIFY (1):從頭開始,重新執行分類過程。
    4. TC_ACT_SHOT (2)丟棄包
    5. TC_ACT_PIPE (3):如果有下一個 action,執行之。
    6. 其他值:定義在 include/uapi/linux/pkt_cls.h [5]BPF and XDP Reference Guide from Cilium [6] 有進一步介紹。
    7. 沒有定義在以上頭文件中的值,屬於未定義返回值(unspecified return codes)。

    3 direct-action

    有了以上基礎,現在可以討論 direct-action 了。

    3.1 傳統 classifier+action 模式的限制

    上面看到,

    • classifer 能對包進行匹配,但** 返回的 classid**;它 只能告訴系統接下來把這個包送到那個 class(隊列), 但無法讓系統對這個包執行動作(drop、allow、mirror 等)。
    • action 返回的是動作,告訴系統接下來要對這個包做什麼(drop、allow、mirror 等),但它無 法對包進行分類(規則匹配)。

    所以,如果要實現”匹配+執行動作“的目的—— 例如,如果源 IP 是 10.1.1.1,則 drop 這 個包 —— 就需要兩個步驟:一個 classifier 和一個 action,即 classfifier+action 模式。

    3.2 爲 tc ebpf classifier 引入 direct-action 模式

    雖然 eBPF 有一些限制,例如單個程序的指令數是有上限的、只允許有限循環等等,但 它提供了一種數據包處理的強大語言。這帶來的結果之一是:對於很多場景,eBPF classifier 已經有足夠的能力完成完成任務處理,無需再 attach 額外的 qdisc 或 class 了,對於 tc 層的數據包過濾(pass/drop/etc)場景尤其如此。

    所以,爲了

    • 避免因套用 tc 原有流程而引入一個功能單薄的 action
    • 簡化那些 classfier 獨自就能完成所有工作的場景
    • 提升性能

    針對 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 相應的包了。

    • 性能方面,這顯然也是更優的,因爲 TC 子系統無需再調用到額外的 action 模塊 ,而後者是在內核之外的(external to the kernel)
    • 從使用方來說,使用 direct-action flag 也是最簡單的、最快的,是現在的推薦方式。

    3.3 能爲 tc ebpf action 引入 "direct-classifier" 模式嗎?

    那麼,TC eBPF action 能完成類似功能嗎?也就是說,能用 action 模塊來完成處理包+返回 “pass” 或 “drop” 嗎?答案是不行:actions 並沒有直接 attach 到某個 qdisc,它們只能用於包從某個 classifier 出來的地方, 這也就意味着:無論如何都得有個 classifier/filter

    另一個問題:這意味着TC eBPF actions 毫無用處了嗎?也不是。eBPF action 仍然還可以用在其他 filters 後面。例如下面這個場景,

    1. attach 一個 u32 filter 到一個 qdisc,根據包中的某些字段做(初步)過濾

    2. 在這個 filter 後面再加一個 ebpf action(做進一步過濾)

      因爲 ebpf action 中可以實現邏輯處理,因此可以在這裏做額外判斷,如果包滿 足某些額外的條件,就返回 drop。

    以上就是 ebpf action 可以使用的場景之一。但坦白說,我見過的場景都是 eBPF 程序同 時負責 filtering 和返回 action,而不需要額外的 filters。

    3.4 tc ebpf classifier 返回值被重新解讀,是否因此丟失了 classid 信息?

    正常 classifier 返回的是 classid,提示系統接下來應該把包送到哪個 class 做進一步處理。而現在, tc ebpf classifier direct-action 模式返回的是 action 結果。

    這是否意味着 eBPF classifier 丟失了 classid 信息?

    答案是:NO,我們仍然可以從其他地方獲得這個 classid 信息。傳遞給 filter 程序 的參數是 struct __skb_buff,其中有個 tc_classid 字段,存儲的就是返回的 classid。後面介紹內核實現時會看到。

    4 新的 qdisc 類型:clsact

    direct-action 模式引入內核和 iproute2 之後幾個月, 內核 Linux 4.5 添加了一個新的 qdisc 類型:clsact

    • clsactingress qdisc 類似,能夠以 direct-action 模式 attach eBPF 程序, 其 特點是不會執行任何排隊(does not perform any queuing)。
    • clsactingress 的超集,因爲它 還支持在 egress 上以 direct-action 模式 attach eBPF 程序,而在此之前我們是無法做到這一點的。

    更多關於 clsact qdisc 信息見commit log[7]👉Cilium Guide。

    5 完整示例(eBPF 程序 + tc 命令)

    下面展示如何編寫一個 tc ebpf filter (classifier),以及如何編譯、加載、附着到內核 。

    5.1 eBPF 程序(tc ebpf classifier/filter)

    下面這段程序根據包的大小和協議類型進行處理,可能會 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;
    }

    5.2 編譯

    使用 clang/LLVM 將我們的 ebpf filter 程序編譯爲編譯成目標文件:

    $ clang -O2 -emit-llvm -c foo.c -o - | \
        llc -march=bpf -mcpu=probe -filetype=obj -o foo.o

    5.3 加載到內核

    首先需要創建一個 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)程序已經開始工作了。

    5.4 清理

    $ tc qdisc del dev eth0 clsact

    6 實現

    6.1 內核實現

    內核對 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 1break;
        case htons(ETH_P_IP):   skb->tc_classid 2break;
        case htons(ETH_P_IPV6): skb->tc_classid 3break;
        defaultreturn TC_ACT_SHOT;
      }

      return TC_ACT_OK;
    }

    尤其值得一提的是下面這段邏輯,

    做一點解釋:

    • filter_res = BPF_PROG_RUN(prog->filter, skb); 這個函數 執行 eBPF 程序(classifier/filter),並將返回值存到 filter_res
    • 在這個 patch 之前,eBPF 程序返回的是 classid,因此我們看到原有的 邏輯是:
      • 如果 filter_res !=0 && filter_res != -1,那 res->classid = filter_res;
      • 然後執行 ret = tcf_exts_exec(skb, &prog->exts, res);,這會 調用到相關的 action 模塊,對包執行 action
    • 有了這個 patch,並且使用了 da 模式,filter_res 被解讀爲 action 值( prog->exts_integratedtrue 時表示 direct-action)。此時,
      • classid 是從 qdisc_skb_cb(skb)->tc_classid 獲取的,其中 struct __sk_buff *skb 是傳遞給 eBPF 程序的上下文
      • 將 filter_res 作爲 action 值,執行 ret = cls_bpf_exec_opcode(filter_res);而非調用外部 action 模塊),然後退出循環

    6.2 iproute2/tc 實現

    相應的 iproute2 commit faa8a463002f[9], 添加了對 tc da|direct-action 的支持。

    7 總結

    本文介紹了 tc ebpf 中 da 模式的來龍去脈,並給出了詳細的使用案例。截至本文發表時,da 模式不僅是使用 tc ebpf 的推薦方式,而且 據我所知也是唯一方式。

    參考資料

    1. On getting tc classifier fully programmable with cls_bpf [10], Daniel Borkmann, netdev 1.1, Sevilla, February 2016
    2. Linux kernel commit 045efa82ff56 [11] cls_bpf: introduce integrated actions, Daniel Borkmann and Alexei Starovoitov, September 2015
    3. Linux kernel commit 1f211a1b929c [12] net, sched: add clsact qdisc, Daniel Borkmann, January 2016
    4. iproute2 commit faa8a463002f [13] f_bpf: allow for optional classid and add flags, Daniel Borkmann, September 2015
    5. iproute2 commit 8f9afdd53156 [14] tc, clsact: add clsact frontend, Daniel Borkmann, January 2016

    參考資料

    [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/



    你可能還喜歡

    點擊下方圖片即可閱讀

    爲容器時代設計的高級 eBPF 內核特性(FOSDEM, 2021)

    雲原生是一種信仰 🤘



    碼關注公衆號

    後臺回覆◉k8s◉獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!



    點擊 "閱讀原文" 獲取更好的閱讀體驗!


    發現朋友圈變“安靜”了嗎?

    本文分享自微信公衆號 - 雲原生實驗室(cloud_native_yang)。
    如有侵權,請聯繫 [email protected] 刪除。
    本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章