zoukankan      html  css  js  c++  java
  • 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,除了会影响调度结果之外,还会出现上述问题,可能造成的后果是非常严重的。

  • 相关阅读:
    字符编码
    visual studio 2015 安装记录和问题修复
    TCP状态转换图的理解
    静态库与动态库的编译链接
    运行库glibc
    堆栈的简单认识
    Makefile学习总结
    关于STM32单片机的IAP实现
    ubuntu12.0.4安装启动后无法进入图形操作界面
    观察者模式
  • 原文地址:https://www.cnblogs.com/cheyunhua/p/15167594.html
Copyright © 2011-2022 走看看