この記事は、CYBOZU SUMMER BLOG FES ’25の記事です。
こんにちは。
クラウド基盤本部 Cloud Platform 部で Kubernetes 基盤(Neco)のネットワークを担当している寺嶋(terassyi)です。
Neco では Cilium を Kubernetes ネットワークに採用しています。
先日我々の Cilium 活用の事例が CNCF のサイトに公開されましたので、ぜひそちらもご覧ください。
本記事では Cilium の通信制御のコアである Cilium の Connection Tracking の挙動と実装について解説するものです。
前書き
Cilium は eBPF をパケット処理などに採用したコンテナネットワークを管理するソフトウェアです。Cilium は高速な Kubernetes ネットワークを提供するため、kube-proxy 代替機能や Connection Tracking を eBPF ベースで独自に実装しています。
本記事は大きく以下の三つのパートから構成されます。
- Cilium の紹介
- Cilium の conntrack の理解に必要な要素の解説
- Conntrack の一般的な解説
- Cilium が管理する Kubernetes ネットワークの概要
- Cilium の BPF プログラムや BPF マップの概要
- Cilium の conntrack の挙動と実装の解説
※ 本記事は、 Cilium 1.16.12 のコードをもとに執筆します。Cilium は開発スピードが非常に早いソフトウェアです。本記事で解説した挙動が、新しいバージョンでは修正されている可能性があることに注意してください。
本記事で解説する Cilium の挙動や実装は Neco で利用している構成を前提としています。
目次
Cilium とは
Cilium は Kubernetes をはじめとしたクラウドネイティブ環境のネットワークにおけるパフォーマンス向上やセキュリティ・オブザーバビリティを提供するソフトウェアです。
後述する CNI プラグイン機能や Network Policy や L4LB、トラフィック可視化などの機能を提供しています。
Cilium はデータプレーンに eBPF を採用しており、 高速で安全なデータプレーンを実現しています。
CNI プラグイン
Cilium は CNI プラグインとして実装されています。CNI プラグイン(CNI) とはコンテナのネットワークを設定するコンポーネントです。CNI Spec という仕様が公開されており、実装は各プラグインに委ねられています。
CNI プラグイン は様々な実装が存在しています。広く利用されているものに Calico や flannel があります。Neco でも独自の Coil というプラグインを開発して利用しています。
eBPF
Cilium のパケット処理は eBPF で実装されています。
eBPFの基本的な情報については以下の資料をご覧ください。
Neco での Cilium の活用例
Cilium は非常に機能が多いソフトウェアです。Neco では、その中で必要な機能のみに絞って利用しています。我々が主に利用している機能は以下です。
- kube-proxy replacement(KPR)
- Network Policy
- Layer 4 Load Balancer
我々がどのように Cilium を利用して Kubernetes のネットワークを構築しているかはすでにブログが公開されているのでこちらを読んでみてください。
Cilium のすべての機能の土台となる kube-proxy replacement は iptables で実装されていた kube-proxy のスケーラビリティ問題を克服するために開発されました。
興味のある方は以下の動画を観てみてください。
Deep Dive の準備
本パートでは Cilium の conntrack を深く知るための準備として、いくつかの前提となる知識を紹介します。
始めに、conntrack の概要を一般的な視点で解説します。Conntrack は Cilium に特有の技術ではありません。身近な conntrack の利用例を紹介して conntrack への解像度を上げます。
次に、Cilium が管理する Kubernetes ネットワークの概要を紹介します。
本記事では Cilium が管理する Kubernetes ネットワークとは、Cilium の kube-proxy replacement(以下 KPR) が導入された Kubernetes のネットワークを指します。
Cilium の conntrack は Kubernetes ネットワークに最適化して実装されているので、Cilium の conntrack を理解するには KPR の通信の仕組みを理解しておく必要があります。
最後に、Cilium の conntrack の実装を理解するために必要な Cilium の具体的な BPF プログラムや BPF マップの構成を紹介します。
※ 本パートの各項目は内容を既に知っている場合は読み飛ばしていただいて構いません。
Connection Tracking とは
Connection Tracking(以下 conntrack)は、大まかに言うと通信の状態を保存、追跡する機能です。
送信元と宛先のアドレスやポート、プロトコルなどの情報を記録し、ステートフルな通信の制御や負荷分散などに利用します。
具体例は、NAT や Load Balancer、Firewall、Syn Flooding 攻撃に対する防御などがあります。
Linux の Conntrack
Conntrack は Linux では netfilter サブモジュールとして実装されています。netfilter は iptables や nftables コマンドで設定でき、IP masquerade(Network Address Port Translation; NAPT) やパケットフィルタリングなどに利用されます。
それらの機能のコアとして conntrack が利用されています。
ここでは IP masquerade を例にとって conntrack の仕組みを簡単に紹介します。
この図では Local Network 上の local-b というアドレスを持つ Client が External Network 上の、global-b というアドレスを持つ Server に接続するために、IP masquerade が設定された local-a と global-a というアドレスを持つ Proxy を利用するシナリオを示しています。

