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


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

問題背景

集群環境為K8s v1.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上發現有以下的規則在其它節點上沒有

-A CNI-DN-xxxx -p tcp -m tcp --dport 3306 -j DNAT --to-destination 10.224.0.222:3306
-A CNI-HOSTPORT-DNAT -m comment --comment "dnat name": \"cni0\" id: \"xxxxxxxxxxxxx\"" -j CNI-DN-xxx
-A CNI-HOSTPORT-SNAT -m comment --comment "snat name": \"cni0\" id: \"xxxxxxxxxxxxx\"" -j CNI-SN-xxx
-A CNI-SN-xxx -s 127.0.0.1/32 -d 10.224.0.222/32 -p tcp -m tcp --dport 80 -j MASQUERADE

其中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 supports hostPort. You can use the official portmap[1] plugin offered by the CNI plugin team or use your own plugin with portMapping functionality.

If you want to enable hostPort support, you must specify portMappings capability in your cni-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,除了會影響調度結果之外,還會出現上述問題,可能造成的后果是非常嚴重的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM