🥅

[Kubernetes 1.30] kube-proxy の nftables モード

2024/05/17に公開

kube-proxy

  • Service へのトラフィックをプロキシするコンポーネントのデフォルト実装

    • e.g.) Cluster IP への通信を Pod IP にリダイレクトする
    • EndpointSlice, Service, Node などのオブジェクトの変更を検知して Service を介したトラフィックのルーティングを可能にする
  • Container Network Interface (CNI) vs kube-proxy

    • CNI が Pod 間で通信できるように Pod IP の払い出しやルーティングをセットアップする

    • Pod は一時的なものかつ Pod IP は再利用されるので、Pod IP に信頼性はない

      • Pod IP を直接指定して通信するのは現実的ではない
    • Pod IP を抽象化したサービス検出の仕組みの一端を担うのが kube-proxy

      • Kubernetes におけるサービス検出は Service プロキシ + Cluster DNS (e.g. CoreDNS)

      CNI Benchmark: Understanding Cilium Network Performance

      CNI Benchmark: Understanding Cilium Network Performance

  • パフォーマンスの懸念や CNI 連携のし易さから kube-proxy を利用しないケースも

  • kube-proxy はプロキシの実装を --proxy-mode のフラグで変更可能

  • kube-proxy がサポートしているプロキシモード

    • userspace (Kubernetes 1.26 で削除済み)
    • iptables
    • ipvs
    • nftables (1.30 時点でアルファ機能)
    • kernelspace (Windows 限定)

userspace モード


https://kubernetes.io/ja/docs/concepts/services-networking/service/#proxy-mode-userspace

  • Kubernetes 1.2 で iptables モードがデフォルトになる以前は利用されていた
  • iptables のルールでトラフィックをホスト上のポート番号にリダイレクトし (source)、Go のプロキシサーバがリクエストを終端。Service にぶら下がった Pod にリクエストをプロキシする。(source)

iptables モード

  • Kubernetes 1.2 から kube-proxy のデフォルトのプロキシモードとして使われてきた
    • ユーザー数も多く、長年使われてきて安定性は高い
  • パケットのフィルタリングや NAT に使われる iptables をベースに実装
  • iptables の課題
    • Linux におけるファイアウォール機能を実現するために開発された
      • 複雑な負荷分散のアルゴリズムは未搭載
    • Kubernetes のように動的に IP が変わる環境を想定して作られていない
      • Service と Endpoints の数が増えるとルールの更新などのパフォーマンスが悪化

ipvs モード

  • Kubernetes 1.8 で導入され、Kubernetes 1.11 で GA
  • IPVS は iptables と同様に Linux の netfilter サブシステムをベースに実装されている
  • iptables モードの置き換えを目指して開発が始まった
    • 大規模クラスタでのパフォーマンスの改善
      • データ構造にハッシュテーブルを利用
    • IPVS が持つ豊富な負荷分散アルゴリズム
      • wrr (重み付けラウンドロビン), lc (最小コネクション数), …
  • iptables モードの置き換えは実現しなかった
    • iptables モードの機能を全て網羅できておらず、品質も高くない (テストが少ない)
    • IPVS の kernel モジュールが入っていない一部の Linux ディストロで導入の障壁に
    • IPVS だと Service プロキシの要件を実現できないので、iptables を多用している

nftables モード

  • KEP-3866: Add an nftables-based kube-proxy backend で提案された機能で、Kubernetes v1.29 でアルファ機能として入った。Kubernetes 1.31 でベータ昇格を目指している。
  • nftables モードの開発が始まった背景
    • iptables のパフォーマンスの課題や ipvs の制限

    • Linux ディストリビューションが iptables から nftables への移行を進めている

    • 一部のディストリビューションで iptables の削除が予定されている

      • 数年後にリリース予定の RHEL 10 で削除予定
    • Linux カーネルから iptables の機能が削除される予定はないが Linux ディストリビューションの判断で同梱されなくなる可能性がある

    • iptables 1.8.0 から iptables は nftables モードで動作する形になっている

      • nftables モードの iptables は互換性の観点から新しい nftables の機能 (e.g. map) が使えないので、パフォーマンスは改善しない。
      /# iptables -V
      iptables v1.8.9 (nf_tables)
      

nftables モードの仕組み

iptables モードの詳細についてはいろいろな記事や発表で紹介されているので、nftables モードの仕組みについて見ていきます。現在の nftables モードは iptables モードをベースに移植しており、nftables 固有の機能を使う形で実装されています。

Kubernetes 1.30 クラスタの準備

kind v0.23.0 から kube-proxy の nftables モードをサポートしているのでローカル環境で検証することができます。

v0.23.0 の kind をインストール

go install sigs.k8s.io/kind@v0.23.0

kind で Kubernetes 1.30 クラスタを起動

  • nftables モードはアルファ機能のため FeatureGate で有効化する必要がある
  • control plane のノードが 1 台と worker ノードが 2 台の構成
  • kube-proxy のログレベルを高くしている
cat <<EOF | kind create cluster --config -
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  NFTablesProxyMode: true
networking:
  kubeProxyMode: nftables
nodes:
- role: control-plane
  image: kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e
  kubeadmConfigPatches:
  - |
    kind: KubeProxyConfiguration
    metadata:
      name: config
    logging:
      verbosity: 6
- role: worker
  image: kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e
  kubeadmConfigPatches:
  - |
    kind: KubeProxyConfiguration
    metadata:
      name: config
    logging:
      verbosity: 6
- role: worker
  image: kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e
  kubeadmConfigPatches:
  - |
    kind: KubeProxyConfiguration
    metadata:
      name: config
    logging:
      verbosity: 6
EOF

nftables

kind-worker の Node のシェルを取得

docker exec -it kind-worker bash

nft バイナリを使用して nftables のテーブルを表示

nft list tables
実行結果
/# nft list tables
table ip nat
table ip mangle
table ip filter
table ip6 mangle
table ip6 nat
table ip6 filter
table ip kube-proxy  # <- IPv4 に対する kube-proxy 用のテーブル
table ip6 kube-proxy # <- IPv6 に対する kube-proxy 用のテーブル

nftables に設定されている ip ファミリー (IPv4) のルール一覧を表示

nft list ruleset ip
実行結果の一部
table ip kube-proxy {
	set cluster-ips {
		type ipv4_addr
		elements = { 10.96.0.1, 10.96.0.10 }
	}

	set nodeport-ips {
		type ipv4_addr
		elements = { 192.168.228.2 }
	}

	map no-endpoint-services {
		type ipv4_addr . inet_proto . inet_service : verdict
	}

	map no-endpoint-nodeports {
		type inet_proto . inet_service : verdict
	}

	map firewall-ips {
		type ipv4_addr . inet_proto . inet_service : verdict
	}

	map service-ips {
		type ipv4_addr . inet_proto . inet_service : verdict
		elements = { 10.96.0.10 . tcp . 53 : goto service-NWBZK7IH-kube-system/kube-dns/tcp/dns-tcp,
			     10.96.0.10 . udp . 53 : goto service-FY5PMXPG-kube-system/kube-dns/udp/dns,
			     10.96.0.1 . tcp . 443 : goto service-2QRHZV4L-default/kubernetes/tcp/https,
			     10.96.0.10 . tcp . 9153 : goto service-AS2KJYAD-kube-system/kube-dns/tcp/metrics }
	}

	map service-nodeports {
		type inet_proto . inet_service : verdict
	}

	chain filter-prerouting {
		type filter hook prerouting priority dstnat - 10; policy accept;
		ct state new jump firewall-check
	}

	chain filter-input {
		type filter hook input priority -110; policy accept;
		ct state new jump nodeport-endpoints-check
		ct state new jump service-endpoints-check
	}

	chain filter-forward {
		type filter hook forward priority -110; policy accept;
		ct state new jump service-endpoints-check
		ct state new jump cluster-ips-check
	}

	chain filter-output {
		type filter hook output priority -110; policy accept;
		ct state new jump service-endpoints-check
		ct state new jump firewall-check
	}

	chain filter-output-post-dnat {
		type filter hook output priority -90; policy accept;
		ct state new jump cluster-ips-check
	}

	chain nat-prerouting {
		type nat hook prerouting priority dstnat; policy accept;
		jump services
	}

	chain nat-output {
		type nat hook output priority -100; policy accept;
		jump services
	}

	chain nat-postrouting {
		type nat hook postrouting priority srcnat; policy accept;
		jump masquerading
	}

	chain nodeport-endpoints-check {
		ip daddr @nodeport-ips meta l4proto . th dport vmap @no-endpoint-nodeports
	}

	chain service-endpoints-check {
		ip daddr . meta l4proto . th dport vmap @no-endpoint-services
	}

	chain firewall-check {
		ip daddr . meta l4proto . th dport vmap @firewall-ips
	}

	chain services {
		ip daddr . meta l4proto . th dport vmap @service-ips
		ip daddr @nodeport-ips meta l4proto . th dport vmap @service-nodeports
	}

	chain masquerading {
		meta mark & 0x00004000 == 0x00000000 return
		meta mark set meta mark ^ 0x00004000
		masquerade fully-random
	}

	chain cluster-ips-check {
		ip daddr @cluster-ips reject comment "Reject traffic to invalid ports of ClusterIPs"
	}

	chain mark-for-masquerade {
		meta mark set meta mark | 0x00004000
	}

	chain reject-chain {
		reject
	}

	chain endpoint-4GDGBDUZ-default/kubernetes/tcp/https__192.168.228.3/6443 {
		ip saddr 192.168.228.3 jump mark-for-masquerade
		meta l4proto tcp dnat to 192.168.228.3:6443
	}

	chain service-2QRHZV4L-default/kubernetes/tcp/https {
		ip daddr 10.96.0.1 tcp dport 443 ip saddr != 10.244.0.0/16 jump mark-for-masquerade
		numgen random mod 1 vmap { 0 : goto endpoint-4GDGBDUZ-default/kubernetes/tcp/https__192.168.228.3/6443 }
	}

	chain endpoint-2GKX576Q-kube-system/kube-dns/udp/dns__10.244.0.2/53 {
		ip saddr 10.244.0.2 jump mark-for-masquerade
		meta l4proto udp dnat to 10.244.0.2:53
	}

	chain endpoint-VWYSKC3H-kube-system/kube-dns/udp/dns__10.244.0.4/53 {
		ip saddr 10.244.0.4 jump mark-for-masquerade
		meta l4proto udp dnat to 10.244.0.4:53
	}

	chain service-FY5PMXPG-kube-system/kube-dns/udp/dns {
		ip daddr 10.96.0.10 udp dport 53 ip saddr != 10.244.0.0/16 jump mark-for-masquerade
		numgen random mod 2 vmap { 0 : goto endpoint-2GKX576Q-kube-system/kube-dns/udp/dns__10.244.0.2/53, 1 : goto endpoint-VWYSKC3H-kube-system/kube-dns/udp/dns__10.244.0.4/53 }
	}

	chain endpoint-BFW5VCWB-kube-system/kube-dns/tcp/dns-tcp__10.244.0.2/53 {
		ip saddr 10.244.0.2 jump mark-for-masquerade
		meta l4proto tcp dnat to 10.244.0.2:53
	}

	chain endpoint-5ZODOYIN-kube-system/kube-dns/tcp/dns-tcp__10.244.0.4/53 {
		ip saddr 10.244.0.4 jump mark-for-masquerade
		meta l4proto tcp dnat to 10.244.0.4:53
	}

	chain service-NWBZK7IH-kube-system/kube-dns/tcp/dns-tcp {
		ip daddr 10.96.0.10 tcp dport 53 ip saddr != 10.244.0.0/16 jump mark-for-masquerade
		numgen random mod 2 vmap { 0 : goto endpoint-BFW5VCWB-kube-system/kube-dns/tcp/dns-tcp__10.244.0.2/53, 1 : goto endpoint-5ZODOYIN-kube-system/kube-dns/tcp/dns-tcp__10.244.0.4/53 }
	}

	chain endpoint-SUJXBOPR-kube-system/kube-dns/tcp/metrics__10.244.0.2/9153 {
		ip saddr 10.244.0.2 jump mark-for-masquerade
		meta l4proto tcp dnat to 10.244.0.2:9153
	}

	chain endpoint-UIZQ3LM6-kube-system/kube-dns/tcp/metrics__10.244.0.4/9153 {
		ip saddr 10.244.0.4 jump mark-for-masquerade
		meta l4proto tcp dnat to 10.244.0.4:9153
	}

	chain service-AS2KJYAD-kube-system/kube-dns/tcp/metrics {
		ip daddr 10.96.0.10 tcp dport 9153 ip saddr != 10.244.0.0/16 jump mark-for-masquerade
		numgen random mod 2 vmap { 0 : goto endpoint-SUJXBOPR-kube-system/kube-dns/tcp/metrics__10.244.0.2/9153, 1 : goto endpoint-UIZQ3LM6-kube-system/kube-dns/tcp/metrics__10.244.0.4/9153 }
	}
}

iptables と異なり table を自由に作成可能。テーブルはアドレスファミリー (e.g. ipip6, inet, arp, …) 毎に作成可能で kube-proxy は ipip6 のテーブルをそれぞれ作成します。

table ip kube-proxy {
  ...
}

set (集合) や map (連想配列) のデータ構造が利用でき、高速なデータの参照が可能です。

	set cluster-ips {
		type ipv4_addr
		elements = { 10.96.0.1, 10.96.0.10 }
	}
  • type には ipv4_addrinet_proto などデータ型を指定可能
  • set 型はキーが存在するか参照が可能で、elements に要素を書き込んでいく

map にはキーと値を指定。値に verdict 文 (e.g. accept, drop, return, goto, jump, …) を指定した vmap (verdict map) もある。

	map service-ips {
		type ipv4_addr . inet_proto . inet_service : verdict
		elements = { 10.96.0.10 . tcp . 53 : goto service-NWBZK7IH-kube-system/kube-dns/tcp/dns-tcp,
			     10.96.0.10 . udp . 53 : goto service-FY5PMXPG-kube-system/kube-dns/udp/dns,
			     10.96.0.1 . tcp . 443 : goto service-2QRHZV4L-default/kubernetes/tcp/https,
			     10.96.0.10 . tcp . 9153 : goto service-AS2KJYAD-kube-system/kube-dns/tcp/metrics }
	}
  • 要素の . は連結を意味し、上記の例だと IP アドレスとプロトコル、ポート番号を連結したものがキーとなっている
    • map / vmap のデータ型として複数のデータ型を連結したものが利用できる

chain にルールを割り当てる。iptables のように事前作成された chain (e.g. INPUT, OUTPUT, …) は存在せず利用者が作成する必要がある。

	chain nat-prerouting {
		type nat hook prerouting priority dstnat; policy accept;
		jump services
	}
  • type に nat や filter, route の事前定義された chain の種類を指定可能
    • nat や filter, route の事前定義された chain を Base chain と呼ぶ
    • ユーザーが独自に定義した Regular chain も利用可能
    • kube-proxy は Base chain と Regular chain を両方とも利用
  • hook に prerouting や forward などの netfilter のフックを指定
  • priority にルールの優先度を指定。値が小さいほど優先度は高い
    • 事前定義された優先度 (e.g. dstnat → -100) も存在する
    • raw (-300) → dstnat (-100) → filter (0) → srcnat (100) のように事前定義された優先度で netfilter のフックの優先順が表現されている
  • policy は現状 accept か drop を指定可能
    • drop の場合、その chain のルールを最後まで評価し終わったタイミングでパケットを破棄

netfilter の hook とパケットフロー

引用: https://github.com/kubernetes/kubernetes/tree/v1.30.0/pkg/proxy/nftables

             +================+      +=====================+
             | hostNetwork IP |      | hostNetwork process |
             +================+      +=====================+
                         ^                |
  -  -  -  -  -  -  -  - | -  -  -  -  - [*] -  -  -  -  -  -  -  -  -
                         |                v
                     +-------+        +--------+
                     | input |        | output |
                     +-------+        +--------+
                         ^                |
      +------------+     |   +---------+  v      +-------------+
      | prerouting |-[*]-+-->| forward |--+-[*]->| postrouting |
      +------------+         +---------+         +-------------+
            ^                                           |
 -  -  -  - | -  -  -  -  -  -  -  -  -  -  -  -  -  -  |  -  -  -  -
            |                                           v
       +---------+                                  +--------+
   --->| ingress |                                  | egress |--->
       +---------+                                  +--------+
  • kube-proxy は ingress や egress のフックに関与しない
  • Service 経由の通信の場合、prerouting → forward → postrouting のフックを通る
    • 送信元と送信先の Pod がどの Node で動いているかによらず (i.e. 同一 Node or 別の Node)、netfilter の同じフックを通る
    • Pod 間通信のルーティングに関しては CNI やクラウドのネットワーク基盤の実装による
  • Service 経由で Pod から Node 上のプロセスや hostNetwork: true の Pod への通信は、prerouting → input のフックを通る
  • Service 経由で Node 上のプロセスや hostNetwork: true の Pod から Pod への通信の場合、output → postrouting のフックを通る

nftables モードの挙動

Service を介した Pod 間の通信

server と client のアプリケーションを NodeSelector を利用して別々の Node にデプロイし、Cluster IP で公開します。client から server への通信の流れを見ていきます。

アプリケーションと Service の作成
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
  labels:
    app: backends
spec:
  replicas: 3
  selector:
    matchLabels:
      app: backends
  template:
    metadata:
      labels:
        app: backends
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        args:
        - netexec
        - --http-port=80
        - --delay-shutdown=30
      nodeSelector:
        kubernetes.io/hostname: kind-worker
---
apiVersion: v1
kind: Service
metadata:
  name: backends
spec:
  type: ClusterIP
  selector:
    app: backends
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
  labels:
    app: client
spec:
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        command: ["sleep", "infinity"]
      nodeSelector:
        kubernetes.io/hostname: kind-worker2
EOF

作成した Pod

❯ kubectl get pods -owide
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE           NOMINATED NODE   READINESS GATES
backends-6f6d65ffcd-frvcg   1/1     Running   0          44s   10.244.2.11   kind-worker    <none>           <none>
backends-6f6d65ffcd-hs452   1/1     Running   0          44s   10.244.2.12   kind-worker    <none>           <none>
backends-6f6d65ffcd-s56pg   1/1     Running   0          44s   10.244.2.10   kind-worker    <none>           <none>
client-778b7d784-vwrk5      1/1     Running   0          44s   10.244.1.5    kind-worker2   <none>           <none>

払い出された ClusterIP

❯ kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
backends     ClusterIP   10.96.160.122   <none>        80/TCP    70s
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   7h8m

client の Pod が存在する kind-worker2 の Node のシェルを取得

docker exec -it kind-worker2 bash

nftables のルールの詳細を表示

nft list ruleset ip

kube-proxy が作成した nftables の chain を見ていきます。Service を介した Pod 間の通信の場合、kind-worker2 の prerouting → forward → postrouting のフックを通り、kind-worker の prerouting → forward → postrouting のフックを通って Pod に辿り着きます。

      +------------+         +---------+         +-------------+
      | prerouting |-[*]-+-->| forward |--+-[*]->| postrouting |
      +------------+         +---------+         +-------------+
            ^                                           |
 -  -  -  - | -  -  -  -  -  -  -  -  -  -  -  -  -  -  |  -  -  -  -
            |                                           v
        +--------+                                  +--------+
        | client |                                  | server |
        +--------+                                  +--------+

