[譯] 雲原生世界中的資料包標記(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. https://cilium.io
  2. https://cilium.io/slack
  3. https://github.com/cilium/cilium
  4. https://twitter.com/ciliumproject

Mark registry

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

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