はじめに
eBPF の勉強として Cilium Network Policy がどのように反映され、どのように制御されているかソースコードを追いかけてみたのと、実際に L7 の CiliumNetworkPolicy を試してみたのでそのまとめ
対象の Cilium バージョンは 1.15.4
CiliumNetworkPolicy リソースの反映
まずは CiliumNetworkPolicy リソースを作成した際、どのように反映されるかを調べてみた
pkg/k8s/watchers/cilium_network_policy.go
の K8sWatcher.ciliumNetworkPoliciesInit() で CiliumNetworkPolicy リソースの追加、更新、削除イベントを検知している
ポリシーの反映にはエンドポイント再構築 (eBPF プログラムの再ロードと eBPF Map の更新) を伴うようで、cilium/daemon/cmd/policy.go
の Daemon.policyAdd() でポリシーリポジトリにポリシーを配布し、エンドポイントの再構築をキュー経由でトリガーしている
エンドポイントの再構築は cilium/pkg/endpoint/policy.go
の Endpoint.regenerate() で行われている
Cilium Network Policy は eBPF で実装されているので、eBPF でルールを扱えるように pkg/endpoint/bpf.go
の Endpoint.addPolicyKey() で eBPF Map へ変換され、書き込みは pkg/bpf/map_linux.go
の Map.Update() で行われている
Go からみた eBPF Map の構造体は pkg/maps/policymap/policymap.go
の PolicyKey で定義されている
type PolicyKey struct { Prefixlen uint32 `align:"lpm_key"` Identity uint32 `align:"sec_label"` TrafficDirection uint8 `align:"egress"` Nexthdr uint8 `align:"protocol"` DestPortNetwork uint16 `align:"dport"` // In network byte-order }
eBPF からみた eBPF Map の構造体は bpf/lib/common.h
の policy_key で定義されている
struct policy_key { struct bpf_lpm_trie_key lpm_key; __u32 sec_label; __u8 egress:1, pad:7; __u8 protocol; /* can be wildcarded if 'dport' is fully wildcarded */ __u16 dport; /* can be wildcarded with CIDR-like prefix */ };
これで L3/L4 の Cilium Network Policy は eBPF で評価できるはず
Cilium Network Policy は L7 にも対応しており、eBPF ではなく Envoy で制御されている
Envoy へポリシーを配布しているのは pkg/envoy/xds_server.go
の xdsServer.UpdateNetworkPolicy()
if policy != nil { policyRevision := policy.Revision callback = func(err error) { if err == nil { go ep.OnProxyPolicyUpdate(policyRevision) } } }
proxyPolicyRevision を更新して Envoy に反映している
func (e *Endpoint) OnProxyPolicyUpdate(revision uint64) { // NOTE: unconditionalLock is used here because this callback has no way of reporting an error e.unconditionalLock() if revision > e.proxyPolicyRevision { e.proxyPolicyRevision = revision } e.unlock() }
Cilium Network Policy による評価
続いては Cilium Network Policy でパケットをどのように評価し、パスしたりドロップしたりしているか、Endpoint to Endpoint (同一ホスト上の Pod to Pod) 通信、かつ IPv4 の Egress Policy (通信元でのポリシー評価) ケースでの実装を追ってみる
Endpoint to Endpoint においてパケットがどのように処理されるかの概要は 公式ドキュメント がわかりやすい
Endpoint to Endpoint 通信の場合、コンテナからパケットが出る際の tc でフックされ bpf/bpf_lxc.c
の cil_from_container() が呼び出される
L3/L4 のポリシーを評価しているのは bpf/lib/policy.h
の __policy_can_access() で、パケットから得られた情報を eBPF Map と同じ構造にセットし、システムコール bpf_map_lookup_elem で eBPF Map と突き合わせを行いポリシーを評価している
struct policy_key key = { .lpm_key = { POLICY_FULL_PREFIX, {} }, /* always look up with unwildcarded data */ .sec_label = remote_id, .egress = !dir, .pad = 0, .protocol = proto, .dport = dport, }; [...] policy = map_lookup_elem(map, &key); if (likely(policy && !policy->wildcard_dport)) { cilium_dbg3(ctx, DBG_L4_CREATE, remote_id, local_id, dport << 16 | proto); *match_type = POLICY_MATCH_L3_L4; /* 1. id/proto/port */ goto check_policy; } [...] check_policy: return __account_and_check(ctx, policy, ext_err, proxy_port);
L7 のポリシーを評価しているのは Envoy で、eBPF からのパケットリダイレクトには Linux カーネルの transparent proxy (tproxy) を利用しているとのこと
tproxy が有効な場合、bpf/lib/proxy.h
の __ctx_redirect_to_proxy() から bpf_sk_assign() ヘルパー関数を呼び出し、tproxy のソケットをアサインしてパケットをリダイレクトしている
#ifdef ENABLE_TPROXY static __always_inline int assign_socket_tcp(struct __ctx_buff *ctx, struct bpf_sock_tuple *tuple, __u32 len, bool established) { int result = DROP_PROXY_LOOKUP_FAILED; struct bpf_sock *sk; __u32 dbg_ctx; sk = skc_lookup_tcp(ctx, tuple, len, BPF_F_CURRENT_NETNS, 0); if (!sk) goto out; if (established && sk->state == BPF_TCP_TIME_WAIT) goto release; if (established && sk->state == BPF_TCP_LISTEN) goto release; dbg_ctx = sk->family << 16 | ctx->protocol; result = sk_assign(ctx, sk, 0); cilium_dbg(ctx, DBG_SK_ASSIGN, -result, dbg_ctx); if (result == 0) result = CTX_ACT_OK; else result = DROP_PROXY_SET_FAILED; release: sk_release(sk); out: return result; }
試してみる
Envoy へのパケットのリダイレクトを除き、概ね仕組みがわかったので実際に試してみる
まずは、Kind でさくっとクラスタを立ち上げて Cilium をインストールする
$ cat << EOF > cluster.yaml apiVersion: kind.x-k8s.io/v1alpha4 kind: Cluster nodes: - role: control-plane extraPortMappings: - containerPort: 30599 hostPort: 8080 listenAddress: 127.0.0.1 - role: worker - role: worker - role: worker networking: disableDefaultCNI: true podSubnet: 10.10.0.0/16 serviceSubnet: 10.11.0.0/16 apiServerAddress: 127.0.0.1 apiServerPort: 6443 EOF $ kind create cluster --config cluster.yaml $ helm install cilium cilium/cilium --version 1.15.4 \ --namespace kube-system \ --set envoy.enabled=true $ kubectl get po -n kube-system NAME READY STATUS RESTARTS AGE cilium-8d4qp 1/1 Running 0 22m cilium-envoy-2676w 1/1 Running 0 22m cilium-envoy-bhbrn 1/1 Running 0 22m cilium-envoy-thlmq 1/1 Running 0 22m cilium-envoy-wzkvw 1/1 Running 0 22m cilium-jnptw 1/1 Running 0 22m cilium-operator-5d64788c99-rhl76 1/1 Running 0 22m cilium-operator-5d64788c99-v7dtk 1/1 Running 0 22m cilium-tpmkw 1/1 Running 0 22m cilium-twp5s 1/1 Running 0 22m coredns-76f75df574-dsb9n 1/1 Running 0 23m coredns-76f75df574-xgx7z 1/1 Running 0 23m etcd-kind-control-plane 1/1 Running 0 23m kube-apiserver-kind-control-plane 1/1 Running 0 23m kube-controller-manager-kind-control-plane 1/1 Running 0 23m kube-proxy-fq296 1/1 Running 0 22m kube-proxy-j2wfc 1/1 Running 0 22m kube-proxy-lppbc 1/1 Running 0 23m kube-proxy-pdztg 1/1 Running 0 22m kube-scheduler-kind-control-plane 1/1 Running 0 23m
テスト用のクライアントとサーバアプリケーションを起動する
$ kubectl create ns sample $ kubectl run client --image alpine --labels 'app=client' -n sample --command -- sleep infinity $ kubectl exec client -c client -n sample -- sh -c 'apk --update add curl ca-certificates' $ kubectl run server --image hashicorp/http-echo --labels 'app=server' --port 5678 -n sample -- -text='hello world' $ kubectl get po -n sample NAME READY STATUS RESTARTS AGE client 1/1 Running 0 10m server 1/1 Running 0 3m35s $ kubectl expose po server --port=5678 --target-port=5678 -n sample $ kubectl get svc -n sample NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE server ClusterIP 10.11.129.123 <none> 5678/TCP 3s $ kubectl exec client -n sample -- curl -s server.sample.svc.cluster.local:5678 hello world
ここから L7 の CiliumNetworkPolicy を作成していくのだが、その前に現状のポリシーの様子を確認してみる
今回は Egress Policy を検証するため、client
アプリケーションのエンドポイントを確認する
$ kubectl get po -n sample -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES client 1/1 Running 0 33m 10.0.2.42 kind-worker2 <none> <none> server 1/1 Running 0 32m 10.0.1.97 kind-worker3 <none> <none> $ kubectl get po -n kube-system -l k8s-app=cilium -o wide | grep kind-worker2 cilium-jnptw 1/1 Running 0 64m 172.18.0.3 kind-worker2 <none> <none>
client
アプリケーションのエンドポイントを確認してみると 1955
であるとわかる
$ kubectl exec cilium-jnptw -c cilium-agent -n kube-system -- cilium-dbg endpoint list ENDPOINT POLICY (ingress) POLICY (egress) IDENTITY LABELS (source:key[=value]) IPv6 IPv4 STATUS ENFORCEMENT ENFORCEMENT 191 Disabled Disabled 4 reserved:health 10.0.2.26 ready 1402 Disabled Disabled 1 reserved:host ready 1955 Disabled Enabled 28112 k8s:app=client 10.0.2.42 ready k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=sample k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=sample
CiliumNetworkPolicy を作成してないエンドポイントの状態
$ kubectl exec cilium-jnptw -c cilium-agent -n kube-system -- cilium-dbg bpf policy get -n 1955 POLICY DIRECTION IDENTITY PORT/PROTO PROXY PORT AUTH TYPE BYTES PACKETS PREFIX Allow Ingress 0 ANY NONE disabled 0 0 0 Allow Ingress 1 ANY NONE disabled 0 0 0 Allow Egress 0 ANY NONE disabled 0 0 0
CiliumNetworkPolicy を作成してみる
$ cat << EOF | kubectl apply -f - apiVersion: "cilium.io/v2" kind: CiliumNetworkPolicy metadata: name: sample namespace: sample spec: endpointSelector: matchLabels: app: client egress: - toEndpoints: - matchLabels: app: server toPorts: - ports: - port: '5678' protocol: TCP rules: http: - method: GET path: / - toEndpoints: - matchLabels: k8s:io.kubernetes.pod.namespace: kube-system k8s-app: kube-dns EOF
Egress Policy が増えていることが確認できる
$ kubectl exec cilium-jnptw -c cilium-agent -n kube-system -- cilium-dbg bpf policy get -n 1955 POLICY DIRECTION IDENTITY PORT/PROTO PROXY PORT AUTH TYPE BYTES PACKETS PREFIX Allow Ingress 0 ANY NONE disabled 0 0 0 Allow Ingress 1 ANY NONE disabled 0 0 0 Allow Egress 8600 ANY NONE disabled 0 0 0 Allow Egress 11584 5678/TCP 12440 disabled 0 0 24 Allow Egress 15567 ANY NONE disabled 0 0 0
cilium-agent のログを確認してみると、ソースコードを追いかけてわかったように、k8s-watcher で CiliumNetworkPolicy リソースを検知したのち、ポリシーがエンドポイントにインポートされ、eBPF プログラムと eBPF Map の再構築が行われている様子がわかる
$ kubectl logs cilium-jnptw -n kube-system time="2024-06-07T06:32:28Z" level=info msg="Policy Add Request" ciliumNetworkPolicy="[&{EndpointSelector:{\"matchLabels\":{\"any:app\":\"client\",\"k8s:io.kubernetes.pod.namespace\":\"sample\"}} NodeSelector:{} Ingress:[] IngressDeny:[] Egress:[{EgressCommonRule:{ToEndpoints:[{\"matchLabels\":{\"any:app\":\"server\",\"k8s:io.kubernetes.pod.namespace\":\"sample\"}}] ToRequires:[] ToCIDR: ToCIDRSet:[] ToEntities:[] ToServices:[] ToGroups:[] aggregatedSelectors:[]} ToPorts:[{Ports:[{Port:5678 Protocol:TCP}] TerminatingTLS:<nil> OriginatingTLS:<nil> ServerNames:[] Listener:<nil> Rules:0xc0003b51f0}] ToFQDNs:[] ICMPs:[] Authentication:<nil>} {EgressCommonRule:{ToEndpoints:[{\"matchLabels\":{\"any:k8s-app\":\"kube-dns\",\"k8s:io.kubernetes.pod.namespace\":\"kube-system\"}}] ToRequires:[] ToCIDR: ToCIDRSet:[] ToEntities:[] ToServices:[] ToGroups:[] aggregatedSelectors:[]} ToPorts:[] ToFQDNs:[] ICMPs:[] Authentication:<nil>}] EgressDeny:[] Labels:[k8s:io.cilium.k8s.policy.derived-from=CiliumNetworkPolicy k8s:io.cilium.k8s.policy.name=sample k8s:io.cilium.k8s.policy.namespace=sample k8s:io.cilium.k8s.policy.uid=7756e978-2aa8-4baf-a4ed-9cfe64d01088] Description:}]" policyAddRequest=5eb088ab-a5a0-4cc0-b319-3af097f3ce51 subsys=daemon time="2024-06-07T06:32:28Z" level=info msg="Imported CiliumNetworkPolicy" ciliumNetworkPolicyName=sample k8sApiVersion= k8sNamespace=sample subsys=k8s-watcher time="2024-06-07T06:32:28Z" level=info msg="Policy imported via API, recalculating..." policyAddRequest=5eb088ab-a5a0-4cc0-b319-3af097f3ce51 policyRevision=26 subsys=daemon time="2024-06-07T06:32:29Z" level=info msg="Re-pinning map with ':pending' suffix" bpfMapName=cilium_calls_01955 bpfMapPath=/sys/fs/bpf/tc/globals/cilium_calls_01955 subsys=bpf time="2024-06-07T06:32:29Z" level=info msg="Unpinning map after successful recreation" bpfMapName=cilium_calls_01955 bpfMapPath="/sys/fs/bpf/tc/globals/cilium_calls_01955:pending" subsys=bpf time="2024-06-07T06:32:29Z" level=info msg="Rewrote endpoint BPF program" ciliumEndpointName=sample/client containerID=7f7d63e703 containerInterface= datapathPolicyRevision=25 desiredPolicyRevision=26 endpointID=1955 identity=28112 ipv4=10.0.2.42 ipv6= k8sPodName=sample/client subsys=endpoint
せっかくなので、ポリシーの eBPF Map を確認しようとしたがキャッシュが無効化されており参照できないとのこと
$ kubectl exec cilium-jnptw -c cilium-agent -n kube-system -- cilium-dbg map list Name Num entries Num errors Cache enabled cilium_tunnel_map 3 0 true cilium_lb4_services_v2 12 0 true cilium_lb4_reverse_nat 5 0 true cilium_policy_01402 0 0 false cilium_policy_01955 0 0 false cilium_runtime_config 0 0 false cilium_ipcache 18 0 true cilium_lb4_backends_v3 7 0 true cilium_lb4_source_range 0 0 true cilium_policy_00191 0 0 false cilium_lxc 4 0 true cilium_auth_map 0 0 false cilium_node_map 0 0 false cilium_metrics 0 0 false cilium_l2_responder_v4 0 0 false $ kubectl exec cilium-jnptw -c cilium-agent -n kube-system -- cilium-dbg map get cilium_policy_01955 Cache is disabled
念の為、アプリケーションの疎通確認
$ kubectl exec client -n sample -- curl -s server.sample.svc.cluster.local:5678 hello world
CiliumNetworkPolicy での許可メソッドを POST に変更すると、しっかりと拒否されることも確認できた
$ cat << EOF | kubectl apply -f - apiVersion: "cilium.io/v2" kind: CiliumNetworkPolicy metadata: name: sample namespace: sample spec: endpointSelector: matchLabels: app: client egress: - toEndpoints: - matchLabels: app: server toPorts: - ports: - port: '5678' protocol: TCP rules: http: - method: POST path: / - toEndpoints: - matchLabels: k8s:io.kubernetes.pod.namespace: kube-system k8s-app: kube-dns EOF $ kubectl exec client -n sample -- curl -s server.sample.svc.cluster.local:5678 Access denied
さいごに
まだまだ細かいところで何をしているかはわからないけど、Cilium の中で eBPF がどのように利用されているのかが大枠で把握でき、今後業務で利用することがあるかはわからないが、Cilium Network Policy で何かトラブルが起きた時にソースコードから原因分析するというアプローチが取れそう
eBPF については eBPF Map への書き込みや参照がどのように行われているかを把握でてきた程度だが、とりあえず入門としては十分なんじゃないかと思う
どこかで実際に eBPF プログラムのサンプルを書いて動かすということをやってみたい
なお、Cilium 自体への興味も出てきて、kube-proxy を置き換えることができたり、サービスメッシュとしても振る舞ったりするとのことなので、その辺りも深ぼって調査してみたい
さいごに、普段アプリケーションの読み書きをすることがほとんどないためソースコードを追いかけるのに非常に時間がかかったけど、思ったよりも楽しめたのはよかった (次はもっと効率的に作業できるとうれしい)