[譯] 雲原生世界中的數據包標記(packet mark)(LPC, 2020)

語言: CN / TW / HK

譯者序

本文翻譯自 2020 年 Joe Stringer 在 Linux Plumbers Conference 的一篇分享: Packet Mark In a Cloud Native World 。 探討一個在網絡和安全領域 非常重要但又討論甚少的主題 :skb mark。

skb mark 是打在 內核數據包 (skb )上的 數字標記 ,例如,可能是一個 16bit 或 32bit 整數表示。這個 mark 只存在於每台主機內部 ,當包從網卡發出去之後,這個信息 就丟失了 —— 也就是説,它並 沒有存儲在任何 packet header 中

skb mark 用於傳遞狀態信息。在主機的網絡處理路徑上,網絡應用(network applications)可以在一個地方給包打上 mark,稍後在另一個地方根據 mark 值對包進行相 應操作,據此可以實現 NAT、QoS、LoadBalancing 等功能。

這裏的一個問題是: mark 是一個開放空間 ,目前還沒有任何行業規範,因此 任何應用 可以往裏面寫入任何值 —— 只要稍後它自己能正確解讀就行了,但每個 skb 的 mark 只有一 份。顯而易見, 當主機內同時運行了多個網絡應用並且它們都在使用 skb mark 時 (例 如,kube-proxy + Cilium),就有可能發生衝突,導致包被莫名其妙地轉發、丟棄或 修改等問題,因為它們彼此並不感知對方的 mark 語義。

由於譯者水平有限,本文不免存在遺漏或錯誤之處。如有疑問,請查閲原文。

以下是譯文。

準備這次分享時,我產生了一個疑問是: 互聯網是如何連接到一起的 (how is the internet held together)?比如,是靠膠帶(duct tape)嗎?—— 這當然是開玩笑。

今天討論的主題 —— skb mark —— 要比膠帶嚴肅的多。另外,本文將聚焦在雲原生( cloud native)領域,因為過去 2~5 年這一領域出現了很多新的網絡插件(software plugins),正是它們在控制着現在的網絡。

1 背景

1.1 Linux 世界中的 mark

mark 在不同子系統中有不同的叫法,例如,

  • fw_mark

    我不確定這是 firewall mark 還是 forwarding mark 的縮寫。 iptables 和內核路由層 (routing layer)會用到這個 mark。

    // include/linux/skbuff.h,代碼來自 Kernel 4.19,下同。譯註
        
      struct sk_buff {
          ...
          union {
              __u32    mark;
              __u32    reserved_tailroom;
          };
          ...
      }
  • ct_mark

    連接跟蹤(conntrack)的 mark,這個 mark 並沒有打在 skb->mark 上,但使 用方式是類似的:先將信息存到 mark,到了用的地方再取出來,根據 mark 狀態 進行相應處理。

  • skb_mark

    OVS 裏面的一個 mark,雖然和 skb->mark 不是一個東西,但二者是強關聯的。

  • SO_MARKinclude/uapi/asm-generic/socket.h

    用户空間 socket 層的 mark。應用層可以用 setsockopt() 將某些信息傳遞到 netfilter 和 tc 之類的子系統中。後面會看到使用案例。

  • xfrm_mark

    來自 變換子系統 (transform subsystem)。

    // include/uapi/linux/xfrm.h
    
      struct xfrm_mark {
          __u32           v; /* value */
          __u32           m; /* mask */
      };
  • pkt_mark

    OVS 字段,引入的目的是對 OVS skb_mark 做通用化,因為後者用於 Linux,而 OVS 可能運行在非 Linux 機器上。

1.2 mark 有什麼用?

説了這麼多,那這些 mark 到底有什麼用? —— 如果不設置,那它們就沒什麼用。

換句話説, mark 能發揮多少作用、完成哪些功能 ,全看應用怎麼用它 。 比如當需要編程控制內核的處理行為時,就會和這些 mark 打交道。很多黑科技就源於此。

1.3 mark 註冊中心

如果你開發了一個網絡軟件,流量收發都沒問題。但設置了某些 mark 位之後, 流量就莫名其妙地在某些地方消失了,就像進入了黑洞,或者諸如此類的一些事情。 這很可能是機器上運行的其他軟件也在用 mark,和你的衝突了。

不幸的是, 當前並沒有一個權威機構能吿訴你,哪些軟件在使用 mark,以及它們是如何 使用的 。因此,想要在自己的應用中設置 mark 字段時,如何通知到外界,以及如何確保 不會與別人的 mark 衝突,就是一件很困難的事情,因為沒有一箇中心式的註冊中心在管理 這些 mark —— 直到大約一個月之前,Dave 發了下面這條推文:

Dave 創建了這個 github repo,但注意,這裏並不是教大家如何使用 mark,這也不是一個 決策機構,而 只是一份文檔,記錄大家正在使用的 mark 。如果你在用自己的 mark 方 案,強烈建議你記錄到到這個 repo。

1.4 Cilium 網絡

我來自 Cilium 團隊,Cilium 是一個雲原生網絡方案,提供了眾多的網絡、可觀測性和安全能力:

我們會用到 mark,比如處理 kube-proxy + Cilium 的兼容問題。

1.5 眾多 CNCF 網絡插件

為了準備這次分享,我還潛入雲原生領域進行了諸多探索。下圖是一些 CNCF 雲原生網絡插 件,它們多少都用到了 mark。

想了解具體的某個插件是如何使用 mark 的,可以找到它的源碼,然後搜索 mark 關鍵字。

這裏的一個考慮是:不同網絡插件提供的功能可能是 可疊加 的。 舉個例子,如果你已經在用 flannel,然後又想用 Cilium 的可觀測性和安全能力,那就可 以同時運行這兩種網絡插件 —— 顯然,這裏的 前提 是:Cilium 和 flannel 要對內核如 何處理包有一致的理解,這樣才能確保 Cilium 沿某個路徑轉發包時,它們不會被丟棄( drop)。要做到這一點,Cilium 就需要理解包括 flannel 在內的一些組件是如何設置和使 用 mark 的。

下面我們就來看一些 mark 的典型使用場景。

2 使用案例

這裏整理了 7 個使用案例。我們會看到它們要完成各自的功能,分別需要使用 mark 中 的幾個比特位。

2.1 網絡策略(network policy)

第一個場景是網絡安全策略,這是 K8s 不可或缺的組成部分。

這種場景用一個比特位就夠了,可以表示兩個值:

  • drop:白名單模式,默認全部 drop,顯式配置 allow 列表
  • allow:黑名單模式,默認全部 allow,顯式配置 drop 列表

默認 drop 模式

默認情況下, K8s 會自帶一條 iptables 規則,drop 掉沒有顯式放行(allow)的流量

工作機制比較簡單:

首先,在一條 iptables chain 中給經過的包打上一個 drop 標記(佔用 skb->mark 中一個比特就夠了),

(k8s node) $ iptables -t nat -L
...
Chain KUBE-MARK-DROP (0 references)
target     prot opt source     destination
MARK       all  --  anywhere   anywhere     MARK or 0x8000 # 所有經過這條規則的包執行:skb->mark |= 0x8000

稍後在另一條 chain 中檢查這個標誌位,如果仍然處於置位狀態,就丟棄這個包:

(k8s node) $ iptables -L
Chain INPUT (policy ACCEPT)
target         prot opt source               destination
KUBE-FIREWALL  all  --  anywhere             anywhere  # 如果這條規則前面沒有其他規則,就會跳轉到下面的 KUBE-FIREWALL 

...

Chain KUBE-FIREWALL (2 references)
target     prot opt source               destination
DROP       all  --  anywhere             anywhere      # /* drop marked packets */ mark match 0x8000/0x8000

默認 allow 模式

這是白名單模式的變種:先給每個包打上允許通行(allow)標記,也是佔用一個比特,稍 後再通過檢查 skb->mark 有沒有置位來決定是否放行。

另一個類似的場景是 加解密 : 對需要加密的流量設置某些 mark,然後在執行加密的地方做檢查,對設置了 skb->mark 的執行加密,沒有設置的不執行。

通用處理模式

