Z.S.K.'s Records

Kubernetes学习(hostPort劫持了我的请求)

最近排查了一个kubernetes中使用了hostport后遇到比较坑的问题,奇怪的知识又增加了.

问题背景

集群环境为K8s v1.15.9,cni指定了flannel-vxlan跟portmap, kube-proxy使用mode为ipvs,集群3台master,同时也是node,这里以node-1,node-2,node-3来表示。

集群中有2个mysql, 部署在两个ns下,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上发现有以下的规则在其它节点上没有

1
2
3
4
-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文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "k8s-pod-network",
"cniVersion": "0.3.0",
"plugins": [
{
# ...其它的plugin
}
{
"type": "portmap",
"capabilities": {"portMappings": true}
}
]
}

参考: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/

也就是如果使用了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项目的meta/portmap/portmap.go中查看完整的源码

所以,最终是调用portmap写入的这些规则.

端口占用

进一步实验发现,hostport可以通过iptables命令查看到, 但是无法在ipvsadm中查看到

使用lsof/netstat也查看不到这个端口,这是因为hostport是通过iptables对请求中的目的端口进行转发的,并不是在主机上通过端口监听

既然lsof跟netstat都查不到端口信息,那这个端口相当于没有处于listen状态?

如果这时再部署一个hostport提定相同端口的应用会怎么样呢?

结论是: 使用hostPort的应用在调度时无法调度在已经使用过相同hostPort的主机上,也就是说,在调度时会考虑hostport

如果强行让其调度在同一台机器上,那么就会出现以下错误,如果不删除的话,这样的错误会越来越多,吓的作者赶紧删了.

如果这个时候创建一个nodePort类型的svc, 端口也为31123,结果会怎么样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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,除了会影响调度结果之外,还会出现上述问题,可能造成的后果是非常严重的

参考文章:

转载请注明出处https://izsk.me

Z.S.K. wechat
Scan Me To Read on Phone
I know you won't do this,but what if you did?