- Client は外部ネットワークの Server の y というポートで公開されているサービスにアクセスするために、
local-b:x -> global-b:yというパケット(SYN)を Proxy に向けて送出します。 - パケットを受け取った Proxy は masquerade の設定に従って、そのパケットを自分の global-a というアドレスに変換します。この時に conntrack table に対応するアドレスとポート、通信の状態(
SYNSENT)を保存します。 - アドレスとポートを書き換えて
global-a:z -> global-b:yとして Server に向けてパケットを転送します。 - Server は
global-b:y -> global-a:zに対して返答(SYN/ACK)を返します。 - Proxy は Server からの
SYN/ACKを観測して、conntrack エントリの状態をSYNRECVに更新します。 - Proxy は Server からの応答を Client に返します。
- その後、Client と Server 間で TCP コネクションが確立され、アプリケーションの通信が始まります。このとき、conntrack は接続確立を観測して conntrack エントリの状態を
ESTABLISHEDに更新します。
コネクションの終了時も TCP の終了シーケンスに則って似たような処理が実施されて、保存されていた conntrack エントリは削除されます。
Cilium も同じような仕組みで conntrack を実装していますが、より Kubernetes ネットワークに最適化されたものになっています。
Linux カーネルの conntrack の詳細に興味のある方は以下の資料を読んでみてください。
Cilium が管理する Kubernetes ネットワーク
Cilium は Kubernetes クラスタ内の全ての通信を扱います。また、Cilium の conntrack は後述する Kubernetes の各種通信方式に応じて様々な形で通信を管理します。
ここでは、 Cilium を導入したクラスタにおける Kubernetes ネットワークの概要を解説します。
※ 冒頭で述べた通り、ここでは Cilium kube-proxy Replacement(KPR) を前提としたネットワークについて説明します。
通常の kube-proxy は iptables や nftables を利用して通信制御を実装していますが、Cilium の KPR は kube-proxy を無効化して、eBPF で独自の通信制御を実装しています。
Pod 間通信
最も基礎的な通信です。Cilium の BPF プログラムがパケットを処理する以外で特別なことはありません。
ClusterIP Service の通信
Service リソースの ClusterIP type は、Cilium では Pod 間通信とほとんど変わりません。
Pod から送出された ClusterIP service 宛の通信は Cilium の BPF プログラムによって、送信元の Pod のノード上でその ClusterIP の任意のバックエンド Pod のアドレスに変換します。それ以降は Pod 間通信と同様の処理を行います。
※ Cilium は ClusterIP Service の実装として SocketLB と Per-Packet LB の二つを提供しています。デフォルトは SocketLB ですが、本記事では Per-Packet LB を解説します。
LoadBalancer Service の通信
Service リソースの LoadBalancer type は、クラスタ内のエンドポイントをクラスタ外部に公開するために利用します。クラスタ外部にアドレス(EXTERNAL-IP)を公開して外部からの通信を受け付けて負荷分散する Layer 4 Load Balancer です。
LoadBalancer service は .spec.externalTrafficPolicy の値(Cluster or Local)によって処理が異なります。
externalTrafficPolicy=Cluster
externalTrafficPolicy=Cluster(eTP=Cluster) の場合、かなり複雑な通信経路になります。
利用するメリットは、LoadBalancer service のバックエンド Pod への負荷が均等になることです。
Cilium はいくつかの実装を提供していますが、我々は Maglev hashing + DSR(Direct Server Return) with Geneve encap という組み合わせで L4LB を利用しています。
※ 本記事では DSR with Geneve encacp に限って解説します。
DSR with Geneve encap の詳細については以前 Cloud Native Days で発表していますのでそちらを参照してください。
Maglev hashing は consistent hashing アルゴリズムの一つで、 耐障害性や高い可用性を備えた負荷分散を提供します。
公開資料だと以下がまとまっています。
ここでは簡単に、DSR with Geneve encacp の L4LB の通信の仕組みを解説します。
L4LB の通信では、重要な役割を持つノードが二つあります。
- 中間ノード(Load Balancing Node)
- 「LB ノード」と呼ぶ場合もありますが、本記事では「中間ノード」と表記します
- 外部クライアントからのパケットを一次受けするノード
- 負荷分散の役割を担います
- バックエンドノード
- 転送すべきバックエンド Pod が存在するノード
- このノードから直接クライアントに返信します(DSR)
cilium-agent が動作するすべてのノードが中間ノードとして振る舞うことができます。中間ノードとなるノードは何らかの方法(BGP が利用されることが多いです。)でクラスタの上流のルータに LB Service の EXTERNAL-IP を広報します。
下図は中間ノードとして選択された NodeA と、バックエンドの Pod1 が存在する NodeB が LB Service の通信を処理する様子を示しています。

- 上流のルータは ECMP(Equal Cost Multi Path) の仕組みを使って、外部のクライアントからの
EXTERNAL-IP宛の通信を任意の中間ノード(今回はNodeAが選択されたという想定)に転送します NodeAが適切なバックエンド(今回はNodeB上のPod1)を選択してさらにパケットを転送します- 転送の際に、
NodeAの cilium-agent は Geneve プロトコル でカプセル化して、LB のEXTERNAL-IPとポートの情報をNodeBに引き渡します。 NodeBの cilium-agent がカプセル化の解除などの必要な処理をした後、バックエンド Pod にパケットが到達します
返信パケットはNodeBの cilium-agent が必要なデータを持っているので、クライアントに直接送信できます
externalTrafficPolicy=Local
externalTrafficPolicy=Local(eTP=Local) の場合はかなりシンプルで、外部ネットワークから通信を受けた時、直接バックエンドの Pod に転送されます。
この方式は Cluster がバックエンド Pod に均等に負荷を分散できるのに対して、Local はノード単位での負荷分散までしか対応できません。また、耐障害性も劣るので、特別な事情がない限り Cluster を利用する方が好ましいです。
NodePort Service の通信
Service リソースの NodePort type もクラスタ内のエンドポイントをクラスタ外に公開するために利用します。LoadBalancer service と異なるのは、サービスの公開のために、EXTERNAL-IP の代わりにポートを利用することです。任意の Kubernetes ノードのそのポート宛の通信を、NodePort service に紐づいたバックエンドに転送します。
※ Cilium の BPF プログラムの実装としては、NodePort と LoadBalancer は同一です。
Cilium の BPF プログラムとマップの概要
Cilium は eBPF でパケット処理を行います。
eBPF はパケット処理のためにさまざまなフックポイントを提供しており、Cilium では主に TC にアタッチする BPF プログラムでパケットを処理します。
※ Linux カーネル 6.6 以降から TC の強化版の TCX が実装されました。
TCX は従来の TC Classifier の強化版と思っていただければ十分です。
Cilium はこれを v1.16 からサポートしていて、デフォルトで利用されるのは TCX です。
[PATCH bpf-next v6 0/8] BPF link support for tc BPF programs – Daniel Borkmann
※ BPF プログラム上は TC と TCX で変化はほとんどなく、基本的な挙動も変わらないので、本記事では馴染みのある TC で以降の解説を行います。
※ 設定次第では XDP モードを利用することもできますし、一部で cgroup も利用します。
TC は Ingress/Egress それぞれにプログラムをアタッチできます。
Cilium がカーネルにアタッチする BPF プログラムの概要は以下の図のようになっています。