まずは、prerouting のフックから見ていきます。

  chain filter-prerouting {
	  type filter hook prerouting priority dstnat - 10; policy accept;
	  ct state new jump firewall-check
  }

	chain firewall-check {
		ip daddr . meta l4proto . th dport vmap @firewall-ips
	}

	map firewall-ips {
		type ipv4_addr . inet_proto . inet_service : verdict
	}

  chain nat-prerouting {
	  type nat hook prerouting priority dstnat; policy accept;
	  jump services
  }

prerouting のフックに関わる chain は 2 つあります。

  • filter-preroutingnat-prerouting も filter に関わる Base chain
  • 優先度から filter-prerouting -> nat-prerouting の順番でルールを評価する
  • filter-prerouting のルールは conntrack (netfilter のコネクション追跡の機能) に接続情報のない新規接続の場合、firewall-check の chain に移動します。ルールの評価が終わったら filter-prerouting の chain に戻ってくる
  • firewall-check はユーザー定義の Regular chain で firewall-ips の vmap を参照して verdict 式を実行する
    • firewall-check の chain は Service type LoadBalancer の loadBalancerSourceRanges で利用 (他にもあるかも?)
  • nat-preroutingservices の chain に移動してルールの評価が終わったら nat-prerouting の chain に戻ってくる

services の chain のルールを見ていきます。

	chain services {
		ip daddr . meta l4proto . th dport vmap @service-ips
		ip daddr @nodeport-ips meta l4proto . th dport vmap @service-nodeports
	}

	map service-ips {
		type ipv4_addr . inet_proto . inet_service : verdict
		elements = { 10.96.0.10 . tcp . 53 : goto service-NWBZK7IH-kube-system/kube-dns/tcp/dns-tcp,
			     10.96.0.10 . udp . 53 : goto service-FY5PMXPG-kube-system/kube-dns/udp/dns,
			     10.96.109.220 . tcp . 80 : goto service-GZEOLGI7-default/backends/tcp/,
			     10.96.0.1 . tcp . 443 : goto service-2QRHZV4L-default/kubernetes/tcp/https,
			     10.96.0.10 . tcp . 9153 : goto service-AS2KJYAD-kube-system/kube-dns/tcp/metrics }
	}

	map service-nodeports {
		type inet_proto . inet_service : verdict
	}
  • services の chain のルールを評価します。services-ips の map を <送信先 IP アドレス>.<プロトコル>.<送信先ポート番号> のキーで参照し、verdict 文を実行する。
    • th はトランスポートヘッダー (skbuff の transport_header)
  • goto 文を実行し service- の chain に移動
    • service-GZEOLGI7-default/backends/tcp/ に移動
      • tcp/ で終わっているのは Service でポート名 (spec.ports[0].name) を明示していないから

service- の chain のルールを見ていきます。

	chain service-GZEOLGI7-default/backends/tcp/ {
		ip daddr 10.96.109.220 tcp dport 80 ip saddr != 10.244.0.0/16 jump mark-for-masquerade
		numgen random mod 3 vmap { 0 : goto endpoint-T3CG7JQT-default/backends/tcp/__10.244.1.16/80, 1 : goto endpoint-KHR7BBTM-default/backends/tcp/__10.244.1.17/80, 2 : goto endpoint-6E5VCUSA-default/backends/tcp/__10.244.1.18/80 }
	}

	chain mark-for-masquerade {
		meta mark set meta mark | 0x00004000
	}

	chain endpoint-T3CG7JQT-default/backends/tcp/__10.244.1.16/80 {
		ip saddr 10.244.1.16 jump mark-for-masquerade
		meta l4proto tcp dnat to 10.244.1.16:80
	}

	chain endpoint-KHR7BBTM-default/backends/tcp/__10.244.1.17/80 {
		ip saddr 10.244.1.17 jump mark-for-masquerade
		meta l4proto tcp dnat to 10.244.1.17:80
	}

	chain endpoint-6E5VCUSA-default/backends/tcp/__10.244.1.18/80 {
		ip saddr 10.244.1.18 jump mark-for-masquerade
		meta l4proto tcp dnat to 10.244.1.18:80
	}
  • 送信先の IP アドレスが 10.96.37.150 (Cluster IP の VIP) かつプロトコルが TCP かつ送信元 IP アドレスが Pod に割り当てる IP 範囲 (kube-proxy に指定した clusterCIDR) でないなら、mark-for-masquerade の chain に移動

    • postrouting 時に SNAT するための印をビット OR で付与

    • SNAT は postrouting chain の中で行う必要があるので、パケットに印を付けて後で処理できるようにしている

    • clusterCIDR は kube-proxy の設定で確認可能

      ❯ kubectl get ds -n kube-system kube-proxy -ojsonpath='{.spec.template.spec.containers[0].command}'
      ["/usr/local/bin/kube-proxy","--config=/var/lib/kube-proxy/config.conf","--hostname-override=$(NODE_NAME)"]%
      
      ❯ kubectl exec -it -n kube-system ds/kube-proxy -- sh
      # cat /var/lib/kube-proxy/config.conf | grep clusterCIDR
      clusterCIDR: 10.244.0.0/16
      
  • ランダムな数字を Service にぶら下がる Enddpoint の数で割った余りに応じて移動する chain が異なる

    • numgen random でランダムな数字を生成して mod 3 で 3 で割った余りを計算し、それをキーとして vmap を参照して verdict 文を実行
    • ラウンドロビンではない点に注意
  • endpoint- の chain では Service の Cluster IP を DNAT して Pod IP に置き換える

    • 送信元の IP が送信先の IP と同じ hairpin トラフィックの場合、prerouting 時に SNAT するための印を付ける
    • プロトコルが TCP の場合は DNAT して例えば 10.244.1.16:80 にパケットを転送

forward のフックを見ていきます。filter-forward は文字通りパケットのフィルタリング処理を行う chain です。

	chain filter-forward {
		type filter hook forward priority -110; policy accept;
		ct state new jump service-endpoints-check
		ct state new jump cluster-ips-check
	}

	chain service-endpoints-check {
		ip daddr . meta l4proto . th dport vmap @no-endpoint-services
	}

	map no-endpoint-services {
		type ipv4_addr . inet_proto . inet_service : verdict
	}

	chain cluster-ips-check {
		ip daddr @cluster-ips reject comment "Reject traffic to invalid ports of ClusterIPs"
	}

	set cluster-ips {
		type ipv4_addr
		elements = { 10.96.0.1, 10.96.0.10,
			     10.96.109.220 }
	}
  • conntrack に記録されていない新しい接続の場合、service-endpoints-check の chain に移動し、処理が終わったら filter-forward の chain に戻る
  • service-endpoints-check chain では <送信先 IP アドレス>.<プロトコル>.<ポート番号> をキーに no-endpoint-services の vmap を参照して verdict 文を評価
    • 今回だと Endpoint の紐づいていない Service は存在しないので、ルールの評価はスキップ
  • 同様に conntrack に記録されていない新しい接続の場合、cluster-ips-check の chain に移動し、処理が終わったら filter-forward の chain に戻る
  • cluster-ips-check の chain では送信先 IP アドレスをキーに cluster-ips に存在するか確認し verdict 文を評価する
    • forward のフックの時点で送信先 IP アドレスがまだ Cluster IP ということは prerouting フックで DNAT されていないことを意味する
    • Service が公開していないポート番号に対してトラフィックを送ると、prerouting フックで DNAT されず、このルールに引っ掛かりパケットは reject される