總結起來,這些場景的使用模式都是類似的: 通過 packet mark 和 iptables 規則實現複雜的流量路徑控制

skb->mark

典型場景:netfilter -> netfilter 流量過濾。

2.2 透明加密(transparent encryption)

加解密需要兩個比特:一個加密標誌位,一個解密標誌位。

  • 常規的做法就是設置加密比特位,表示接下來需要對這個包做加密;或者設置解密位表示 要做解密。

  • 變種:有很多的可用祕鑰,在 mark 存放要用的祕鑰索引(index)。

典型場景:{ eBPF, netfilter } -> xfrm

2.3 Virtual IP Service(DNAT)

這種 Service 會有一個 VIP 作為入口,然後通過 DNAT 負載均衡到後端實例(backends)。

典型情況下,完成這個功能需要一或兩個比特,設置之後來表示需要對這些包做 DNAT。但 嚴格來説,這不是唯一的實現方式。你也可以自己寫一些邏輯來匹配目的 IP 和端口,然後 對匹配到的包執行 DNAT。

如果內核版本較老,那我們基於 eBPF 的 Service 實現可能會受限,此時就需 要與其他軟件協同工作才能提供完整的 Service 功能。

我遇到過的一個場景是 OVS bridge(OVS -> routing -> OVS)。OVS 會設置一些 mark, 然後傳給內核的策略路由模塊,內核做策略路由之後再重新轉發回 OVS,在 OVS 完成最終的 DNAT。

我遇到的最複雜的場景可能是 kube-router,我們會將 Service 信息寫入內核,kube-router 會查看 Service 列表,提取三元組哈希成 30bit 寫入 skb-mark ,稍後內核裏的 IPVS 再根據規則匹配這 些 mark 做某些負載均衡。

典型場景:{ eBPF, netfilter } -> netfilter

2.4 IP Masquerade(動態 SNAT)

和前面 Service/DNAT 類似,這裏是設置某些比特位來做 SNAT。例如在前面某個地方設置 mark, 稍後在 IPVS 裏檢查這個 mark,然後通過 IP masquerade 做某些形式的負載均衡。

兩個變種:

  1. 設置一個比特位,表示不要做 SNAT(1 bit, skip SNAT)。

    網絡插件負責配置容器的網絡連通性。但容器能否被集羣外(或公網)訪問就因插件 而異了。如果想讓應用被公網訪問,就需要通過某種方式配置一個公網 IP 地址。 這裏討論的就是這種場景。

    典型情況下,此時仍然只需要一個 bit,表明 不要對設置了 mark 的包做 SNAT(非公 網流量) ;沒有設置 mark 包需要做 masquerade/SNAT,這些是公網流量。

    具體到 Cilium CNI plugin,可以在創建 pod 時聲明 帶哪些 label 的 pod 在出集羣時( egress)應當使用哪個特定的 源 IP 地址 。例如,一台 node 上運行了三個應用的 pod,這些 pod 訪問公網時,可以分別使用不同的 src ip 做 SNAT 到公網。

    上面的場景中, 連接都是主動從 node 發起的 ,例如,node 內的應用主動訪問公 網。實際中還有很多的連接是 從外部發起的,目的端是 node 內的應用 。例如,來 自 VPN 的訪問 node 內應用的流量。

    這種情況下,如果將流量轉發到本機協議棧網絡,可以給它們打上一個 mark,表示要 做 SNAT。這樣響應流量也會經過這個 node,然後沿着反向路徑回到 VPN 客户端。

  2. 用 32bit,選擇用哪個 SRC IP 做 SNAT/Masquerade。

典型場景:{eBPF, OVS, netfilter} -> netfilter

2.5 Multi-homing

非對稱路徑

這種場景在 AWS 環境中最常見。

背景信息:每個 AWS node(EC2)都有

  • 一個 primary 設備,提供了到外部的網絡連通性, node 默認路由走這裏
  • 多個 secondary 設備( node 默認路由不經過它們 ),每個設備上有多個獨立的 IP 地 址,典型情況下是 8 個。在 node 上部署容器時,會從這些 secondary IP 地址中選擇 一個來用。當 8 個地址用完之後,可以再分配一個 secondary device attach 到 EC2 (所以每個 EC2 都有多個網絡)。

