Cilium 如何處理 L7 流量
還記得在 使用 Cilium 增強(qiáng) Kubernetes 網(wǎng)絡(luò)安全[1] 示例中,我們通過(guò)設(shè)置網(wǎng)絡(luò)策略限制鈦戰(zhàn)機(jī) tiefighter 訪問(wèn)死星 deathstar 的 /v1/exhaust-port 端點(diǎn),但放行著陸請(qǐng)求 /v1/request-landing。在提起 Cilium 時(shí),都說(shuō)其是使用 eBPF 技術(shù)推動(dòng)的用于提供、保護(hù)和觀察容器工作負(fù)載之間的網(wǎng)絡(luò)連接的開(kāi)源軟件。eBPF 可以處理 L3/4 的數(shù)據(jù)包,但是對(duì)復(fù)雜的 L7 的協(xié)議處理的成本比較高,并且無(wú)法應(yīng)對(duì) L7 協(xié)議策略的靈活性。Cilium 引入 Envoy Proxy[2](Cilium 定制的發(fā)行版)作為 L7 代理,來(lái)處理該場(chǎng)景。
那 Cilium 是如何處理 L7 流量的呢?今天就讓我們一探究竟。
注,這篇的內(nèi)容是基于目前最新的 Cilium 1.13.3 和 proxy 1.23.9,不同版本間會(huì)有差異。
在開(kāi)始之前先搭建先前的“星球大戰(zhàn)”環(huán)境,或者你也可以直接跳到 Debug 階段[3]。
環(huán)境搭建
集群
export INSTALL_K3S_VERSION=v1.27.1+k3s1
curl -sfL https://get.k3s.io | sh -s - --disable traefik --disable local-storage --disable metrics-server --disable servicelb --flannel-backend=none --write-kubeconfig-mode 644 --write-kubeconfig ~/.kube/config
安裝 Cilium
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/master/stable.txt)
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
sha256sum --check cilium-linux-${CLI_ARCH}.tar.gz.sha256sum
sudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin
rm cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
cilium install
安裝示例應(yīng)用
kubectl apply -n default -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: deathstar
labels:
app.kubernetes.io/name: deathstar
spec:
type: ClusterIP
ports:
- port: 80
selector:
org: empire
class: deathstar
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: deathstar
labels:
app.kubernetes.io/name: deathstar
spec:
replicas: 1
selector:
matchLabels:
org: empire
class: deathstar
template:
metadata:
labels:
org: empire
class: deathstar
app.kubernetes.io/name: deathstar
spec:
containers:
- name: deathstar
image: docker.io/cilium/starwars
---
apiVersion: v1
kind: Pod
metadata:
name: tiefighter
labels:
org: empire
class: tiefighter
app.kubernetes.io/name: tiefighter
spec:
containers:
- name: spaceship
image: docker.io/tgraf/netperf
---
apiVersion: v1
kind: Pod
metadata:
name: xwing
labels:
app.kubernetes.io/name: xwing
org: alliance
class: xwing
spec:
containers:
- name: spaceship
image: docker.io/tgraf/netperf
EOF
設(shè)置策略
kubectl apply -n default -f - <<EOF
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "rule1"
spec:
description: "L7 policy to restrict access to specific HTTP call"
endpointSelector:
matchLabels:
org: empire
class: deathstar
ingress:
- fromEndpoints:
- matchLabels:
org: empire
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "POST"
path: "/v1/request-landing"
EOF
測(cè)試
kubectl exec tiefighter -- curl -s -XPUT deathstar.default.svc.cluster.local/v1/exhaust-port
#Access denied
kubectl exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
#Ship landed
查看 pod 信息。
kubectl get po -o wide -n default
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
deathstar-7848d6c4d5-58jc8 1/1 Running 0 6h57m 10.0.0.111 ubuntu-dev3 <none> <none>
xwing 1/1 Running 0 6h57m 10.0.0.209 ubuntu-dev3 <none> <none>
tiefighter 1/1 Running 0 6h57m 10.0.0.123 ubuntu-dev3 <none> <none>
后面 debug 的操作我們會(huì)直接在 cilium 的 agent pod 進(jìn)行。
agent=$(kubectl get po -l app.kubernetes.io/name=cilium-agent -n kube-system -o jsonpath='{.items[0].metadata.name}')
Debug
先貼上總結(jié)的圖。
怎么下手呢?
在 深入探索 Cilium 的工作機(jī)制[4] 時(shí),我們對(duì) Cilium 的網(wǎng)絡(luò)策略處理機(jī)制一筆帶過(guò):
Cilium Agent 中運(yùn)行著大量的 watcher,其中一個(gè)就是 CiliumNetworkPolicy watcher。當(dāng)策略創(chuàng)建或者更新時(shí),Agent 會(huì)對(duì)策略進(jìn)行轉(zhuǎn)換并將規(guī)則存儲(chǔ)到 BPF Map 中。在網(wǎng)絡(luò)通信時(shí),BPF 程序會(huì)對(duì)網(wǎng)絡(luò)流量進(jìn)行檢查并決定應(yīng)當(dāng)允許或者拒絕訪問(wèn)。
實(shí)際上這里的處理比較復(fù)雜,我們從 watcher 的初始化入手。
- #enableK8sWatchers[5] 開(kāi)啟一些列的 watcher
- #ciliumNetworkPoliciesInit[6] 開(kāi)啟 CiliumNetworkPolicy watcher
- #PolicyAdd[8] 將規(guī)則寫(xiě)入 Daemon[9] 的策略倉(cāng)庫(kù)中,實(shí)際發(fā) PolicyAddEvent 到 repository-change-queue 隊(duì)列中。
- #policyAdd[10] 對(duì)規(guī)則進(jìn)行預(yù)處理,并收集與規(guī)則相關(guān)的 endpoint(需要重新生成 endpoint 的數(shù)據(jù),如加載 BPF 程序、更新 map 等),推送 PolicyReactionEvent 事件
- EndpointRegenerationEvent#Handle[12] 事件的處理過(guò)程
- Endpoint.regenerateBPF[14] 重新加載 datapath BPF 程序,刷新 Map。
- Endpoint.regenerate[13]
- PolicyReactionEvent.Handle[11] 事件處理的過(guò)程,依次處理所有策略相關(guān)的 endpoint,最后有發(fā)出 EndpointRegenerationEvent 事件
- #addCiliumNetworkPolicyV2[7] 添加 CiliumNetworkPolicy 的處理
至此我們 apply 的網(wǎng)絡(luò)策略被寫(xiě)入到 map 中。
接下來(lái)看下 ebpf 程序有任何使用該策略。
eBPF
還記得在 Kubernetes 網(wǎng)絡(luò)學(xué)習(xí)之 Cilium 與 eBPF[15] 中我們分析容器發(fā)出的數(shù)據(jù)包,被 LXC BPF Ingress程序處理。這里不再贅述,處理流程可以看那篇文章。
我們先查看死星的 endpoint id 和 identity 分別為 863 和 2033。
kubectl get ciliumendpoint -n default
NAME ENDPOINT ID IDENTITY ID INGRESS ENFORCEMENT EGRESS ENFORCEMENT VISIBILITY POLICY ENDPOINT STATE IPV4 IPV6
tiefighter 2216 29439 <status disabled> <status disabled> <status disabled> ready 10.0.0.123
deathstar-7848d6c4d5-58jc8 863 2033 <status disabled> <status disabled> <status disabled> ready 10.0.0.111
xwing 775 5513 <status disabled> <status disabled> <status disabled> ready 10.0.0.209
使用 endpoint id 通過(guò)通過(guò)命令查看為死星配置的網(wǎng)絡(luò)策略,可以看到其中的兩條 ingress 的策略,其代理端口 19313,這個(gè)端口就是 Cilium 中 L7 代理的監(jiān)聽(tīng)端口。
kubectl exec $agent -n kube-system -c cilium-agent -- cilium bpf policy get 863
POLICY DIRECTION LABELS (source:key[=value]) PORT/PROTO PROXY PORT BYTES PACKETS
Allow Ingress reserved:host ANY NONE 0 0
reserved:kube-apiserver
Allow Ingress k8s:app.kubernetes.io/name=deathstar 80/TCP 19313 0 0
k8s:class=deathstar
k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default
k8s:io.cilium.k8s.policy.cluster=default
k8s:io.cilium.k8s.policy.serviceaccount=default
k8s:io.kubernetes.pod.namespace=default
k8s:org=empire
Allow Ingress k8s:app.kubernetes.io/name=tiefighter 80/TCP 19313 0 0
k8s:class=tiefighter
k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default
k8s:io.cilium.k8s.policy.cluster=default
k8s:io.cilium.k8s.policy.serviceaccount=default
k8s:io.kubernetes.pod.namespace=default
k8s:org=empire
Allow Egress reserved:unknown ANY NONE 0 0
BPF 程序處理流量在檢查策略時(shí) bpf_lxc.c#L1842[16],檢查配置的策略帶有代理端口執(zhí)行 POLICY_ACT_PROXY_REDIRECT 將流量重定向給代理(端口 19313,地址為主機(jī)地址)。
Cilium Proxy
Cilium agent 提供了 xds server 實(shí)現(xiàn),通過(guò) Unix Domain Socket /var/run/cilium/xds.sock 與 proxy 進(jìn)行通信,下發(fā)配置。
我們參考 cilium-bugtool 的 dump 源碼[17],dump 代理的配置。
kubectl exec $agent -n kube-system -c cilium-agent -- curl -s --unix-socket /var/run/cilium/envoy-admin.sock http://admin/config_dump?include_eds
從配置 config.json 中可以看到 Cilium 在 envoy proxy 中實(shí)現(xiàn)了如下三個(gè)不同類型的過(guò)濾器(Filter):
- listener filter
- filter
- http filter
監(jiān)聽(tīng)器過(guò)濾器
監(jiān)聽(tīng)器過(guò)濾器(Listener Filter)`cilium.BpfMetadata`[18] 會(huì)從幾個(gè)數(shù)據(jù)源中準(zhǔn)備元數(shù)據(jù):策略、監(jiān)聽(tīng)器設(shè)置、請(qǐng)求方的標(biāo)識(shí)等。數(shù)據(jù)源包括 xds 配置、BPF map cilium_ipcache、cilium_ct4_global(ct:connection tracking。當(dāng)然還包括 ct6 相關(guān)的 map)。
從數(shù)據(jù)源中獲取的數(shù)據(jù)保存在 socket option 中(proxy 源碼 bpf_metadata.cc#L364[19]),作為上下文元數(shù)據(jù)的在其他的過(guò)濾器中使用。
元數(shù)據(jù)數(shù)據(jù)源
xds filter 配置,這里提供了 bpf map 的根目錄 /sys/fs/bpf,以及 is_ingress: true 表示當(dāng)前 filter 是在入口監(jiān)聽(tīng)器上(ingress listener):
{
"name": "cilium.bpf_metadata",
"typed_config": {
"@type": "type.googleapis.com/cilium.BpfMetadata",
"bpf_root": "/sys/fs/bpf",
"is_ingress": true
}
xds network policy 配置(截取了 proxy 的部分配置),從配置中可以找到 endpoint 的 IP 和 id,以及前面我們?cè)O(shè)置的 規(guī)則[20]:
{
"@type": "type.googleapis.com/cilium.NetworkPoliciesConfigDump",
"networkpolicies": [
{
"endpoint_ips": [
"10.0.0.111"
],
"endpoint_id": "863",
"ingress_per_port_policies": [
{
"port": 80,
"rules": [
{
"http_rules": {
"http_rules": [
{
"headers": [
{
"name": ":method",
"safe_regex_match": {
"google_re2": {},
"regex": "POST"
}
},
{
"name": ":path",
"safe_regex_match": {
"google_re2": {},
"regex": "/v1/request-landing"
}
}
]
}
]
}
}
]
}
],
"egress_per_port_policies": [
{}
],
"conntrack_map_name": "global"
},
...
}
Map cilium_ipcache,可以通過(guò)連接信息中的 IP 地址獲取身份標(biāo)識(shí),如死星的 `identity`[21] 為 2033(見(jiàn) proxy 源碼 bpf_metadata.cc#L165[22]):
kubectl exec $agent -n kube-system -c cilium-agent -- cilium bpf ipcache list
IP PREFIX/ADDRESS IDENTITY
10.0.0.67/32 identity=1 encryptkey=0 tunnelendpoint=0.0.0.0 nodeid=0
10.0.0.111/32 identity=2033 encryptkey=0 tunnelendpoint=0.0.0.0 nodeid=0
10.0.0.123/32 identity=29439 encryptkey=0 tunnelendpoint=0.0.0.0 nodeid=0
10.0.0.243/32 identity=4 encryptkey=0 tunnelendpoint=0.0.0.0 nodeid=0
10.0.0.160/32 identity=19608 encryptkey=0 tunnelendpoint=0.0.0.0 nodeid=0
10.0.0.209/32 identity=5513 encryptkey=0 tunnelendpoint=0.0.0.0 nodeid=0
192.168.1.13/32 identity=1 encryptkey=0 tunnelendpoint=0.0.0.0 nodeid=0
0.0.0.0/0 identity=2 encryptkey=0 tunnelendpoint=0.0.0.0 nodeid=0
Map cilium_ct4_global,從連接跟蹤(connection tracking)中獲取請(qǐng)求方的 identity(SourceSecurityID 29439,鈦戰(zhàn)機(jī)的標(biāo)識(shí)):
cilium bpf ct list global
TCP OUT 10.0.0.123:48954 -> 10.0.0.111:80 expires=58774 RxPackets=4 RxBytes=435 RxFlagsSeen=0x1b LastRxReport=58764 TxPackets=6 TxBytes=522 TxFlagsSeen=0x1b LastTxReport=58764 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=4 SourceSecurityID=29439 IfIndex=0
TCP IN 10.0.0.67:33988 -> 10.0.0.111:80 expires=58776 RxPackets=6 RxBytes=659 RxFlagsSeen=0x1b LastRxReport=58766 TxPackets=4 TxBytes=386 TxFlagsSeen=0x1b LastTxReport=58766 Flags=0x0013 [ RxClosing TxClosing SeenNonSyn ] RevNAT=0 SourceSecurityID=29439 IfIndex=0
TCP IN 10.0.0.123:48954 -> 10.0.0.111:80 expires=80364 RxPackets=6 RxBytes=522 RxFlagsSeen=0x1b LastRxReport=58764 TxPackets=0 TxBytes=0 TxFlagsSeen=0x00 LastTxReport=0 Flags=0x0051 [ RxClosing SeenNonSyn ProxyRedirect ] RevNAT=0 SourceSecurityID=29439 IfIndex=0
過(guò)濾器
過(guò)濾器(Filter)`cilium.NetworkFilter`[23] 工作在 L4,用于處理已建立的鏈接,應(yīng)用端口級(jí)的策略,即 L4 策略。
從上下文元數(shù)據(jù)中保存的 endpoint 相關(guān)的策略中查找與目標(biāo)端口相關(guān)的策略,檢查請(qǐng)求方證書(shū)中的 sni 和請(qǐng)求方的身份標(biāo)識(shí) identity 是否在白名單中,見(jiàn) proxy 源碼 network_filter.cc#L169[24]。
假如策略上設(shè)置了 L7 的協(xié)議,會(huì)使用 Golang 編寫(xiě)的解析器對(duì) L7 的數(shù)據(jù)進(jìn)行解析。
在本示例中并未使用 L4 的策略。
HTTP 過(guò)濾器
HTTP 過(guò)濾器(HTTP Filter)`cilium.L7Policy`[25] 是本文的重點(diǎn),但相對(duì)其他兩個(gè)過(guò)濾器來(lái)說(shuō)邏輯就簡(jiǎn)單多了。
"http_filters": [
{
"name": "cilium.l7policy",
"typed_config": {
"@type": "type.googleapis.com/cilium.L7Policy",
"access_log_path": "/var/run/cilium/access_log.sock"
}
}
在過(guò)濾器對(duì) HTTP 請(qǐng)求頭進(jìn)行解碼時(shí)(見(jiàn) proxy 源碼 l7policy.cc#L97[26]),依然是從上下文元數(shù)據(jù)中獲取策略等內(nèi)容。拿到策略后,與請(qǐng)求方(對(duì)于這里 ingress 的場(chǎng)景檢查請(qǐng)求方,如果是 egress 的場(chǎng)景,檢查上游的標(biāo)識(shí))的標(biāo)識(shí)、請(qǐng)求頭的信息進(jìn)行比對(duì),決定放行還是拒絕請(qǐng)求。
總結(jié)
整篇看下來(lái),Cilium 在處理 L7 流量上的實(shí)現(xiàn)還是比較復(fù)雜的,牽扯多個(gè)組件協(xié)同。eBPF 在 L3/L4 流量處理上有著優(yōu)異的性能優(yōu)勢(shì),但是對(duì) L7 流量處理仍然無(wú)法脫離 sidecar 代理(不論 sidecar 是 per pod 還是 per node)。而 L7 流量處理也恰恰有著非常多的使用場(chǎng)景,不僅僅是 HTTP 協(xié)議。
參考資料
[1] 使用 Cilium 增強(qiáng) Kubernetes 網(wǎng)絡(luò)安全: https://atbug.com/enhance-kubernetes-network-security-with-cilium/
[2] Envoy Proxy: https://github.com/cilium/proxy
[3] Debug 階段: #debug
[4] 深入探索 Cilium 的工作機(jī)制: https://atbug.com/deep-dive-into-cilium/#網(wǎng)絡(luò)策略
[5] #enableK8sWatchers: https://github.com/cilium/cilium/blob/f9bdd00c4910bfe3bac3b208fdfbb9452487e776/pkg/k8s/watchers/watcher.go#L525
[6] #ciliumNetworkPoliciesInit: https://github.com/cilium/cilium/blob/f9bdd00c4910bfe3bac3b208fdfbb9452487e776/pkg/k8s/watchers/cilium_network_policy.go#L85
[7] #addCiliumNetworkPolicyV2: https://github.com/cilium/cilium/blob/f9bdd00c4910bfe3bac3b208fdfbb9452487e776/pkg/k8s/watchers/cilium_network_policy.go#L159
[8] #PolicyAdd: https://github.com/cilium/cilium/blob/f9bdd00c4910bfe3bac3b208fdfbb9452487e776/daemon/cmd/policy.go#L224
[9] Daemon: https://atbug.com/deep-dive-into-cilium/#agent
[10] #policyAdd: https://github.com/cilium/cilium/blob/f9bdd00c4910bfe3bac3b208fdfbb9452487e776/daemon/cmd/policy.go#L249
[11] PolicyReactionEvent.Handle: https://github.com/cilium/cilium/blob/f9bdd00c4910bfe3bac3b208fdfbb9452487e776/daemon/cmd/policy.go#L454
[12] EndpointRegenerationEvent#Handle: https://github.com/cilium/cilium/blob/f9bdd00c4910bfe3bac3b208fdfbb9452487e776/pkg/endpoint/events.go#L27
[13] Endpoint.regenerate: https://github.com/cilium/cilium/blob/f9bdd00c4910bfe3bac3b208fdfbb9452487e776/pkg/endpoint/policy.go#L286
[14] Endpoint.regenerateBPF: https://github.com/cilium/cilium/blob/f9bdd00c4910bfe3bac3b208fdfbb9452487e776/pkg/endpoint/bpf.go#L584
[15] Kubernetes 網(wǎng)絡(luò)學(xué)習(xí)之 Cilium 與 eBPF: https://atbug.com/learn-cilium-and-ebpf/#第-2-步pod1-lxc-bpf-ingress
[16] bpf_lxc.c#L1842: https://github.com/cilium/cilium/blob/f9bdd00c4910bfe3bac3b208fdfbb9452487e776/bpf/bpf_lxc.c#L1842
[17] dump 源碼: https://github.com/cilium/cilium/blob/f9bdd00c4910bfe3bac3b208fdfbb9452487e776/bugtool/cmd/root.go#L505
[18] cilium.BpfMetadata: https://github.com/cilium/proxy/blob/v1.23/cilium/bpf_metadata.cc
[19] bpf_metadata.cc#L364: https://github.com/cilium/proxy/blob/v1.23/cilium/bpf_metadata.cc#L364
[20] 規(guī)則: #設(shè)置策略
[21] identity: https://atbug.com/deep-dive-into-cilium/#端點(diǎn)-endpoint
[22] bpf_metadata.cc#L165: https://github.com/cilium/proxy/blob/v1.23/cilium/bpf_metadata.cc#L165
[23] cilium.NetworkFilter: https://github.com/cilium/proxy/blob/v1.23/cilium/network_filter.cc
[24] network_filter.cc#L169: https://github.com/cilium/proxy/blob/v1.23/cilium/network_filter.cc#L169
[25] cilium.L7Policy: https://github.com/cilium/proxy/blob/v1.23/cilium/l7policy.cc
[26] l7policy.cc#L97: https://github.com/cilium/proxy/blob/v1.23/cilium/l7policy.cc#L97