ノード上の Pod は veth(Virtual Ethernet Device) のペアがコンテナ側とホスト側で一対一対応で作成されて、通信を行います。
Cilium は上の図のように、ノードのプライマリなデバイス(ここでは eth0)と必要に応じてトンネルデバイス(cilium_geneve)、ホスト側の veth にそれぞれ異なる BPF プログラムを Ingress/Egress それぞれにアタッチしています。
※ cilium_geneve デバイスは設定値に応じて cilium-agent が作成するデバイスです。
本記事中では LB のパケットを DSR するために利用されます。
以前似た内容で LT をした際の資料です。興味のある方は覗いてみてください。
Cilium は BPF プログラムと同様に複数の BPF マップも作成します。その用途は多岐に渡りますが、本記事で登場するのは CT map(正式名称:cilium_ct4_global/cilium_ct_any4_global) と Service map(正式名称:cilium_lb4_services_v2) 、RevNAT map(正式名称:cilium_lb4_reverse_nat) などです。それぞれ以下の用途で利用されます。
- CT map
cilium_ct4_global(TCP 用)cilium_ct_any4_global(TCP 以外のプロトコル用)- Conntrack テーブル。本記事の主役
- 詳細は CT map で解説します
- Service map
cilium_lb4_services_v2- Kubernetes の Service のエンドポイントなどの情報を保存しておきます
- 各 Service のアドレスとポートなどをキー、上記のような情報をバリューに保持します
- RevNAT map
cilium_lb4_reverse_nat- Service の通信の reverse NAT に利用します
- Service に対して一意な ID をキー、Service のアドレスとポートをバリューに保持します
- NAT map
cilium_snat_v4_external- Pod からパケットをクライアントに DSR するときに SNAT するための情報を保存します
Cilium Connection Tracking Deep Dive
ここまで Cilium の conntrack の前提知識を紹介しました。
ここからは本題の Cilium の conntrack がどのように実装され、動作しているかを解説します。
※ Cilium v1.16.12 実装をベースに紹介します。最新版では異なる実装となっている可能性は十分あるので注意してください。
GitHub – cilium/cilium at v1.16.12
※ IPv4 の実装のみを解説します。
※ 今後 CT map と書くとき、基本的には cilium_ct4_global を指します。cilium_ct_any4_global について言及する際は明に記述します。
Cilium の conntrack は eBPF による パケット処理の実装と Go で書かれた control plane (cilium-agent) の二つの部分に分けられます。
Conntrack についてはコアとなるのは実際にパケットを処理する BPF プログラムです。ほとんどの処理は BPF プログラム内で完結しますが、CT map の GC(Garbage Collection) を control plane で実施しています。
概要
まずは概要を掴みましょう。
下図は Cilium の conntrack の概要を示しています。

CT map はノードごとに作成され、ノード上の各デバイスにアタッチされた BPF プログラムから操作され、パケットごとに対応するエントリが最新の状態に更新されます。
また、cilium-agent は定期的にユーザー空間から CT map を GC して不要なエントリを削除します。
CT map
本格的に通信の解説に入る前に、CT map について説明します。
Cilium-の-BPF-プログラムとマップの概要 で述べた通り、CT map には TCP 用の cilium_ct4_global とそれ以外のプロトコル用の cilium_ct_any4_global という二つが存在します。
BPF マップは基本的にはキーバリューストアです。マップのデータ構造はさまざまなものが選べますが、CT map は LRU(Least Recently Used) Hash を利用しています。
LRU Hash については以下を参照してください。
Map Type ‘BPF_MAP_TYPE_LRU_HASH’ – eBPF Docs
cilium_ct4_global、cilium_ct_any4_global どちらも共通のキーとバリューを持ち、マップのサイズは上限値はありますが、設定によって変更可能です。
BPF マップの定義
実際の定義は以下のようになっています。
cilium/bpf/lib/conntrack_map.h at v1.16.12 · cilium/cilium · GitHub
struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __type(key, struct ipv4_ct_tuple); __type(value, struct ct_entry); __uint(pinning, LIBBPF_PIN_BY_NAME); __uint(max_entries, CT_MAP_SIZE_TCP); } CT_MAP_TCP4 __section_maps_btf;
キー構造体(ipv4_ct_tuple)
キーとなる ipv4_ct_tuple は 5-tuple と flag をフィールドとして持ちます。
CT エントリは内部的に以下の三つのタイプに分類され、flag に埋め込まれます。
CT_SERVICE- Service の通信用
- Service 宛の通信の状態を保存して、転送すべきバックエンド Pod などを判断できるようにします
CT_EGRESS- Pod から外に出ていく(Egress)通信用
CT_INGRESS- Pod に入っていく(Ingress)通信用
cilium/bpf/lib/common.h at v1.16.12 · cilium/cilium · GitHub
struct ipv4_ct_tuple {
__be32 daddr;
__be32 saddr;
__be16 dport;
__be16 sport;
__u8 nexthdr;
__u8 flags;
} __packed;
バリュー構造体(ct_entry)
バリューとなる ct_entry は多くのフィールドを持ちます。ここで全てを紹介はできないので、適宜説明することとします。
cilium/bpf/lib/common.h at v1.16.12 · cilium/cilium · GitHub
struct ct_entry { __u64 reserved0; __u64 backend_id; __u64 packets; __u64 bytes; __u32 lifetime; __u16 rx_closing:1, tx_closing:1, reserved1:1, lb_loopback:1, seen_non_syn:1, node_port:1, proxy_redirect:1, dsr_internal:1, from_l7lb:1, reserved2:1, from_tunnel:1, reserved3:5; __u16 rev_nat_index; __u16 ifindex; __u8 tx_flags_seen; __u8 rx_flags_seen; __u32 src_sec_id; __u32 last_tx_report; __u32 last_rx_report; };
以降は Kubernetes の各通信方式ごとに Cilium の conntrack がどのように動作するか説明します。
※ 以降の解説では NodePort と LoadBalancer を同一視し、便宜上 LoadBalancer として解説します。
また、LoadBalancer については externalTrafficPolicy=Cluster のみを解説します。
※ Neco で現在利用している設定 Maglev hashing + DSR with Geneve encap に限って解説します。
※ 以降の解説では Cilium の eBPF のコードを GitHub へのリンクをポイントしながら解説します。Cilium の BPF プログラムは tail call やマクロを多用した非常に複雑なコードになっています。
エディタの定義ジャンプが機能しないため、コードを読む際は tail call を意識して文字列検索を駆使して頑張ってください。
Pod 間通信
最も基本的な通信です。そのため conntrack の操作も最もシンプルです。
しかし、これ以降に紹介する Service 関連の通信の基礎となるので非常に重要でもあります。
NodeA 上の Pod1(10.64.0.36) から NodeB 上の Pod2(10.64.0.64) への通信を例に動作を下図を用いて解説します。