postrouting のフックを見ていきます。

	chain nat-postrouting {
		type nat hook postrouting priority srcnat; policy accept;
		jump masquerading
	}

	chain masquerading {
		meta mark & 0x00004000 == 0x00000000 return
		meta mark set meta mark ^ 0x00004000
		masquerade fully-random
	}
  • masquerading chain でパケットの印を確認して必要に応じて SNAT します。
    • ビット AND を使ってパケットに印が付いているかどうか確認

    • 印が付いていない場合はそれ以降の処理をスキップ

    • 印が付いている場合は 0x00004000 とのビット XOR により印をリセットし、送信元の IP アドレスをホストの IP アドレス (正確にはパケットが外に出る時に通るインターフェイスに割り当てられた IP アドレス) に変換し、ランダムなポート番号で SNAT

    • SNAT 時にポート番号も変換しているのは、同一 Node 上の 2 つの Pod が同時に同じ別の Node 上の Pod にリクエストを投げるとします。IP アドレス毎にポート番号は全て利用可能なため、以下のように同じエフェメラルポートを利用する可能性があります。

      • 10.244.1.2:45600 ↔ 10.244.10.2:80
      • 10.244.1.3:45600 ↔ 10.244.10.2:80

      SNAT で送信元の IP アドレスだけを Node の IP アドレスに変換すると、以下のようになります。パケットを送信する時は良いですが、パケットが返ってくる時に 2 つの接続を見分けることができません。異なる 2 つの Pod からリクエストを投げているのに、同じ IP アドレスとポート番号に変換されているからです。

      • 192.168.228.2:45600 ↔ 10.244.10.2:80
      • 192.168.228.2:45600 ↔ 10.244.10.2:80

      SNAT で送信元のポート番号も変換する場合は、conntrack テーブルを参照することで 2 つの接続を見分けることができます。

      • 192.168.228.2:33333 ↔ 10.244.10.2:80
      • 192.168.228.2:44444 ↔ 10.244.10.2:80

      以上から SNAT 時にポート番号も含めることが重要です。ただし、衝突の問題は fully-random (全てのポート範囲でランダム) でも完全に回避できる訳ではありません。(k/k#76699 (comment))

パケットが変換されているか確認します。tcpdump でパケットをキャプチャしても良いのですが、conntrack テーブルに記録された 5-tupple を確認します。

kind-worker2 の Node (client の Pod がいる Node) で conntrack テーブルを監視します。

docker exec -it kind-worker2 bash
watch conntrack -L -p tcp --dport 80

client の Pod から curl でリクエストを投げます。

❯ kubectl exec -it deploy/client -- curl http://backends/hostname
backends-6f6d65ffcd-qww8k

kind-worker2 の conntrack テーブルに新しくエントリが追加されます。

/# conntrack -L -p tcp --dport 80
tcp      6 98 TIME_WAIT src=10.244.2.11 dst=10.96.109.220 sport=44920 dport=80 src=10.244.1.17 dst=10.244.2.11 sport=80 dport=44920 [ASSURED] mark=0 use=1

client の Pod IP (10.244.2.11) と 44920 番ポートの組み合わせで server の Cluster IP (10.96.109.220) の 80 番ポートに通信すると、パケットの送信先は backend の Pod IP (10.244.1.17) と 80 番ポートの組み合わせで DNAT され、client の Pod IP (10.244.2.11) の 44920 番ポートに届けられる。今回は Pod 間の通信のため、SNAT は発生しません。

/# conntrack -L -p tcp --dport 80 --dst-nat
tcp      6 82 TIME_WAIT src=10.244.2.11 dst=10.96.109.220 sport=44920 dport=80 src=10.244.1.17 dst=10.244.2.11 sport=80 dport=44920 [ASSURED] mark=0 use=1

# 80 番ポートへのパケットで SNAT された接続情報はない
/# conntrack -L -p tcp --dport 80 --src-nat

作成したリソースを削除します。

cat <<EOF | kubectl delete -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
---
apiVersion: v1
kind: Service
metadata:
  name: backends
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
EOF

Endpoint が紐付いていない Service を介した通信

Service のラベルセレクタにマッチする Pod が存在しない場合の挙動を見ていきます。

Service と client の作成
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: backends
spec:
  type: ClusterIP
  selector:
    app: backends
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
  labels:
    app: client
spec:
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        command: ["sleep", "infinity"]
      nodeSelector:
        kubernetes.io/hostname: kind-worker2
EOF

作成した Pod

❯ kubectl get pods -owide
NAME                     READY   STATUS    RESTARTS   AGE   IP            NODE           NOMINATED NODE   READINESS GATES
client-778b7d784-d6s62   1/1     Running   0          36s   10.244.2.12   kind-worker2   <none>           <none>

作成した Service

❯ kubectl get svc -owide
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE   SELECTOR
backends     ClusterIP   10.96.22.132   <none>        80/TCP    59s   app=backends
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP   9h    <none>

Endpoint / EndpointSlice は紐付いていない

❯ kubectl get endpoints
NAME         ENDPOINTS            AGE
backends     <none>               96s
kubernetes   192.168.228.4:6443   9h

❯ kubectl get endpointslice
NAME             ADDRESSTYPE   PORTS     ENDPOINTS       AGE
backends-zx9zt   IPv4          <unset>   <unset>         83s
kubernetes       IPv4          6443      192.168.228.4   9h

kind-worker2 の Node のシェルを取得

docker exec -it kind-worker2 bash

nftables のルールの詳細を表示

nft list ruleset ip

Endpoint が紐付いていない場合は、forward フックの chain で reject されます。

      +------------+         +---------+
      | prerouting |-[*]-+-->| forward |
      +------------+         +---------+
            ^                                           
 -  -  -  - | -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -
            |                                           
        +--------+
        | client |
        +--------+

forward のフックで service-endpoints-check の chain に移動している箇所から見ていきます。no-endpoint-services の vmap に要素が追加されているので、ClusterIP (10.96.22.132) への通信は reject されます。

	chain filter-forward {
		type filter hook forward priority -110; policy accept;
		ct state new jump service-endpoints-check
		ct state new jump cluster-ips-check
	}

	chain service-endpoints-check {
		ip daddr . meta l4proto . th dport vmap @no-endpoint-services
	}

	map no-endpoint-services {
		type ipv4_addr . inet_proto . inet_service : verdict
		elements = { 10.96.22.132 . tcp . 80 comment "default/backends" : goto reject-chain }
	}

	set cluster-ips {
		type ipv4_addr
		elements = { 10.96.0.1, 10.96.0.10,
			     10.96.22.132 }
	}

	chain reject-chain {
		reject
	}

client の Pod からこの Endpoint の紐付いていない ClusterIP に対してリクエストを投げる。パケットが reject されたことで Connection refused が発生していることが確認できる。

❯ kubectl exec -it deploy/client -- curl http://backends/hostname
curl: (7) Failed to connect to backends port 80 after 1 ms: Connection refused
command terminated with exit code 7

作成したリソースを削除します。

cat <<EOF | kubectl delete -f -
apiVersion: v1
kind: Service
metadata:
  name: backends
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
EOF

ホスト上のプロセスから Service を介した Pod への通信

server と client のアプリケーションを NodeSelector を利用して別の Node にデプロイします。client は hostNetwork: true で起動し、Cluster IP を経由して server に接続します。

アプリケーションと Service の作成
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
  labels:
    app: backends
spec:
  selector:
    matchLabels:
      app: backends
  template:
    metadata:
      labels:
        app: backends
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        args:
        - netexec
        - --http-port=80
        - --delay-shutdown=30
      nodeSelector:
        kubernetes.io/hostname: kind-worker
---
apiVersion: v1
kind: Service
metadata:
  name: backends
spec:
  type: ClusterIP
  selector:
    app: backends
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
  labels:
    app: client
spec:
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        command: ["sleep", "infinity"]
      nodeSelector:
        kubernetes.io/hostname: kind-worker2
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
EOF

作成した Pod

❯ kubectl get pods -owide
NAME                        READY   STATUS    RESTARTS   AGE   IP              NODE           NOMINATED NODE   READINESS GATES
backends-6f6d65ffcd-mfzqj   1/1     Running   0          11s   10.244.1.19     kind-worker    <none>           <none>
client-5c4bc678bb-xh5v6     1/1     Running   0          11s   192.168.228.2   kind-worker2   <none>           <none>

払い出された ClusterIP

❯ kubectl get svc -owide
NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE   SELECTOR
backends     ClusterIP   10.96.17.39   <none>        80/TCP    31s   app=backends
kubernetes   ClusterIP   10.96.0.1     <none>        443/TCP   10h   <none>

Node IP

❯ kubectl get nodes -owide
NAME                 STATUS   ROLES           AGE   VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION                        CONTAINER-RUNTIME
kind-control-plane   Ready    control-plane   10h   v1.30.0   192.168.228.4   <none>        Debian GNU/Linux 12 (bookworm)   6.7.11-orbstack-00143-ge6b82e26cd22   containerd://1.7.15
kind-worker          Ready    <none>          10h   v1.30.0   192.168.228.3   <none>        Debian GNU/Linux 12 (bookworm)   6.7.11-orbstack-00143-ge6b82e26cd22   containerd://1.7.15
kind-worker2         Ready    <none>          10h   v1.30.0   192.168.228.2   <none>        Debian GNU/Linux 12 (bookworm)   6.7.11-orbstack-00143-ge6b82e26cd22   containerd://1.7.15

client の Pod が存在する kind-worker2 の Node のシェルを取得

docker exec -it kind-worker2 bash

nftables のルールの詳細を表示

nft list ruleset ip

kind-worker2 の output → postrouting のフックを通り、kind-worker の prerouting → forward → postrouting のフックを通って Pod に辿り着きます。

                                      +========+
                                      | client |
                                      +========+
                                          |
  -  -  -  -  -  -  -  - - -  -  -  -  - [*] -  -  -  -  -  -  -  -  -
                                          v
                                      +--------+
                                      | output |
                                      +--------+
                                          |
                                          v      +-------------+
                                          +-[*]->| postrouting |
                                                 +-------------+
                                                        |
 -  -  -   -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  |  -  -  -  -
                                                        v
                                                    +--------+
                                                    | server |
                                                    +--------+

output のフックで動作する chain は 3 つあります。優先度から filter-output -> nat-output -> filter-output-post-dnat の順番でルールを評価します。

	chain filter-output {
		type filter hook output priority -110; policy accept;
		ct state new jump service-endpoints-check
		ct state new jump firewall-check
	}

	chain nat-output {
		type nat hook output priority -100; policy accept;
		jump services
	}

	chain filter-output-post-dnat {
		type filter hook output priority -90; policy accept;
		ct state new jump cluster-ips-check
	}

filter-output の chain を見ていきます。

	chain filter-output {
		type filter hook output priority -110; policy accept;
		ct state new jump service-endpoints-check
		ct state new jump firewall-check
	}
	
	chain service-endpoints-check {
		ip daddr . meta l4proto . th dport vmap @no-endpoint-services
	}

	chain firewall-check {
		ip daddr . meta l4proto . th dport vmap @firewall-ips
	}
  • conntrack テーブルに記録されていない新しい接続の場合は、service-endpoints-check の chain に飛び、評価が終わったらまた戻る
    • service-endpoints-check の chain では Service に Endpoint がぶら下がっているか確認
  • conntrack テーブルに記録されていない新しい接続の場合は、firewall-check の chain に飛び、評価が終わったらまた戻る
    • firewall-check の chain では Service の .spec.loadBalancerSourceRanges で指定した IP 範囲以外からの通信を拒否する

nat-output の chain を見ていきます。

	chain nat-output {
		type nat hook output priority -100; policy accept;
		jump services
	}

	chain services {
		ip daddr . meta l4proto . th dport vmap @service-ips
		ip daddr @nodeport-ips meta l4proto . th dport vmap @service-nodeports
	}

	map service-ips {
		type ipv4_addr . inet_proto . inet_service : verdict
		elements = { 10.96.0.10 . tcp . 53 : goto service-NWBZK7IH-kube-system/kube-dns/tcp/dns-tcp,
			     10.96.0.10 . udp . 53 : goto service-FY5PMXPG-kube-system/kube-dns/udp/dns,
			     10.96.17.39 . tcp . 80 : goto service-GZEOLGI7-default/backends/tcp/,
			     10.96.0.1 . tcp . 443 : goto service-2QRHZV4L-default/kubernetes/tcp/https,
			     10.96.0.10 . tcp . 9153 : goto service-AS2KJYAD-kube-system/kube-dns/tcp/metrics }
	}

	chain service-GZEOLGI7-default/backends/tcp/ {
		ip daddr 10.96.17.39 tcp dport 80 ip saddr != 10.244.0.0/16 jump mark-for-masquerade
		numgen random mod 1 vmap { 0 : goto endpoint-EMAQKF3J-default/backends/tcp/__10.244.1.19/80 }
	}

	chain endpoint-EMAQKF3J-default/backends/tcp/__10.244.1.19/80 {
		ip saddr 10.244.1.19 jump mark-for-masquerade
		meta l4proto tcp dnat to 10.244.1.19:80
	}

	chain mark-for-masquerade {
		meta mark set meta mark | 0x00004000
	}
  • nat-prerouting の chain と大きく違いはない
  • nat-output の chain から services の chain に移動し、Cluster IP と NodePort それぞれの chain に移動
  • service-ips の vmap で一致した service-GZEOLGI7-default/backends/tcp/ chain に移動
  • service-GZEOLGI7-default/backends/tcp/ の chain で ip saddr != 10.244.0.0/16 (Pod IP の範囲でない) の式に一致するので、mark-for-masquerade chain に移動
    • パケットに印を付けて postrouting chain で SNAT できるようにする

最後に filter-output-post-dnat の chain を見ていきます。

	chain filter-output-post-dnat {
		type filter hook output priority -90; policy accept;
		ct state new jump cluster-ips-check
	}

	chain cluster-ips-check {
		ip daddr @cluster-ips reject comment "Reject traffic to invalid ports of ClusterIPs"
	}

	chain cluster-ips-check {
		ip daddr @cluster-ips reject comment "Reject traffic to invalid ports of ClusterIPs"
	}
  • filter-forward の処理であったように、送信先 IP アドレスをキーに cluster-ips に存在するか確認し verdict 文を評価
    • 送信先 IP アドレスがまだ Cluster IP ということは prerouting フックで DNAT されていないことを意味する
    • Service が公開していないポート番号に対してトラフィックを送るとこのルールに引っ掛かりパケットは reject される

postrouting のフックは先ほど紹介したように SNAT しているだけなので割愛します。

	chain nat-postrouting {
		type nat hook postrouting priority srcnat; policy accept;
		jump masquerading
	}

	chain masquerading {
		meta mark & 0x00004000 == 0x00000000 return
		meta mark set meta mark ^ 0x00004000
		masquerade fully-random
	}

パケットが変換されているか確認します。tcpdump でパケットをキャプチャしても良いのですが、conntrack テーブルに記録された 5-tupple を確認します。

kind-worker2 の Node (client の Pod がいる Node) で conntrack テーブルを監視します。

docker exec -it kind-worker2 bash
watch conntrack -L -p tcp --dport 80

client の Pod から curl でリクエストを投げます。

kubectl exec -it deploy/client -- curl http://backends/hostname

kind-worker2 の conntrack テーブルに新しくエントリが追加されます。

/# conntrack -L -p tcp --dport 80
tcp      6 114 TIME_WAIT src=192.168.228.2 dst=10.96.17.39 sport=56948 dport=80 src=10.244.1.19 dst=192.168.228.2 sport=80 dport=56130 [ASSURED] mark=0 use=1

client の Pod は hostNetwork を有効化しています。kind-worker2 の Node IP (192.168.228.2) と 56948 番ポートの組み合わせで server の Cluster IP (10.96.17.39) の 80 番ポートに通信すると、パケットの送信先は backend の Pod IP (10.244.1.19) と 80 番ポートの組み合わせで DNAT され、ポケットの送信元は kind-worker2 の Node IP (192.168.228.2) の 56130 番ポートで SNAT されます。

/# conntrack -L -p tcp --dport 80 --dst-nat
tcp      6 77 TIME_WAIT src=192.168.228.2 dst=10.96.17.39 sport=56948 dport=80 src=10.244.1.19 dst=192.168.228.2 sport=80 dport=56130 [ASSURED] mark=0 use=1

/# conntrack -L -p tcp --dport 80 --src-nat
tcp      6 80 TIME_WAIT src=192.168.228.2 dst=10.96.17.39 sport=56948 dport=80 src=10.244.1.19 dst=192.168.228.2 sport=80 dport=56130 [ASSURED] mark=0 use=1

作成したリソースを削除します。

cat <<EOF | kubectl delete -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
---
apiVersion: v1
kind: Service
metadata:
  name: backends
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
EOF

NodePort の Service に Node IP 経由で通信

NodePort を利用して Node 上のエフェメラルポートを払い出し、ホスト上からリクエストを投げて挙動を確認します。

アプリケーションと Service の作成
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
  labels:
    app: backends
spec:
  selector:
    matchLabels:
      app: backends
  template:
    metadata:
      labels:
        app: backends
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        args:
        - netexec
        - --http-port=80
        - --delay-shutdown=30
      nodeSelector:
        kubernetes.io/hostname: kind-worker
---
apiVersion: v1
kind: Service
metadata:
  name: backends
spec:
  type: NodePort
  selector:
    app: backends
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
EOF

作成した Pod

❯ kubectl get pods -owide
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE          NOMINATED NODE   READINESS GATES
backends-6f6d65ffcd-zlp2n   1/1     Running   0          13s   10.244.1.20   kind-worker   <none>           <none>

払い出された NodePort とエフェメラルポート番号

❯ kubectl get svc -owide
NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE   SELECTOR
backends     NodePort    10.96.82.46   <none>        80:31384/TCP   23s   app=backends
kubernetes   ClusterIP   10.96.0.1     <none>        443/TCP        10h   <none>

Node IP

❯ kubectl get nodes -owide
NAME                 STATUS   ROLES           AGE   VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION                        CONTAINER-RUNTIME
kind-control-plane   Ready    control-plane   10h   v1.30.0   192.168.228.4   <none>        Debian GNU/Linux 12 (bookworm)   6.7.11-orbstack-00143-ge6b82e26cd22   containerd://1.7.15
kind-worker          Ready    <none>          10h   v1.30.0   192.168.228.3   <none>        Debian GNU/Linux 12 (bookworm)   6.7.11-orbstack-00143-ge6b82e26cd22   containerd://1.7.15
kind-worker2         Ready    <none>          10h   v1.30.0   192.168.228.2   <none>        Debian GNU/Linux 12 (bookworm)   6.7.11-orbstack-00143-ge6b82e26cd22   containerd://1.7.15

kind-worker2 の Node のシェルを取得

docker exec -it kind-worker2 bash

nftables のルールの詳細を表示

nft list ruleset ip

NodePort は hostNetwork IP への通信と同義なので、prerouting → input のフックを通ります。

             +================+
             | hostNetwork IP |
             +================+
                         ^                
  -  -  -  -  -  -  -  - | -  -  -  -  -  -  -  -  -  -  -  -  -  -  -
                         |                
                     +-------+
                     | input |
                     +-------+
                         ^    
      +------------+     |
      | prerouting |-[*]-+
      +------------+      
            ^                                           
 -  -  -  - | -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -
            |                                           
        +--------+                                  
        | client |                                  
        +--------+                                  

prerouting のフックを見ていきます。

	set nodeport-ips {
		type ipv4_addr
		elements = { 192.168.228.2 }
	}

	map service-nodeports {
		type inet_proto . inet_service : verdict
		elements = { tcp . 31384 : goto external-GZEOLGI7-default/backends/tcp/ }
	}

	chain nat-prerouting {
		type nat hook prerouting priority dstnat; policy accept;
		jump services
	}

	chain services {
		ip daddr . meta l4proto . th dport vmap @service-ips
		ip daddr @nodeport-ips meta l4proto . th dport vmap @service-nodeports
	}

	chain external-GZEOLGI7-default/backends/tcp/ {
		jump mark-for-masquerade
		goto service-GZEOLGI7-default/backends/tcp/
	}

	chain service-GZEOLGI7-default/backends/tcp/ {
		ip daddr 10.96.82.46 tcp dport 80 ip saddr != 10.244.0.0/16 jump mark-for-masquerade
		numgen random mod 1 vmap { 0 : goto endpoint-LZ75E627-default/backends/tcp/__10.244.1.20/80 }
	}

	chain endpoint-LZ75E627-default/backends/tcp/__10.244.1.20/80 {
		ip saddr 10.244.1.20 jump mark-for-masquerade
		meta l4proto tcp dnat to 10.244.1.20:80
	}

	chain mark-for-masquerade {
		meta mark set meta mark | 0x00004000
	}
  • nat-prerouting の chain からいつものように services の chain に移動する
  • 送信先 IP アドレスが nodeport-ips の set に保存された Node IP に一致するので、service-nodeports の vmap を <プロトコル>.<エフェメラルなポート番号> のキーで参照する
  • external-GZEOLGI7-default/backends/tcp/ の chain に飛んでパケットに postrouting chain で SNAT するためのの印を付け、service-GZEOLGI7-default/backends/tcp/ の chain に移動する
  • あとはいつも通りランダムな接続先の Pod IP を選んで DNAT する

kind-worker2 の Node (client の Pod がいる Node) で conntrack テーブルを監視します。追跡する送信先のポート番号は kubectl get svc -owide で確認したエフェメラルポートです。

docker exec -it kind-worker2 bash
watch conntrack -L -p tcp --dport 31384

ホストから kind-worker2 の NodePort に向けて curl でリクエストを投げます。

curl http://$(kubectl get nodes kind-worker2 -ojsonpath='{.status.addresses[0].address}'):$(kubectl get svc backends -ojsonpath='{.spec.ports[0].nodePort}')

kind-worker2 の conntrack テーブルに新しくエントリが追加されます。

/# conntrack -L -p tcp --dport 31384
tcp      6 93 TIME_WAIT src=192.168.228.0 dst=192.168.228.2 sport=63939 dport=31384 src=10.244.1.20 dst=192.168.228.2 sport=80 dport=51772 [ASSURED] mark=0 use=1

ホスト IP (192.168.228.0) と 63939 番ポートの組み合わせで NodePort を公開している Node IP (192.168.228.2) の 31384 番ポートに通信すると、パケットの送信先は backend の Pod IP (10.244.1.20) と 80 番ポートの組み合わせに DNAT され、送信元の ホスト IP は Node IP (192.168.228.2) の 51772 番ポートに SNAT される。

/# conntrack -L -p tcp --dport 31384 --dst-nat
tcp      6 79 TIME_WAIT src=192.168.228.0 dst=192.168.228.2 sport=63939 dport=31384 src=10.244.1.20 dst=192.168.228.2 sport=80 dport=51772 [ASSURED] mark=0 use=1

/# conntrack -L -p tcp --dport 31384 --src-nat
tcp      6 76 TIME_WAIT src=192.168.228.0 dst=192.168.228.2 sport=63939 dport=31384 src=10.244.1.20 dst=192.168.228.2 sport=80 dport=51772 [ASSURED] mark=0 use=1

作成したリソースを削除します。

cat <<EOF | kubectl delete -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
---
apiVersion: v1
kind: Service
metadata:
  name: backends
EOF

eTP=Local な Service の NodePort に Node IP 経由で通信

externalTrafficPolicy=Local の NodePort を利用して Node 上のエフェメラルポートを払い出し、ホスト上からリクエストを投げて挙動を確認します。

アプリケーションと Service の作成
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
  labels:
    app: backends
spec:
  selector:
    matchLabels:
      app: backends
  template:
    metadata:
      labels:
        app: backends
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        args:
        - netexec
        - --http-port=80
        - --delay-shutdown=30
      nodeSelector:
        kubernetes.io/hostname: kind-worker
---
apiVersion: v1
kind: Service
metadata:
  name: backends
spec:
  type: NodePort
  selector:
    app: backends
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  externalTrafficPolicy: Local
EOF

作成した Pod

❯ kubectl get pods -owide
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE          NOMINATED NODE   READINESS GATES
backends-6f6d65ffcd-jm9tb   1/1     Running   0          7s    10.244.1.21   kind-worker   <none>           <none>

払い出された NodePort とエフェメラルポート番号

❯ kubectl get svc -owide
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE   SELECTOR
backends     NodePort    10.96.28.245   <none>        80:31337/TCP   17s   app=backends
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        10h   <none>

Node IP

❯ kubectl get nodes -owide
NAME                 STATUS   ROLES           AGE   VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION                        CONTAINER-RUNTIME
kind-control-plane   Ready    control-plane   10h   v1.30.0   192.168.228.4   <none>        Debian GNU/Linux 12 (bookworm)   6.7.11-orbstack-00143-ge6b82e26cd22   containerd://1.7.15
kind-worker          Ready    <none>          10h   v1.30.0   192.168.228.3   <none>        Debian GNU/Linux 12 (bookworm)   6.7.11-orbstack-00143-ge6b82e26cd22   containerd://1.7.15
kind-worker2         Ready    <none>          10h   v1.30.0   192.168.228.2   <none>        Debian GNU/Linux 12 (bookworm)   6.7.11-orbstack-00143-ge6b82e26cd22   containerd://1.7.15

kind-worker2 の Node のシェルを取得

docker exec -it kind-worker2 bash

nftables のルールの詳細を表示

nft list ruleset ip

NodePort は hostNetwork IP への通信と同義なので、prerouting → input フックを通ります。

             +================+
             | hostNetwork IP |
             +================+
                         ^                
  -  -  -  -  -  -  -  - | -  -  -  -  -  -  -  -  -  -  -  -  -  -  -
                         |                
                     +-------+
                     | input |
                     +-------+
                         ^    
      +------------+     |
      | prerouting |-[*]-+
      +------------+      
            ^                                           
 -  -  -  - | -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -
            |                                           
        +--------+                                  
        | client |                                  
        +--------+                                  

prerouting フックの一部を見ていきます。external-GZEOLGI7-default/backends/tcp/ chain が先ほどと異なっているので、そちらだけ見ていきます。

	chain nat-prerouting {
		type nat hook prerouting priority dstnat; policy accept;
		jump services
	}

	chain services {
		ip daddr . meta l4proto . th dport vmap @service-ips
		ip daddr @nodeport-ips meta l4proto . th dport vmap @service-nodeports
	}

	set nodeport-ips {
		type ipv4_addr
		elements = { 192.168.228.2 }
	}

	map service-nodeports {
		type inet_proto . inet_service : verdict
		elements = { tcp . 32062 : goto external-GZEOLGI7-default/backends/tcp/ }
	}
	
	chain external-GZEOLGI7-default/backends/tcp/ {
		ip saddr 10.244.0.0/16 goto service-GZEOLGI7-default/backends/tcp/ comment "short-circuit pod traffic"
		fib saddr type local jump mark-for-masquerade comment "masquerade local traffic"
		fib saddr type local goto service-GZEOLGI7-default/backends/tcp/ comment "short-circuit local traffic"
	}

	chain service-GZEOLGI7-default/backends/tcp/ {
		ip daddr 10.96.28.245 tcp dport 80 ip saddr != 10.244.0.0/16 jump mark-for-masquerade
		numgen random mod 1 vmap { 0 : goto endpoint-KY4HFOKH-default/backends/tcp/__10.244.1.21/80 }
	}
  • 送信元 IP が Pod IP の範囲 (ClusterCIDR) の場合、SNAT する必要がないので service-GZEOLGI7-default/backends/tcp/ の chain に移動する
    • fib 文を使って送信元の IP がプライベート IP アドレス帯の場合のみ DNAT する
  • 今回はホスト (プライベート IP) からリクエストを投げているので SNAT するための印を付けてから service-GZEOLGI7-default/backends/tcp/ の chain に移動し、DNAT する

次に input フックの一部を見ていきます。

	chain filter-input {
		type filter hook input priority -110; policy accept;
		ct state new jump nodeport-endpoints-check
		ct state new jump service-endpoints-check
	}

	chain nodeport-endpoints-check {
		ip daddr @nodeport-ips meta l4proto . th dport vmap @no-endpoint-nodeports
	}

	set nodeport-ips {
		type ipv4_addr
		elements = { 192.168.228.2 }
	}

	map no-endpoint-nodeports {
		type inet_proto . inet_service : verdict
		elements = { tcp . 32062 comment "default/backends" : drop }
	}
  • kind-worker2 の Node には backends の Pod は存在しない
  • そのため、送信先の IP アドレスが kind-worker2 の Node IP の場合、no-endpoint-nodeports の vmap によりパケットが drop される
    • reject ではなく drop
    • パケットは闇に消え、リクエストはタイムアウトする

ホストから kind-worker2 の NodePort に向けて curl でリクエストを投げるとタイムアウトします。

❯ curl http://$(kubectl get nodes kind-worker2 -ojsonpath='{.status.addresses[0].address}'):$(kubectl get svc backends -ojsonpath='{.spec.ports[0].nodePort}')/hostname -m 10
curl: (28) Connection timed out after 10005 milliseconds

ホストから kind-worker の NodePort に向けて curl でリクエストを投げるとレスポンスが返ってきます。

❯ curl http://$(kubectl get nodes kind-worker -ojsonpath='{.status.addresses[0].address}'):$(kubectl get svc backends -ojsonpath='{.spec.ports[0].nodePort}')/hostname
backends-6f6d65ffcd-jm9tb

作成したリソースを削除します。

cat <<EOF | kubectl delete -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
---
apiVersion: v1
kind: Service
metadata:
  name: backends
EOF

iTP=Local な Service を介した Pod 間の通信

internalTrafficPolicy=Local の場合、同一 Node 上に存在する Pod にのみリクエストが届きます。異なる Node にしか Pod が存在しない場合は、パケットが drop されます。

異なる Node 上の Pod 間の通信

ClusterIP を経由した異なる Node 上の Pod 間の通信の挙動を確認します。この場合、パケットが drop されるはずです。

アプリケーションと Service の作成
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
  labels:
    app: backends
spec:
  selector:
    matchLabels:
      app: backends
  template:
    metadata:
      labels:
        app: backends
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        args:
        - netexec
        - --http-port=80
        - --delay-shutdown=30
      nodeSelector:
        kubernetes.io/hostname: kind-worker
---
apiVersion: v1
kind: Service
metadata:
  name: backends
spec:
  type: ClusterIP
  selector:
    app: backends
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  internalTrafficPolicy: Local
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
  labels:
    app: client
spec:
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        command: ["sleep", "infinity"]
      nodeSelector:
        kubernetes.io/hostname: kind-worker2
EOF

作成した Pod

❯ kubectl get pods -owide
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE           NOMINATED NODE   READINESS GATES
backends-6f6d65ffcd-z5dtf   1/1     Running   0          98s   10.244.2.18   kind-worker    <none>           <none>
client-778b7d784-nhrcc      1/1     Running   0          10m   10.244.1.10   kind-worker2   <none>           <none>

作成した ClusterIP の Service

❯ kubectl get svc -owide
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE     SELECTOR
backends     ClusterIP   10.96.59.189   <none>        80/TCP    4m44s   app=backends
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP   25h     <none>

kind-worker2 の Node のシェルを取得

docker exec -it kind-worker2 bash

nftables のルールの詳細を表示

nft list ruleset ip

iTP=Local な Service を介した Pod 間の通信の場合、kind-worker2 の prerouting → forward → postrouting のフックを通り、foward のフックでパケットが drop されます。

      +------------+         +---------+
      | prerouting |-[*]-+-->| forward |
      +------------+         +---------+
            ^                                           
 -  -  -  - | -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -
            |                                           
        +--------+                                  
        | client |                                  
        +--------+                                 

prerouting フックの NAT ルールを見ていきます。

	chain nat-prerouting {
		type nat hook prerouting priority dstnat; policy accept;
		jump services
	}

	chain services {
		ip daddr . meta l4proto . th dport vmap @service-ips
		ip daddr @nodeport-ips meta l4proto . th dport vmap @service-nodeports
	}

	map service-ips {
		type ipv4_addr . inet_proto . inet_service : verdict
		elements = { 10.96.0.10 . tcp . 53 : goto service-NWBZK7IH-kube-system/kube-dns/tcp/dns-tcp,
			     10.96.0.10 . udp . 53 : goto service-FY5PMXPG-kube-system/kube-dns/udp/dns,
			     10.96.0.1 . tcp . 443 : goto service-2QRHZV4L-default/kubernetes/tcp/https,
			     10.96.0.10 . tcp . 9153 : goto service-AS2KJYAD-kube-system/kube-dns/tcp/metrics }
	}
  • いつも通り services chain で service-ips の vmap を見て service- の chain に飛ぼうとしますが、10.96.59.189 . tcp . 80 のキーが存在しないため NAT は行われない

次に forward フックのパケットフィルタリングのルールを見ていきます。

	chain filter-forward {
		type filter hook forward priority -110; policy accept;
		ct state new jump service-endpoints-check
		ct state new jump cluster-ips-check
	}

	chain service-endpoints-check {
		ip daddr . meta l4proto . th dport vmap @no-endpoint-services
	}

	map no-endpoint-services {
		type ipv4_addr . inet_proto . inet_service : verdict
		elements = { 10.96.59.189 . tcp . 80 comment "default/backends" : drop }
	}
  • kind-worker2 の Node には backends の Pod は存在しません。そのため、no-endpoint-services の vmap に今回作成した Service の情報が入っており、パケットが drop される
    • reject ではなく drop
    • パケットは闇に消え、リクエストはタイムアウトする

kind-worker2 の client から iTP=Local の Service に curl でリクエストを投げるとタイムアウトします。

❯ kubectl exec -it deploy/client -- curl http://backends/hostname -m 10
curl: (28) Connection timed out after 10003 milliseconds
command terminated with exit code 28

作成したリソースを削除します。

cat <<EOF | kubectl delete -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
---
apiVersion: v1
kind: Service
metadata:
  name: backends
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
EOF

同一 Node 上の Pod 間の通信

ClusterIP を経由した同一 Node 上の Pod 間の通信の挙動を確認します。kind-worker2 の Node に backends と client の Pod が存在するため、問題なく通信できます。

アプリケーションと Service の作成
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
  labels:
    app: backends
spec:
  selector:
    matchLabels:
      app: backends
  template:
    metadata:
      labels:
        app: backends
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        args:
        - netexec
        - --http-port=80
        - --delay-shutdown=30
      nodeSelector:
        kubernetes.io/hostname: kind-worker2
---
apiVersion: v1
kind: Service
metadata:
  name: backends
spec:
  type: ClusterIP
  selector:
    app: backends
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  internalTrafficPolicy: Local
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
  labels:
    app: client
spec:
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        command: ["sleep", "infinity"]
      nodeSelector:
        kubernetes.io/hostname: kind-worker2
EOF

作成した Pod

❯ kubectl get pods -owide
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE           NOMINATED NODE   READINESS GATES
backends-5dc4d988d6-65fnj   1/1     Running   0          10s   10.244.2.14   kind-worker2   <none>           <none>
client-778b7d784-k6w45      1/1     Running   0          10s   10.244.2.15   kind-worker2   <none>           <none>

作成した ClusterIP の Service

❯ kubectl get svc -owide
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE   SELECTOR
backends     ClusterIP   10.96.83.251   <none>        80/TCP    21s   app=backends
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP   11h   <none>

kind-worker2 の Node のシェルを取得

docker exec -it kind-worker2 bash

nftables のルールの詳細を表示

nft list ruleset ip

iTP=Local な Service を介した Pod 間の通信の場合、kind-worker2 の prerouting → forward → postrouting のフックを通り同一 Node 上の Pod に辿り着きます。

      +------------+         +---------+         +-------------+
      | prerouting |-[*]-+-->| forward |--+-[*]->| postrouting |
      +------------+         +---------+         +-------------+
            ^                                           |
 -  -  -  - | -  -  -  -  -  -  -  -  -  -  -  -  -  -  |  -  -  -  -
            |                                           v
        +--------+                                  +--------+
        | client |                                  | server |
        +--------+                                  +--------+

prerouting フックの NAT ルールを見ていきます。

	chain nat-prerouting {
		type nat hook prerouting priority dstnat; policy accept;
		jump services
	}

	chain services {
		ip daddr . meta l4proto . th dport vmap @service-ips
		ip daddr @nodeport-ips meta l4proto . th dport vmap @service-nodeports
	}

	map service-ips {
		type ipv4_addr . inet_proto . inet_service : verdict
		elements = { 10.96.0.10 . tcp . 53 : goto service-NWBZK7IH-kube-system/kube-dns/tcp/dns-tcp,
			     10.96.0.10 . udp . 53 : goto service-FY5PMXPG-kube-system/kube-dns/udp/dns,
			     10.96.83.251 . tcp . 80 : goto local-GZEOLGI7-default/backends/tcp/,
			     10.96.0.1 . tcp . 443 : goto service-2QRHZV4L-default/kubernetes/tcp/https,
			     10.96.0.10 . tcp . 9153 : goto service-AS2KJYAD-kube-system/kube-dns/tcp/metrics }
	}

	chain local-GZEOLGI7-default/backends/tcp/ {
		ip daddr 10.96.83.251 tcp dport 80 ip saddr != 10.244.0.0/16 jump mark-for-masquerade
		numgen random mod 1 vmap { 0 : goto endpoint-LNODCRIZ-default/backends/tcp/__10.244.2.14/80 }
	}

	chain endpoint-LNODCRIZ-default/backends/tcp/__10.244.2.14/80 {
		ip saddr 10.244.2.14 jump mark-for-masquerade
		meta l4proto tcp dnat to 10.244.2.14:80
	}
  • いつも通り services chain で service-ips の vmap を見て service- の chain に飛ぼうとします。今回は、10.96.83.251 . tcp . 80 のキーが存在しており、local-GZEOLGI7-default/backends/tcp/ の chain に飛ぶ
  • local-GZEOLGI7-default/backends/tcp/ の chain では送信元の IP 範囲が Pod に割り当てる IP 範囲 (ClusterCIDR) でない時に mark-for-masquerade chain でパケットに SNAT する印を付けます。その後でローカルな vmap からランダムで転送先の chain を選びます。今回は backends の Pod が 1 つしかないので、 endpoint-LNODCRIZ-default/backends/tcp/__10.244.2.14/80 の chain に飛ぶ
  • endpoint-LNODCRIZ-default/backends/tcp/__10.244.2.14/80 の chain では hairpin トラフィックの場合に SNAT する印を付与し、backends の Pod IP とポート番号で DNAT

次に forward chain のパケットフィルタリングのルールを見ていきます。

	chain filter-forward {
		type filter hook forward priority -110; policy accept;
		ct state new jump service-endpoints-check
		ct state new jump cluster-ips-check
	}

	chain service-endpoints-check {
		ip daddr . meta l4proto . th dport vmap @no-endpoint-services
	}

	map no-endpoint-services {
		type ipv4_addr . inet_proto . inet_service : verdict
	}
  • kind-worker2 の Node に backends の Pod が存在するため、no-endpoint-services の vmap は空っぽです。 パケットは forward chain でフィルタリングされることなく通過します。

postrouting chain の NAT ルールはいつも通り印が付いたパケットを SNAT するだけです。

	chain nat-postrouting {
		type nat hook postrouting priority srcnat; policy accept;
		jump masquerading
	}

	chain masquerading {
		meta mark & 0x00004000 == 0x00000000 return
		meta mark set meta mark ^ 0x00004000
		masquerade fully-random
	}

kind-worker2 の client から iTP=Local の Service に curl でリクエストを投げると今度はレスポンスが返ってきます。

❯ kubectl exec -it deploy/client -- curl http://backends/hostname
backends-5dc4d988d6-65fnj

作成したリソースを削除します。

cat <<EOF | kubectl delete -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
---
apiVersion: v1
kind: Service
metadata:
  name: backends
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
EOF

Session Affinity を設定した Service を介した Pod 間の通信

Service には Session Affinity を設定することができます。現状は送信元 IP をベースにした Session Affinity のみをサポートしています。これにより、Pod からの通信が全て同じ Pod に届くようになります。

Session Affinity を設定した ClusterIP を経由した Pod 間の通信の挙動を確認します。

アプリケーションと Service の作成
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
  labels:
    app: backends
spec:
  replicas: 3
  selector:
    matchLabels:
      app: backends
  template:
    metadata:
      labels:
        app: backends
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        args:
        - netexec
        - --http-port=80
        - --delay-shutdown=30
      nodeSelector:
        kubernetes.io/hostname: kind-worker
---
apiVersion: v1
kind: Service
metadata:
  name: backends
spec:
  type: ClusterIP
  selector:
    app: backends
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  sessionAffinity: ClientIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
  labels:
    app: client
spec:
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        command: ["sleep", "infinity"]
      nodeSelector:
        kubernetes.io/hostname: kind-worker2
EOF

作成した Pod

❯ kubectl get pods -owide
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE           NOMINATED NODE   READINESS GATES
backends-6f6d65ffcd-86d6d   1/1     Running   0          78s   10.244.2.22   kind-worker    <none>           <none>
backends-6f6d65ffcd-qhqvz   1/1     Running   0          78s   10.244.2.24   kind-worker    <none>           <none>
backends-6f6d65ffcd-zjt4g   1/1     Running   0          78s   10.244.2.23   kind-worker    <none>           <none>
client-778b7d784-6mgc6      1/1     Running   0          78s   10.244.1.14   kind-worker2   <none>           <none>

作成した ClusterIP の Service

❯ kubectl get svc -owide
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE   SELECTOR
backends     ClusterIP   10.96.170.107   <none>        80/TCP    90s   app=backends
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   26h   <none>

kind-worker2 の Node のシェルを取得

docker exec -it kind-worker2 bash

nftables のルールの詳細を表示

nft list ruleset ip

Session Affinity を設定した Service を介した Pod 間の通信の場合、kind-worker2 の prerouting → forward → postrouting のフックを通り、kind-worker の prerouting → forward → postrouting のフックを通って Pod に到達します。

      +------------+         +---------+         +-------------+
      | prerouting |-[*]-+-->| forward |--+-[*]->| postrouting |
      +------------+         +---------+         +-------------+
            ^                                           |
 -  -  -  - | -  -  -  -  -  -  -  -  -  -  -  -  -  -  |  -  -  -  -
            |                                           v
        +--------+                                  +--------+
        | client |                                  | server |
        +--------+                                  +--------+

prerouting chain の NAT ルールを見ていきます。

	chain nat-prerouting {
		type nat hook prerouting priority dstnat; policy accept;
		jump services
	}

	chain services {
		ip daddr . meta l4proto . th dport vmap @service-ips
		ip daddr @nodeport-ips meta l4proto . th dport vmap @service-nodeports
	}

	map service-ips {
		type ipv4_addr . inet_proto . inet_service : verdict
		elements = { 10.96.0.10 . tcp . 53 : goto service-NWBZK7IH-kube-system/kube-dns/tcp/dns-tcp,
			     10.96.0.10 . udp . 53 : goto service-FY5PMXPG-kube-system/kube-dns/udp/dns,
			     10.96.170.107 . tcp . 80 : goto service-GZEOLGI7-default/backends/tcp/,
			     10.96.0.1 . tcp . 443 : goto service-2QRHZV4L-default/kubernetes/tcp/https,
			     10.96.0.10 . tcp . 9153 : goto service-AS2KJYAD-kube-system/kube-dns/tcp/metrics }
	}
  • いつも通り services chain で service-ips の vmap を見て service-GZEOLGI7-default/backends/tcp/ の chain に飛びます。
	chain service-GZEOLGI7-default/backends/tcp/ {
		ip daddr 10.96.170.107 tcp dport 80 ip saddr != 10.244.0.0/16 jump mark-for-masquerade
		ip saddr @affinity-O3LHJ3FD-default/backends/tcp/__10.244.2.22/80 goto endpoint-O3LHJ3FD-default/backends/tcp/__10.244.2.22/80
		ip saddr @affinity-PZ2P7BHH-default/backends/tcp/__10.244.2.23/80 goto endpoint-PZ2P7BHH-default/backends/tcp/__10.244.2.23/80
		ip saddr @affinity-CVDUOZVH-default/backends/tcp/__10.244.2.24/80 goto endpoint-CVDUOZVH-default/backends/tcp/__10.244.2.24/80
		numgen random mod 3 vmap { 0 : goto endpoint-O3LHJ3FD-default/backends/tcp/__10.244.2.22/80, 1 : goto endpoint-PZ2P7BHH-default/backends/tcp/__10.244.2.23/80, 2 : goto endpoint-CVDUOZVH-default/backends/tcp/__10.244.2.24/80 }
	}

	set affinity-CVDUOZVH-default/backends/tcp/__10.244.2.24/80 {
		type ipv4_addr
		size 65535
		flags dynamic,timeout
		timeout 3h
	}

	set affinity-O3LHJ3FD-default/backends/tcp/__10.244.2.22/80 {
		type ipv4_addr
		size 65535
		flags dynamic,timeout
		timeout 3h
	}

	set affinity-PZ2P7BHH-default/backends/tcp/__10.244.2.23/80 {
		type ipv4_addr
		size 65535
		flags dynamic,timeout
		timeout 3h
	}

	chain endpoint-CVDUOZVH-default/backends/tcp/__10.244.2.24/80 {
		ip saddr 10.244.2.24 jump mark-for-masquerade
		update @affinity-CVDUOZVH-default/backends/tcp/__10.244.2.24/80 { ip saddr }
		meta l4proto tcp dnat to 10.244.2.24:80
	}

	chain endpoint-O3LHJ3FD-default/backends/tcp/__10.244.2.22/80 {
		ip saddr 10.244.2.22 jump mark-for-masquerade
		update @affinity-O3LHJ3FD-default/backends/tcp/__10.244.2.22/80 { ip saddr }
		meta l4proto tcp dnat to 10.244.2.22:80
	}

	chain endpoint-PZ2P7BHH-default/backends/tcp/__10.244.2.23/80 {
		ip saddr 10.244.2.23 jump mark-for-masquerade
		update @affinity-PZ2P7BHH-default/backends/tcp/__10.244.2.23/80 { ip saddr }
		meta l4proto tcp dnat to 10.244.2.23:80
	}
  • service-GZEOLGI7-default/backends/tcp/ の chain の中に Endpoint の数だけ Session Affinity 用のルールが追加されています。
  • 送信元の IP アドレスが affinity-O3LHJ3FD-default/backends/tcp/__10.244.2.22/80 の set に含まれている場合、endpoint-O3LHJ3FD-default/backends/tcp/__10.244.2.22/80 の chain に飛びます。
  • Session Affinity の状態を管理するための set にこれまでと違っていくつかオプションが設定されています。
    • size で set の要素数を制限しています。
    • flags で set の中身を動的に変更するモードを指定しています。
    • timeout で set の中が要素で溢れないようにしています。タイムアウト時間を経過した古い要素は自動的に削除されます。要素を更新することでタイムアウト時間をリセットできます。後述するように同一の Pod に対するリクエストが 3 時間発生しなかった場合、set から送信元の Pod IP の情報が削除されます。このタイムアウト値は Service の .spec.sessionAffinityConfig.clientIP.timeoutSeconds で変更可能で、デフォルト値が 3 時間になっています。
  • それぞれの set に当てはまらない場合 (初回の接続の場合)、これまでと同様にランダムで生成した数字を Endpoint の数で割った余りをキーとして vmap を参照し、endpoint- の chain に飛びます。
  • endpoint- の chain で DNAT する前に affinity-PZ2P7BHH-default/backends/tcp/__10.244.2.23/80 の set を更新しています。同一の Pod から新たなリクエストがあった場合に、set の中の送信元の Pod IP の情報を更新することでタイムアウト値のカウントダウンをリセットしています。

kind-worker2 の client から Session Affinity が有効な Service に curl で /hostname に対してリクエストを投げます。

kubectl exec -it deploy/client -- curl http://backends/hostname

再度 kind-worker2 の Node 上の nftables のルールを確認します。

	set affinity-CVDUOZVH-default/backends/tcp/__10.244.2.24/80 {
		type ipv4_addr
		size 65535
		flags dynamic,timeout
		timeout 3h
		elements = { 10.244.1.14 timeout 3h expires 2h50m37s234ms }
	}

	set affinity-O3LHJ3FD-default/backends/tcp/__10.244.2.22/80 {
		type ipv4_addr
		size 65535
		flags dynamic,timeout
		timeout 3h
	}

	set affinity-PZ2P7BHH-default/backends/tcp/__10.244.2.23/80 {
		type ipv4_addr
		size 65535
		flags dynamic,timeout
		timeout 3h
	}
  • Session Affinity の状態を追跡するための set の中に送信元の IP アドレス (client の Pod IP) が含まれていることが確認できます。また、3 時間のタイマーが作動しているのが分かります。

再度 backends の Pod にリクエストを投げてみます。

kubectl exec -it deploy/client -- curl http://backends/hostname

kind-worker2 の Node 上の nftables のルールを確認します。

	set affinity-CVDUOZVH-default/backends/tcp/__10.244.2.24/80 {
		type ipv4_addr
		size 65535
		flags dynamic,timeout
		timeout 3h
		elements = { 10.244.1.14 timeout 3h expires 2h59m58s491ms }
	}
  • Session Affinity 用の set の client の Pod IP の情報のタイムアウト値がリセットされていることが確認できます。

作成したリソースを削除します。

cat <<EOF | kubectl delete -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backends
---
apiVersion: v1
kind: Service
metadata:
  name: backends
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
EOF

参考

Discussion