K8s罪魁祸⾸之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, 部署在两个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
翻译过来就是使⽤hostPort后,会在iptables的nat链中插⼊相应的规则,⽽且这些规则是在KUBE-SERVICES规则之前插⼊的,也就是说会优先匹配hostPort的规则,我们常⽤的NodePort规则其实是在KUBE-SERVICES之中,也排在其后
从portmap的源码中果然是可以看到相应的代码
感兴趣的可以的plugins[3]项⽬的meta/中查看完整的源码
所以,最终是调⽤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
nodeselectortargetPort: 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⼯具,⽹上看到⼀篇⽂章,原理写的挺好的,感兴趣的可以看⼀看
⽣产建议
⼀句话,⽣产环境除⾮是必要且⽆他法,不然⼀定不要使⽤hostport,除了会影响调度结果之外,还会出现上述问题,可能造成的后果是⾮常严重的。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论