[Kubernetes 1.30] kube-proxy の nftables モード
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 連携のし易さから kube-proxy を利用しないケースも
- Cilium (Kubernetes Without kube-proxy)
- Calico (eBPF dataplane)
-
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 の数が増えるとルールの更新などのパフォーマンスが悪化
- Linux におけるファイアウォール機能を実現するために開発された
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 固有の機能を使う形で実装されています。
- kube-proxy 入門
- Serviceをたずねて3000行 - Kubernetesコードリーディングの旅
- kube-proxy は何をしてるのか
- The Tale of Kube-proxy: An Epic Exploration of Networking in Kubernetes Clusters
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. ip
や ip6
, inet
, arp
, …) 毎に作成可能で kube-proxy は ip
と ip6
のテーブルをそれぞれ作成します。
table ip kube-proxy {
...
}
set
(集合) や map
(連想配列) のデータ構造が利用でき、高速なデータの参照が可能です。
set cluster-ips {
type ipv4_addr
elements = { 10.96.0.1, 10.96.0.10 }
}
- type には
ipv4_addr
やinet_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-prerouting
もnat-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-prerouting
でservices
の 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 文を実行 - ラウンドロビンではない点に注意
- numgen random でランダムな数字を生成して
-
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