1. 程式人生 > 其它 >K8s罪魁禍首之"HostPort劫持了我的流量"

K8s罪魁禍首之"HostPort劫持了我的流量"

最近排查了一個kubernetes中使用了hostport後遇到比較坑的問題,奇怪的知識又增加了.

問題背景

叢集環境為K8sv1.15.9,cni指定了flannel-vxlanportmap, kube-proxy使用mode為ipvs,叢集3臺master,同時也是node,這裡以node-1/node-2/node-3來表示。

叢集中有2個mysql, 部署在兩個namespace下,mysql本身不是問題重點,這裡就不細說,這裡以mysql-A,mysql-B來表示。

mysql-A落在node-1上,mysql-B落在node-2上,兩個資料庫svc名跟使用者、密碼完全不相同

出現詭異的現象這裡以一張圖來說明會比較清楚一些:

其中綠線的表示訪問沒有問題,紅線表示連線Mysql-A提示使用者名稱密碼錯誤。

特別詭異的是,當在Node-2上通過svc訪問Mysql-A時,輸入Mysql-A的使用者名稱跟密碼提示密碼錯誤,密碼確認無疑,但當輸入Mysql-B的使用者名稱跟密碼,居然能夠連線上,看了下資料,連上的是Mysql-B的資料庫,給人的感覺就是請求轉到了Mysql-A, 最後又轉到了Mysql-B,當時讓人大跌眼鏡。碰到詭異的問題那就排查吧,排查的過程倒是不費什麼事,最主要的是要通過這次踩坑機會挖掘一些奇怪的知識出來。

排查過程

既然在Node-1上連線Mysql-A/Mysql-B都沒有問題,那基本可以排查是Mysql-A的問題

經實驗,在Node-2上所有的服務想要連Mysql-A時,都有這個問題,但是訪問其它的服務又都沒有問題,說明要麼是mysql-A的3306這個埠有問題,通過上一步應該排查了mysql-A的問題,那問題只能出在Node-2上

在k8s中像這樣的請求轉發出現詭異現象,當排除了一些常見的原因之外,最大的嫌疑就是iptables了,作者遇到過多次,這次也不例外,雖然當前叢集使用的ipvs, 但還是照例看下iptables規則,檢視Node-2上的iptables與Node-1的iptables比對,結果有蹊蹺, 在Node-2上發現有以下的規則在其它節點上沒有

-ACNI-DN-xxxx-ptcp-mtcp--dport3306-jDNAT--to-destination10.224.0.222:3306
-ACNI-HOSTPORT-DNAT-mcomment--comment"dnatname":\"cni0\"id:\"xxxxxxxxxxxxx\""-jCNI-DN-xxx
-ACNI-HOSTPORT-SNAT-mcomment--comment"snatname":\"cni0\"id:\"xxxxxxxxxxxxx\""-jCNI-SN-xxx
-ACNI-SN-xxx-s127.0.0.1/32-d10.224.0.222/32-ptcp-mtcp--dport80-jMASQUERADE

其中10.224.0.222為Mysql-B的pod ip,xxxxxxxxxxxxx經查實為Mysql-B對應的pause容器的id,從上面的規則總結一下就是目的為3306埠的請求都會轉發到10.224.0.222這個地址,即Mysql-B。看到這裡,作者明白了為什麼在Node-2上去訪問Node-1上Mysql-A的3306會提示密碼錯誤而輸入Mysql-B的密碼卻可以正常訪問雖然兩個mysql的svc名不一樣,但上面的iptables只要目的埠是3306就轉發到Mysql-B了,當請求到達mysql後,使用正確的使用者名稱密碼自然可以登入成功

原因是找到了,但是又引出來了更多的問題?

  1. 這幾條規則是誰入到iptables中的?
  2. 怎麼解決呢,是不是刪掉就可以?

問題復現

同樣是Mysql,為何Mysql-A沒有呢? 那麼比對一下這兩個Mysql的部署差異

比對發現, 除了使用者名稱密碼,ns不一樣外,Mysql-B部署時使用了hostPort=3306, 其它的並無異常

難道是因為hostPort?

作者日常會使用NodePort,倒卻是沒怎麼在意hostPort,也就停留在hostPort跟NodePort的差別在於NodePort是所有Node上都會開啟埠,而hostPort只會在執行機器上開啟埠,由於hostPort使用的也少,也就沒太多關注,網上短暫搜了一番,描述的也不是很多,看起來大家也用的不多

那到底是不是因為hostPort呢?

Talk is cheap, show me the code

通過實驗來驗證,這裡簡單使用了三個nginx來說明問題, 其中兩個使用了hostPort,這裡特意指定了不同的埠,其它的都完全一樣,釋出到叢集中,yaml檔案如下

apiVersion:apps/v1
kind:Deployment
metadata:
name:nginx-hostport2
labels:
k8s-app:nginx-hostport2
spec:
replicas:1
selector:
matchLabels:
k8s-app:nginx-hostport2
template:
metadata:
labels:
k8s-app:nginx-hostport2
spec:
nodeName:spring-38
containers:
-name:nginx
image:nginx:latest
ports:
-containerPort:80
hostPort:31123