容器訪問另一台 node 上的容器時 ,流量需要發送到對端容器所佔用的那個 secondary 設備上。 這裏會用到 源路由 (source routing),也叫 策略路由 (policy routing)。 默認情況下,策略路由的工作方式是:

  1. 首先匹配包的 SRC IP,選擇對應的路由表,
  2. 然後在該路由表中再按 DST IP 匹配路由,
  3. 對於我們這裏的場景,最終會匹配到經過某條 secondary device 的路由,然後通過這 個 secondary interface 將包發送出去。

當實現 Service 或類似功能時,對於接收端 node,主要有兩種類型的流量:

  1. 從 secondary device 進來的、目的是本機容器的外部流量。也就是上面我們提到的流 量(Pod-to-Pod 流量);
  2. 從 primary device 進來的、目的是本機容器的流量(例如 NodePort Service 流量)。

對於第二種,不做特殊處理就會有問題:

  • 請求能正常從 primary device 進來,然後轉發給容器,被容器正確處理,至此這裏都沒問題,
  • 但從上面的分析可知,如果沒有額外處理,響應流量會從 secondary 設備發送到其所在 的網絡。

導致的問題是:來的路徑和回去的路徑不一致(非對稱路徑),回包會被丟棄。

這裏就是 最經典地會用到 mark 的地方

  • 當流量從 primary 設備進來時,設置一個比特位,記錄在連接跟蹤的 mark(conntrack mark)中。
  • 當響應從 pod 發出時,查詢連接跟蹤記錄。如果設置了這個 mark,就表明這個包需要從 主設備路由出去。

這樣就解決了非對稱路徑的問題。

管理網與業務網分離:socket mark

另一個是 VPN 場景,每台 node 上可能會跑一個 management agent,負責配 置 VPN 網絡。

這種情況下,肯定不能將管理網本身的流量也放到 VPN 網絡。 此時就可以用到 socket mark 。這個狀態會 傳遞給路由層 ,在路由決策時使用。

這樣做到了管理流量和 VPN 流量的分離。

典型:{ socket, netfilter } -> routing

2.6 應用身份(application identity)

Application identity (應用身份)用於網絡層的訪問控制。

在 Cilium 中, 每個 endpoint 都對應一個 identityN:1 ),表示這個容器的安全 身份。Identity 主要用來實現 network policy,佔用的比特數:

  • 一般用 16bit 表示 (業界慣例),Cilium 單集羣 時也是這樣,
  • 如果需要 跨集羣/多集羣 做安全策略,那這個 identity 會擴展到 24bit ,多出來的 8bit 表示 cluster ID。

可以在出向(egress)和入向(ingress)做安全控制:

  • 如果 pod 想訪問其他服務,可以在它的出向(egress)做策略,設置能訪問和不能訪問 哪些資源。如果沒有設置任何策略,就會使用 默認的 allow all 策略
  • 在接收端 pod 的入向(ingress)也可以做策略控制,過濾哪些源過來的允許訪問,哪些不允許。

這裏比較好的一點是:可以 將 identity 以 mark 的方式打在每個包上 ,這樣看到 identity 就知道了包的來源,因此安全策略的實現就可以變得簡單:從包上提取 identity 和 IP、port 等信息,去查找有沒有對應的放行策略就行了。

當與別的系統集成時,這裏會變得更有意思。例如有個叫 portmap 的 CNI 插件,可以做 CNI chaining,感興趣可以去看看。集成時最大的問題是,無法保證在打標(mark)和檢查 mark 之間會發生什麼事情。

典型路徑:{ eBPF, netfilter } -> routing -> eBPF

2.7 服務代理(service proxy)

這裏的最後一個案例是服務代理(service proxy)。

Proxy 會終結來自客户端的請求,然後將其重定向到本機協議棧,隨後請求被監聽在本機協 議棧的服務(service)收起。