Pod 間通信を行う時、送信側(Egress)と受信側(Ingress)双方で conntrack の処理が実行されます。そのため、ここでは Egress/Ingress に分けて説明します。
Pod1 からの Egress
Pod1 から Pod2 に通信をする時、コンテナ内部では一般的なコンテナと同様に Linux カーネルのネットワークスタックを通ってホスト側の veth デバイスにパケットが引き渡されます。
その後、cil_from_container プログラムが実行され、以下のような conntrack の処理が実行されて Pod2 にパケットが送出されます。
※ 箇条書きの番号は上図の NodeA 上の黒の番号と対応しています。
- CT map を lookup(検索)して既存のエントリがないか調べます
- cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
- 新規通信の場合、
CT_NEWというアクションが選択されます
- CT map に
CT_EGRESSのエントリを作成します- cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
- 上図の
NodeA CT mapに示したCT_EGRESSエントリを作成します - ICMP を扱うために、
cilium_ct_any4_globalCT map にもエントリが作成されます- このコネクションに対して ICMP パケットが返ってきた場合に Network Policy の判定をスキップするために作成されます
- ホスト上の
eth0にアタッチされたcil_to_netdevを通った後、Pod2 に向けて送出されます
以下が実際の conntrack エントリです。
node-a# cilium bpf ct list global | grep 10.64.0.64 TCP OUT 10.64.0.36:54260 -> 10.64.0.64:8000 expires=3026362 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3026352 TxFlagsSeen=0x1b LastTxReport=3026352 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0 ICMP OUT 10.64.0.36:0 -> 10.64.0.64:0 related expires=3026412 Packets=0 Bytes=0 RxFlagsSeen=0x00 LastRxReport=0 TxFlagsSeen=0x02 LastTxReport=3026352 Flags=0x0000 [ ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0
Pod2 への Ingress
Pod1 からのパケットを NodeB が受信すると、まずはホストデバイスの cil_from_netdev で必要な処理を実行します。
その後、コンテナのホスト側 veth に転送され、cil_to_container プログラムで以下のように conntrack に関連する処理が実施されます。
※ 箇条書きの番号は上図の NodeB 上の赤の番号と対応しています。
- 受信したパケットに紐づくエントリを lookup します
- cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
- 新規通信の場合、
CT_NEWアクションが選択されます
- CT map に
CT_INGRESSのエントリを作成します- cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
- 上図の
NodeB CT mapにCT_INGRESSエントリを作成します - ICMP を扱うために、
cilium_ct_any4_globalCT map にもエントリが作成されます
Pod1への返信パケットは、すでに (5) でCT_INGRESSエントリを作成しているため、このエントリを利用して管理され、送出されます
以下が実際の conntrack エントリです。
node-b$ cilium bpf ct list global | grep 10.64.0.64 TCP IN 10.64.0.36:54260 -> 10.64.0.64:8000 expires=3026362 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3026352 TxFlagsSeen=0x1b LastTxReport=3026352 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0 ICMP IN 10.64.0.36:0 -> 10.64.0.64:0 related expires=3026412 Packets=0 Bytes=0 RxFlagsSeen=0x02 LastRxReport=3026352 TxFlagsSeen=0x00 LastTxReport=0 Flags=0x0000 [ ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0
ClusterIP Service の通信
Cilium が管理する Kubernetes ネットワーク – ClusterIP Service の通信 で述べたように、パケットフローの観点からは ClusterIP の通信は Pod 間通信とあまり変わりませんが、conntrack の観点では少し複雑になっています。
本セクションでは NodeA 上の Pod1(10.64.0.36) から ClusterIP Service の SvcA(10.68.174.146:80) で、そのバックエンドの Pod2(10.64.0.98) に通信する場合を例に解説します。
ここでは、Pod1 の Egress/Ingress の処理に限って解説します。
※ ClusterIP 通信の Pod2 側の conntrack 処理は Pod 間通信と同じなので省略します。同じになる理由は Cilium が管理する Kubernetes ネットワーク – ClusterIP Service の通信 を参照してください。
Pod1 からの Egress
ClusterIP の通信が Pod 間通信と違うところは、Pod1 は ClusterIP の VIP(Virtual IP) 宛に通信するという点です。
しかし、実際には NodeA 上で Cilium がその ClusterIP に紐づくバックエンド Pod(今回は Pod2)に宛先を書き換えて転送します。
その過程で、Pod 間通信と異なる conntrack の処理が実行されます。
下図は Pod1(10.64.0.36) から ClusterIP Svc1(10.68.174.146) 宛の通信が Pod2(10.64.0.98) に転送されるまでの conntrack に関する処理を示しています。

Pod 間通信と同様に、Cilium 的に処理のエントリーポイントは cil_from_container です。
Pod1から通信を開始する時、その宛先が Service に紐づくアドレスかを Service map から検索します- cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
- 今回は ClusterIP 宛の通信のためその ClusterIP の情報を取得します
- パケットに紐づいた
CT_SERVICEのエントリを検索します- cilium/bpf/lib/lb.h at v1.16.12 · cilium/cilium · GitHub
- 新規通信の場合、
CT_NEWアクションが選択されます - (1) で取得した ClusterIP の情報をもとに、実際に転送するバックエンドを決定します
- cilium/bpf/lib/lb.h at v1.16.12 · cilium/cilium · GitHub
- 余談ですが、session affinity の処理もここで実行します
- CT map に
CT_SERVICEのエントリを作成します- 選択したバックエンドの ID を
ct_entry.backend_idに保存します - Ingress on Pod1 で後述する reverse NAT のための
ct_entry.rev_nat_indexも保存します
- 選択したバックエンドの ID を
- ClusterIP の VIP から選択したバックエンドの IP に DNAT(Destination NAT)します
- ここから Pod 間通信で通ったパスと合流して、CT map に
CT_EGRESSのエントリを作成します- 注意が必要なのは、この
CT_EGRESSのエントリは返信パケットの処理で利用されるので、(2) で作成したCT_SERVICEのエントリの情報を引き継いでエントリが作成されます
- 注意が必要なのは、この
以下が NodeA での CT map の様子です。
node-a# cilium bpf ct list global | grep 10.64.0.36 TCP SVC 10.64.0.36:59530 -> 10.68.174.146:80 expires=3034283 Packets=0 Bytes=0 RxFlagsSeen=0x00 LastRxReport=0 TxFlagsSeen=0x1b LastTxReport=3034273 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=15 SourceSecurityID=0 IfIndex=0 BackendID=19 TCP OUT 10.64.0.36:59530 -> 10.64.0.98:8000 expires=3034283 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3034273 TxFlagsSeen=0x1b LastTxReport=3034273 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=15 SourceSecurityID=3096 IfIndex=0 BackendID=0 ICMP OUT 10.64.0.36:0 -> 10.64.0.98:0 related expires=3034333 Packets=0 Bytes=0 RxFlagsSeen=0x00 LastRxReport=0 TxFlagsSeen=0x02 LastTxReport=3034273 Flags=0x0000 [ ] RevNAT=15 SourceSecurityID=3096 IfIndex=0 BackendID=0
Pod1 への Ingress
次に Pod2 からの返信パケットが Pod1 に到着するまでの処理を解説します。
Pod1 からの Egress で示したように、Pod2 への通信は最終的には単なる Pod 間通信です。そのため、Pod2 はこの通信が ClusterIP 宛の通信であることを認識せず、単に Pod1 への返信としてパケットを送出します。
※ 以下は NodeB 上の Conntrack エントリです。このように、NodeB では CT_SERVICE のエントリは作成されていません。
node-b# cilium bpf ct list global | grep 10.64.0.36 TCP IN 10.64.0.36:59530 -> 10.64.0.98:8000 expires=3034283 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3034273 TxFlagsSeen=0x1b LastTxReport=3034273 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0 ICMP IN 10.64.0.36:0 -> 10.64.0.98:0 related expires=3034333 Packets=0 Bytes=0 RxFlagsSeen=0x02 LastRxReport=3034273 TxFlagsSeen=0x00 LastTxReport=0 Flags=0x0000 [ ] RevNAT=0 SourceSecurityID=3096 IfIndex=0 BackendID=0
下図は Pod2(10.64.0.98) からの返信パケットが Pod1(10.64.0.36) に到着して、Pod1 が Svc1(10.68.174.146:80) からの返信としてパケットを受け取るまでを示しています。

BPF プログラムのエントリーポイントは cil_to_container です。
Pod2からの返信パケットがNodeAに到達して、Pod1のcil_to_containerで処理されます- CT map を検索して Pod1 からの Egress の (4) で作成した既存のエントリ(
CT_EGRESS)を取得します- cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
TAIL_CT_LOOKUP4というマクロ中のコードです
- 取得したエントリ中の
rev_nat_indexを元に、RevNAT map から ClusterIP の情報を取得し、reverse NAT します- cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
- ここで reverse NAT して、パケットを ClusterIP からの返信に見せかけます
- コンテナ側の veth デバイスで
Svc1からのパケットとして受信します
LoadBalancer Service の通信
Cilium が管理する Kubernetes ネットワーク – LoadBalancer Service の通信 で述べたように、LoadBalancer の通信はさらに複雑で、中間ノードとバックエンドノードそれぞれの Ingress/Egress の BPF プログラムで conntrack の処理が発生します。
ここでは、以下のセクションに分けて解説します。
- 中間ノードへの Ingress
- クライアントから一次受けの中間ノードでパケットを受信した際に Geneve でカプセル化して転送します
- バックエンドノードへの Ingress
- バックエンドノードで Geneve パケットを受信して Pod に転送します
- バックエンドノードからの Egress
- Pod からクライアントに DSR で送信します
本セクションでは以下のような LoadBalancer の通信を例に取り、中間ノードとバックエンドノードの Ingress/Egress に分けて解説します。
- クラスタ外部のクライアント(
172.191.0.1)から LoadBalancer Service のLB1(172.190.0.17:80)宛に通信する NodeAが中間ノードとして振る舞い、バックエンド Pod にNodeB上に存在するPod1(10.64.0.39)を選択して転送するPod1からクライアントに DSR する
中間ノードへの Ingress
下図はクラスタ外部のクライアント(172.191.0.1)から LoadBalancer Service の LB1(172.190.0.17:80) 宛に通信した時、NodeA が中間ノードとして振る舞い、NodeB 上の Pod1(10.64.0.39) に到達するまでを示しています。

中間ノードでの処理は図内の eth0 の Ingress にアタッチされた cil_from_netdev でまずは処理されます。
LoadBalancer(以下 LB) と NodePort の処理は nodeport_lb4 という関数で実行されます。
cilium/bpf/bpf_host.c at v1.16.12 · cilium/cilium · GitHub
- 宛先アドレスとポートから Service map を検索して、LB 宛のパケットの場合、LB の情報を取得します
- 取得した LB の情報とパケットを元に
CT_SERVICEのエントリを検索します- cilium/bpf/lib/lb.h at v1.16.12 · cilium/cilium · GitHub
- この辺りの処理は
lb4_localで処理されていて、ClusterIP の時と同一です - 新規通信の場合、
CT_NEWアクションが選択されます - (1) で取得した LB の情報をもとに、実際に転送するバックエンドを決定します
- cilium/bpf/lib/lb.h at v1.16.12 · cilium/cilium · GitHub
Maglev hashingに基づいてバックエンド Pod の選択を行います
- CT map に CT_SERVICE のエントリを作成します
- パケットの宛先をバックエンド Pod に DNAT します
- 選択したバックエンド Pod が中間ノード上に存在するかを判定します
- cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
- 中間ノードにバックエンド Pod が存在するなら CT map に
CT_EGRESSのエントリを作って Pod に転送します- cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
- 本記事では、選択したバックエンド Pod が中間ノード上に存在した場合の解説を省略します
- LB の情報を Geneve パケットのオプション領域に詰めて
cilium_geneveデバイスに転送します- cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
- Geneve のカスタムオプション構造体に、LB の
EXTERNAL-IPとポートを保存します - 保存した情報がリダイレクト時に引き渡されるように登録します
cilium_geneveへリダイレクトします
cilium_geneveでカプセル化され、 Geneve パケットがNodeBに向けて送信されます- 実際には
eth0を経由してNodeBに転送されますが、簡単のため、ここではcilium_geneveから矢印を出しています
- 実際には
以下は中間ノードの実際の CT map の様子です。
node-a# cilium bpf ct list global --time-diff | grep 172.191.0.1 TCP SVC 172.191.0.1:53438 -> 172.190.0.17:80 expires=3619996 (remaining: -305 sec(s)) Packets=0 Bytes=0 RxFlagsSeen=0x00 LastRxReport=0 TxFlagsSeen=0x1b LastTxReport=3619986 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=24 SourceSecurityID=0 IfIndex=0 BackendID=24
バックエンドノードへの Ingress
下図は NodeB が Geneve パケットを受け取って、カプセル化を解いて Pod1(10.64.0.39) にパケットを転送する様子を示しています。
バックエンドノードでは NAT map という BPF マップが利用されます。

バックエンドノードに到達した Geneve パケットはまず eth0 で受信してカーネル内部で cilium_geneve に転送されます。図では簡単のため cilium_geneve で直接受信するように描いています。主な処理は cilium_geneve にアタッチされた cil_from_overlay で実行されます。
-
cilium_geneveに Geneve でカプセル化した LB のパケットが到達しますcilium_geneveの Ingress TC で処理する段階ではすでにカーネルによってカプセル化が解かれています
-
中間ノードへの Ingress と同様に
nodeport_lb4が実行されますcil_from_overlayから実行されるので、IS_BPF_OVERLAY=1で以下が実行されます- DSR が必要なパケットの場合、Geneve オプションから LB のアドレスとポート情報を取り出します
- CT map から
CT_EGRESSかつ DSR フラグ(ct_entry.dsr_internal)が立っているエントリを検索します- cilium/bpf/lib/nodeport.h at v1.16.12 · cilium/cilium · GitHub
- 新規通信の場合、
CT_NEWアクションが選択されます - ここで作るエントリは Egress 用なので入ってきたパケットと宛先を逆向きにしてエントリを作ります
- CT map に
CT_EGRESSかつdsr_internal=trueでエントリを作ります
- DSR のために NAT map に (3) で作成した CT エントリのキーと LB の情報を保存します
Pod1の veth デバイスにパケットを転送します- cilium/bpf/bpf_overlay.c at v1.16.12 · cilium/cilium · GitHub
- パケットのデータ構造(
sk_buff)のメタデータ領域にfrom_tunnel=true(トンネルデバイスからのパケットであるフラグ)を保存します - 転送します
Pod1のホスト側 veth デバイスにアタッチされたcil_to_containerで処理されます- Pod 間通信の Ingress と同様の処理が実行されます
- 異なる点として、(4-b) で
sk_buffのメタデータに保存したfrom_tunnel=trueを扱うことが挙げられます
- 異なる点として、(4-b) で
- CT map を
CT_INGRESSかつct_entry.from_tunnel=trueでエントリを作成します
- Pod 間通信の Ingress と同様の処理が実行されます
以下はバックエンドノードの実際の CT map の様子です。
node-b# cilium bpf ct list global | grep 10.64.0.39 TCP IN 172.191.0.1:53438 -> 10.64.0.39:8000 expires=3619996 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3619986 TxFlagsSeen=0x1b LastTxReport=3619986 Flags=0x0413 [ RxClosing TxClosing SeenNonSyn FromTunnel ] RevNAT=0 SourceSecurityID=2 IfIndex=0 BackendID=0 TCP OUT 172.191.0.1:53438 -> 10.64.0.39:8000 expires=3619996 Packets=0 Bytes=0 RxFlagsSeen=0x1b LastRxReport=3619986 TxFlagsSeen=0x1b LastTxReport=3619986 Flags=0x0093 [ RxClosing TxClosing SeenNonSyn DSRInternal ] RevNAT=0 SourceSecurityID=2 IfIndex=0 BackendID=0 ICMP IN 172.191.0.1:0 -> 10.64.0.39:0 related expires=3620046 Packets=0 Bytes=0 RxFlagsSeen=0x02 LastRxReport=3619986 TxFlagsSeen=0x00 LastTxReport=0 Flags=0x0400 [ FromTunnel ] RevNAT=0 SourceSecurityID=2 IfIndex=0 BackendID=0
バックエンドノードからの Egress
下図は Pod1(10.64.0.39) がクライアントからのパケットを処理して、DSR でクライアントに直接返信パケットを送信する様子を示しています。

エントリーポイントは cil_from_container で、続いて cil_to_netdev に引き渡されて処理が実行されます。
- CT map を
CT_EGRESSで検索します- cilium/bpf/bpf_lxc.c at v1.16.12 · cilium/cilium · GitHub
- エントリが存在するので、それに従って必要な処理をします
-
eth0にパケットを転送します -
eth0上のcil_to_netdevで DSR のパケットか判定して、NAT map を検索します- cilium/bpf/bpf_host.c at v1.16.12 · cilium/cilium · GitHub
- NAT map から取得した情報を元に、パケットの送信元アドレスとポートを LB のものに SNAT します
- 外部のクライアントに転送します
Cilium の conntrack の状態管理
Conntrack は CT map にエントリを作って終わりではありません。特に TCP の場合、End to End のコネクションの状態を追跡、管理しなければなりません。
Cilium は BPF プログラム上で観測した TCP フラグに応じて conntrack エントリのフラグを立てて状態を管理しています。
CT エントリの詳細は CT map – バリュー構造体(ct_entry)を参照して下さい。
TCP の状態管理で重要なのは主に以下のフィールドです。
- lifetime
- そのエントリの有効期限が切れる時刻のタイムスタンプ(以降、生存期間と記述します)
- 生存期間が切れていたら、後述の GC で削除されます
- tx_closing
- 送信側の TCP FIN を観測したら立ちます
- rx_closing
- 受信側の TCP FIN を観測したら立ちます
- seen_no_syn
- TCP SYN 以外のフラグのついた TCP パケットを観測したら立ちます
tx_closing/rx_closing
tx_closing と rx_closing は TCP の終了処理に関連するフラグで、送信側、受信側それぞれで TCP FIN フラグを観測した場合に 1 がセットされます。ct_tcp_select_action 関数で TCP パケットのフラグを解析して、対応する CT エントリにどのような操作を行うかを決定します。FIN か RST を観測すると ACTION_CLOSE が返ります。
cilium/bpf/lib/conntrack.h at v1.16.12 · cilium/cilium · GitHub
ACTION_CLOSE が選択されると、__ct_lookup 関数で CT エントリを取得したときに、それぞれの CT タイプ(ここでは dir 変数)に応じて、tx_closing と rx_closing のフラグがセットされます。
CT_SERVICE のエントリについては ClusterIP の時と LoadBalancer(NodePort) + DSR の時でパケットの向きは異なりますが、どちらも TCP フラグを片方向からしか観測できません。
そのため、片方の FIN を観測した時に tx_closing 、rx_closing 共にセットしています。
- ClusterIP
- LoadBalancer(NodePort) + DSR
CT エントリがまだ有効かどうかは ct_entry_alive 関数で判定されます。
逆に、これが false を返した時は、そのエントリのコネクションは終了したとみなされます。
cilium/bpf/lib/conntrack.h at v1.16.12 · cilium/cilium · GitHub
lifetime
前述の通り、lifetime はそのエントリの生存期間です。TCP や UDP のプロトコル、追跡している TCP の状態などにより細かく生存期間が定められています。
その値は cilium-agent のフラグで指定できます。v1.16.12 現在は以下のようになっています。
--bpf-ct-timeout-regular-any duration Timeout for entries in non-TCP CT table (default 1m0s) --bpf-ct-timeout-regular-tcp duration Timeout for established entries in TCP CT table (default 2h13m20s) --bpf-ct-timeout-regular-tcp-fin duration Teardown timeout for entries in TCP CT table (default 10s) --bpf-ct-timeout-regular-tcp-syn duration Establishment timeout for entries in TCP CT table (default 1m0s) --bpf-ct-timeout-service-any duration Timeout for service entries in non-TCP CT table (default 1m0s) --bpf-ct-timeout-service-tcp duration Timeout for established service entries in TCP CT table (default 2h13m20s) --bpf-ct-timeout-service-tcp-grace duration Timeout for graceful shutdown of service entries in TCP CT table (default 1m0s)
cilium/Documentation/cmdref/cilium-agent.md at v1.16.12 · cilium/cilium · GitHub
これらのフラグの値は最終的に、以下の定数に埋め込まれて BPF プログラム上で利用されます。
#define CT_CONNECTION_LIFETIME_TCP 21600 #define CT_CONNECTION_LIFETIME_NONTCP 60 #define CT_SERVICE_LIFETIME_TCP 21600 #define CT_SERVICE_LIFETIME_NONTCP 60 #define CT_SERVICE_CLOSE_REBALANCE 30 #define CT_SYN_TIMEOUT 60 #define CT_CLOSE_TIMEOUT 10
cilium/bpf/node_config.h at v1.16.12 · cilium/cilium · GitHub
lifetime も様々な箇所で更新されますが、主に更新するのは ct_update_timeout 関数です。
cilium/bpf/lib/conntrack.h at v1.16.12 · cilium/cilium · GitHub
この関数から呼び出される __ct_update_timeout 関数で lifetime の値を 現在時刻 + その状態のタイムアウト値 で上書きします。
cilium/bpf/lib/conntrack.h at v1.16.12 · cilium/cilium · GitHub
ct_update_timeout は上述の __ct_lookup 関数の中で呼び出されるため、パケット毎に生存期間は更新されます。
このようにして、BPF プログラム上ではエントリの生存期間を管理していて、期限切れのエントリの削除は後述の GC により実施されます。
ガーベジコレクション(GC)
これまでは、BPF プログラム上で Cilium の conntrack がどのように実装されているかを見てきました。
Cilium は conntrack の GC を cilium-agent 内のコンポーネントとして実装しており、CT map のエントリ削除を全て GC で実施しています。
GC がトリガーされる要因は以下の 2 つです。
- インターバル
- 生存期間(
lifetime)が切れたエントリを全て削除します
- 生存期間(
- endpoint regeneration (本記事ではスコープ外です)
- Pod が削除されるなど、様々なイベントによりトリガーされます
- 対象の Pod に関連するエントリを全て削除します
- この時、対象 Pod と無関係な生存期間切れのエントリは削除されません
ここではインターバルによりトリガーされる GC を解説します。
GC インターバル計算
以下に示す goroutine が cilium-agent 起動時に起動して、GC インターバル毎に GC が実行されます。
cilium/pkg/maps/ctmap/gc/gc.go at v1.16.12 · cilium/cilium · GitHub
Cilium はデフォルトでは GC インターバルはあらかじめ設定された最大、最小値の範囲内で自動計算された値を利用します。
cilium/pkg/maps/ctmap/ctmap.go at v1.16.12 · cilium/cilium · GitHub
func calculateInterval(prevInterval time.Duration, maxDeleteRatio float64) (interval time.Duration) { interval = prevInterval if maxDeleteRatio == 0.0 { return } switch { case maxDeleteRatio > 0.25: if maxDeleteRatio > 0.9 { maxDeleteRatio = 0.9 } interval = time.Duration(float64(interval) * (1.0 - maxDeleteRatio)).Round(time.Second) if interval < defaults.ConntrackGCMinInterval { interval = defaults.ConntrackGCMinInterval } case maxDeleteRatio < 0.05: interval = time.Duration(float64(interval) * 1.5).Round(time.Second) if interval > defaults.ConntrackGCMaxLRUInterval { interval = defaults.ConntrackGCMaxLRUInterval } } cachedGCInterval = interval return }
前回の GC のエントリ削除率 maxDeleteRatio と、前回の GC インターバル prevInterval から次の GC インターバル interval を算出します。
maxDeleteRatio が 25% より大きい時は以下の式に従って interval を計算します。
interval=prevInterval* (1.0 –maxDeleteRatio)
つまり、次の GC は前回の GC よりも短い間隔で実行されます。
一方、maxDeleteRatio が 5% より小さい時は以下の式に従います。
interval=prevInterval* 1.5
つまり、次の GC は前回の GC よりも 1.5 倍長い間隔で実行されます。
最後に、計算した値が設定した最大、最小値の範囲内にあるかどうかを確かめて、次回の GC インターバルが決定します。
GC の内部処理
GC のエントリーポイントは runGC 関数です。
cilium/pkg/maps/ctmap/gc/gc.go at v1.16.12 · cilium/cilium · GitHub
cilium_ct4_global と cilium_ct_any4_global それぞれに対して生存期間切れのエントリの削除が実行されます。
各マップのエントリの削除は doGC4 関数内で処理されます。
cilium/pkg/maps/ctmap/ctmap.go at v1.16.12 · cilium/cilium · GitHub
最終的に、Map.DumpReliablyWithCallback(cb DumpCallback, stats *DumpStats) という関数が実行されて、マップ内の全てのエントリを Lookup して、それらに対して引数に渡している DumpCallback 型のコールバック関数 cb がそれぞれのエントリに対して呼び出されます。
cilium/pkg/bpf/map_linux.go at v1.16.12 · cilium/cilium · GitHub
ここに渡すコールバック関数は doGC4 内で定義されていて、このコールバック関数の中で、削除対象であればエントリを Remove します。
cilium/pkg/maps/ctmap/ctmap.go at v1.16.12 · cilium/cilium · GitHub
生存期間切れかどうかは以下で検査しています。
cilium/pkg/maps/ctmap/ctmap.go at v1.16.12 · cilium/cilium · GitHub
ここで、Lookup、Remove を強調したのは、これらが個別のシステムコールとして呼び出されるためです。
つまり、GC では 各マップの総エントリ数 + 削除対象エントリ数 分の bpf システムコールが発行されます。数百万エントリ存在するような CT map の GC にはかなりの時間と CPU 負荷がかかります。
※ Cilium 1.18 から、CT map の Lookup が Batch Lookup に変更されました。
これは bpf_map_lookup_batch を利用したもので、1 回のシステムコールで指定した数のエントリを一気に Lookup できます。
これにより、GC の負荷低減が期待されます。
Neco では過去に CT map 使用率が 80% まで増大したことがあります。
その時の状況や対処について、過去にブログを書いているので興味のある方は読んでみてください。
まとめ
以上、Kubernetes ネットワークにおける Cilium の conntrack の挙動と実装を解説しました。
eBPF によるパケット処理の実装では、基本的には Egress/Ingress それぞれの Pod が存在するノード上に、それぞれの方向の CT エントリが作成されます。また、それに応じて ICMP 用のエントリも作成されます。
それに加えて、Service 経由の通信は、その通信が Service のものであることを示すために特別な CT エントリが別途作成されることになります。
それらの CT エントリを通信の状態に応じて更新しています。
また、cilium-agent が定期的に GC して、CT map を健全な状態に保っています。
このように、Cilium は eBPF を用いて conntrack を実装しており、これを基礎として高度な通信制御を実現しています。
コメント