Finally,問題復現:

可以肯定,這些規則就是因為使用了hostPort而寫入的,但是由誰寫入的這個問題還是沒有解決?

罪魁禍首

作者開始以為這些iptables規則是由kube-proxy寫入的, 但是檢視kubelet的原始碼並未發現上述規則的關鍵字

再次實驗及結合網上的探索,可以得到以下結論:

首先從kubernetes的官方發現以下描述:

The CNI networking plugin supportshostPort. You can use the officialportmap[1]plugin offered by the CNI plugin team or use your own plugin with portMapping functionality.

If you want to enablehostPortsupport, you must specifyportMappings capabilityin yourcni-conf-dir. For example:

{
"name":"k8s-pod-network",
"cniVersion":"0.3.0",
"plugins":[
{
#...其它的plugin
}
{
"type":"portmap",
"capabilities":{"portMappings":true}
}
]
}

參考官網的Network-plugins[2]

也就是如果使用了hostPort, 是由portmap這個cni提供portMapping能力,同時,如果想使用這個能力,在配置檔案中一定需要開啟portmap,這個在作者的叢集中也開啟了,這點對應上了

另外一個比較重要的結論是:The CNI 'portmap' plugin, used to setup HostPorts for CNI, inserts rules at the front of the iptables nat chains; which take precedence over the KUBE- SERVICES chain. Because of this, the HostPort/portmap rule could match incoming traffic even if there were better fitting, more specific service definition rules like NodePorts later in the chain

參考: https://ubuntu.com/security/CVE-2019-9946

翻譯過來就是使用hostPort後,會在iptables的nat鏈中插入相應的規則,而且這些規則是在KUBE-SERVICES規則之前插入的,也就是說會優先匹配hostPort的規則,我們常用的NodePort規則其實是在KUBE-SERVICES之中,也排在其後

從portmap的原始碼中果然是可以看到相應的程式碼

感興趣的可以的plugins[3]專案的meta/portmap/portmap.go中檢視完整的原始碼

所以,最終是呼叫portmap寫入的這些規則.

端口占用

進一步實驗發現,hostport可以通過iptables命令檢視到, 但是無法在ipvsadm中檢視到

使用lsof/netstat也檢視不到這個埠,這是因為hostport是通過iptables對請求中的目的埠進行轉發的,並不是在主機上通過埠監聽

既然lsof跟netstat都查不到埠資訊,那這個埠相當於沒有處於listen狀態?

如果這時再部署一個hostport指定相同埠的應用會怎麼樣呢?

結論是: 使用hostPort的應用在排程時無法排程在已經使用過相同hostPort的主機上,也就是說,在排程時會考慮hostport

如果強行讓其排程在同一臺機器上,那麼就會出現以下錯誤,如果不刪除的話,這樣的錯誤會越來越多,嚇的作者趕緊刪了.

如果這個時候建立一個nodePort型別的svc, 埠也為31123,結果會怎麼樣呢?

apiVersion:apps/v1
kind:Deployment
metadata:
name:nginx-nodeport2
labels:
k8s-app:nginx-nodeport2
spec:
replicas:1
selector:
matchLabels:
k8s-app:nginx-nodeport2
template:
metadata:
labels:
k8s-app:nginx-nodeport2
spec:
nodeName:spring-38
containers:
-name:nginx
image:nginx:latest
ports:
-containerPort:80
---
apiVersion:v1
kind:Service
metadata:
name:nginx-nodeport2
spec:
type:NodePort
ports:
-port:80
targetPort:80
nodePort:31123
selector:
k8s-app:nginx-nodeport2

可以發現,NodePort是可以成功建立的,同時監聽的埠也出現了.

從這也可以說明使用hostposrt指定的埠並沒有listen主機的埠,要不然這裡就會提示埠重複之類

那麼問題又來了,同一臺機器上同時存在有hostPort跟nodePort的埠,這個時候如果curl 31123時, 訪問的是哪一個呢?

經多次使用curl請求後,均是使用了hostport那個nginx pod收到請求

原因還是因為KUBE-NODE-PORT規則在KUBE-SERVICE的鏈中是處於最後位置,而hostPort通過portmap寫入的規則排在其之前

因此會先匹配到hostport的規則,自然請求就被轉到hostport所在的pod中,這兩者的順序是沒辦法改變的,因此無論是hostport的應用釋出在前還是在後都無法影響請求轉發

另外再提一下,hostport的規則在ipvsadm中是查詢不到的,而nodePort的規則則是可以使用ipvsadm查詢得到

問題解決

要想把這些規則刪除,可以直接將hostport去掉,那麼規則就會隨著刪除,比如下圖中去掉了一個nginx的hostport

另外使用較多的port-forward也是可以進行埠轉發的,它又是個什麼情況呢? 它其實使用的是socat及netenter工具,網上看到一篇文章,原理寫的挺好的,感興趣的可以看一看

參考: https://vflong.github.io/sre/k8s/2020/03/15/how-the-kubectl-port-forward-command-works.html

生產建議

一句話,生產環境除非是必要且無他法,不然一定不要使用hostport,除了會影響排程結果之外,還會出現上述問題,可能造成的後果是非常嚴重的。