根據具體場景的不同,需要使用至少一個比特位:

  • 1 bit, route locally

    設置了這個比特位,就表示在本機做路由轉發。

    我見過的大部分 service proxy 實際上只需要一個比特,但在實現上,有些卻佔用了整改 mark(16bit)。

  • 16 bit tproxy port towards proxy

    在老內核上,Cilium 會通過 mark 傳遞一個 16bit 的 tproxy port(從 eBPF 傳遞給 Netfilter 子系統),以此指定用哪個代理來轉發流量。

  • 16+ bit Identity from proxy

    還可以通過 proxy 傳遞 identity。 這樣就能夠在處理 flow 的整個過程中保存這份狀態(retain that state)。

典型路徑:

  • eBPF -> { netfilter, routing }
  • netfilter -> routing
  • socket -> { eBPF, netfilter },

3 思考、建議和挑戰

這裏討論一些使用 mark 時的挑戰,以及如何與其他網絡應用互操作(interoperate), 因為多個網絡應用可能在同時對內核網絡棧進行編程(programming the stack)。

3.1 mark 使用方案設計

首先理解最簡單問題:如果要開發一個會設置 skb->mark 的網絡應用,那 如何分配 mark 中的每個比特?

有兩種方式:

  1. 比特位方式:每個 bit 都有特定的語義。

    例如 32bit mark 能提供 32 個功能,每個功能都可以獨立打開或關閉。 因此,當有多個應用時,就可以説應用 A 使用這個比特,應用 B 使用另一個比特,合 理地分配這些比特空間。

    這種方式的一個問題是: 最多隻能提供 32 種功能

  2. Full mark 方式:將 mark 作為一個整形值。

    這樣可以用到整個整形變量的空間,能提供的功能比前一種多的多。例如 32bit 可以提供 42 億個不同的值。

根據我的觀察,很多的軟件在實現中都只使用了一個 bit,如果想做一些更瘋狂和有趣的事 情,那需要將擴充到 32bit,然後在子系統之間傳遞這些信息。

3.2 比特位重載

如果你需要 60 個功能,那顯然應該用 4 個比特位來編碼這些功能,而不是使用 60 個獨 立的比特位。但這種方式也有明顯的限制:每個功能無法獨立打開或關閉。因此用哪種方式 ,取決於你想和哪個子系統集成。

解決這個問題的另一種方式是: 重載(overload)某些比特位

  • 例如,同樣是最低 4bit,在 ingress 和 egress 上下文中,分別表示不同的含義。
  • 又如,根據包的地址範圍來解釋 mark 的含義,到某些地址範圍的包,這些比特表示一種 意思;到其他地址範圍的,表示另一種意思。

這顯然帶來了一些有趣的挑戰。一旦開始 overload 這些 marks,理論上總能構建出 能與其他軟件互操作的軟件。

這個過程中,找到從哪裏開始下手是很重要的。這就是我開始注意到前面提到的那些網絡軟 件的原因之一:因為 所有這些工作最後都是與人打交道 。例如, iptables 設置了第 15bit 表示 drop 的事實,意味着 其他所有插件都要遵守 這些語義 ,並且其他人要避免使用這個 bit。這樣當多個不同的網絡插件或軟件需要協 同工作來提供一組互補或增強的功能時,它們才不會彼此衝突。即, 不同插件或軟件之間要 對比特位的語義有一致的理解

對於 Cilium 來説,這是由我們的用户驅動的,如果用户已經使用了某些插件,並且希望在 這個插件之外同時運行 Cilium,我們就只能從尋求與這些插件的兼容開始。

3.3 發佈和遵守 mark 方案

那麼,我們該如何共享自己的使用方案呢?即,在與其他插件一起運行時,哪個比特位表示 什麼意思,這些比特位提供哪些功能。

從網絡應用角度來説,這裏很重要的一點是: 理清自己的功能,以及協同工作的他軟件的 功能 。例如,如果用户同時運行了 Cilium CNI 和另一個 CNI 插件,後者也提供了加 密功能;那從 Cilium 的角度來説,我們就無需開啟自己的加密功能,讓底層插件來做就行了 。

從現實的角度來説,

  • 讓開發者遵守這些規範、理解它們是如何工作的,是一件有成本的事情;
  • 從複雜度的角度來説,如何管理和部署也是一件很有挑戰的事情,因為更多的軟件或插件 意味着更有可能出錯,排障也會更加困難。本文主要關注在如何分配比特,如何定義語 義,如何與其他應用共享互操作。

3.4 深入理解網絡棧

需要説明的是,在實際中,mark 並不是唯一軟件的集成點(integration point)。

不同的插件可能都會插入 iptables 規則,匹配特定的目的地址、源地址等;甚至還可能 用 mark 來做新的策略路由,應用在不同的領域。

所以,如果你真要實現一個功能,能用到的信息其實不止是 32bit 的 mark,還可以用包頭 中的字段、連接跟蹤中的狀態(conntrack status)等等。因此最終,你會坐下來研究網絡 棧流程圖,理解你的包是如何穿過 TC、eBPF 和 Netfilter 等的。

此外,還需要理解不同的軟件、它們各自的機制,以及包經過這些不同的子系統時的不同路 徑,這會因你啟用的功能以及包的源和目的地址等而異。例如,這裏最常見的場景之一是: 請求流量是正常的,但響應流量卻在某個地方消失了,最終發現是因 multi-home 問題被路 由到了不同的設備。

3.5 少即是多

如果有更多的 bit 可用,你會用來做什麼?

對於 Cilium 來説,我們正在積極探索 用 eBPF 統一子系統之間的協作方式 , 這樣就可以避免在 eBPF 和 Netfilter、Conntrack 等子系統之間傳遞大量元數據了。 如果能原生地在 eBPF 中實現處理邏輯,那就能使用 eBPF 領域的標準工具, 進而就能推理出包的轉發路徑等等,從而減少 mark 的使用。在這種方式下,和其他 軟件集成就會輕鬆很多,因為我們並沒有佔用這些 mark。

當然,這並不是説只有 eBPF 能統一子系統之間的協作,你用 OVS、Netfilter 等等方式, 理論上也能統一。

另一個經常會討論到的問題是:我們 能否擴展 mark 空間 ?直接擴展 skb->mark 字 段我認為是太可能的。

  • 相比之下,添加一個 skb mark extension 之類的新字段,用這個字段做一些事情還是有 可能的,這樣就有更多的通用比特(generic bits)來做事情。
  • 另一種方式是:將某些使用場景規範化。從通用空間中將某些 bits 拿出來,單獨作為某 些場景的專用比特,定義它們的語義,這樣它們之間的互操作就方便多了。但這種方式 會消耗一部分 mark 空間,留給其他網絡應用的 mark 空間會變得更小。

4 總結

最後總結,packet mark 是一種非常強大的機制,使我們能 在不同子系統之間傳遞各種狀態信息

另外,如何定義 mark 的語義,用户有很大的靈活性。當然,反面是如果你的軟件想要 和其他網絡軟件協同工作,那必須事前約定,大家使用的 mark 不能有衝突, 並且彼此還要理解對方的語義(例子:kube-proxy + Cilium 場景)。 這顯然會帶來很多的不確定性,當你試圖實現某些新功能時,可能就會發現這個 mark 對我 來説很有用,但會不會和別的軟件衝突,只有等實際部署到真實環境之後可能才會發現。很 可能直到這時你才會發現:原來這個 mark 已經被某個軟件使用了、它的使用方式是這樣的 、等等。

因此,我希望前面提到的 mark registry 能幫我們解決這個問題,希望大家將自己在用的 mark 以文檔的方式集中到那個 repo。這也算是一個起點,由此我們就能知道,哪些應用的 mark 方式是和我的有衝突的。然後就能深入這個特定項目的源碼,來看能否解決這些衝突 。

另外應該知道,mark 能提供的功能數,以及相應的場景數,要遠遠多於 mark 的比特數。 關鍵在於你需要多少功能。

5 相關鏈接

Cilium

  1. http://cilium.io
  2. http://cilium.io/slack
  3. http://github.com/cilium/cilium
  4. http://twitter.com/ciliumproject

Mark registry

  1. http://github.com/fwmark/registry

« [譯] 利用 eBPF 支撐大規模 K8s Service (LPC, 2019)