edgemesh 利用 iptables 劫持流量时遇到的问题分析

概念

  1. 云边通信:云端容器,通过 serviceName/clusterIP,访问边缘容器;
  2. 边云通信:相对上面而言,反过来,边缘容器,通过 serviceName/clusterIP,访问云端容器;
  3. 边边通信:边缘主机上的容器,通过 serviceName/clusterIP,访问该边缘主机或其他边缘主机的容器;

edgemesh 架构

kubeedge 相比 kubernetes,主要的提供了跨子网构建集群的方案;其最强大的特点就是让 kubernetes 不在局限于一个物理机房之内;集群中的主机节点可以在不同的私有局域网内。

不同私有网络之下的主机要进行相互通信,最主要的就是受限于防火墙 NAT 形态:

  • 全锥形 NAT
  • 受限锥形 NAT
  • 端口受限锥型 NAT
  • 对称型 NAT

上面的各组网方案之中,依次安全性增强,在对称锥形的网络中,所有主机都在各自的防火墙背后,直接穿透访问就非常困难,所以直接 edgemesh 提供了两种穿透访问方案:

部署

edgemesh-server: Deployment 云端

edgemesh-agent: DaemonSet 云端+边缘

直接穿透访问

对于全锥形 NAT、受限性 NAT、部分端口受限锥形 NAT 访问可以直接采用如下方案;

arc01.png

中继穿透访问

对于直接穿透访问不了的请求,edgemesh-agent 就会将流量转发到 edgemesh-server 上进行中继,因为所有的 edgemesh-agent 在启动后,就会于云端的 edgemesh-server 建立一个隧道网络;


arc02.png

edgemesh 原理

在 kubernetes+kubeedge 组成的边缘云集群上,无论云端 or 边缘主机,都部署了 edgemesh-agent,其有两个主要功能:

  • 对外暴露 53 端口,实现了通过 serviceName 查询 k8s:service 配置获取相应 clusterIP 的功能;
  • 在主机上创建一个 DummyDevice 网卡,默认暴露在 169.254.96.16:40001 上一个端口,并且通过 iptables 配置了 PREROUTING 和 OUTPUT 的 EDGE-MESH 链,用于劫持所有的流量访问。
  • 在如下的 iptables 的 NAT 表设置,可以看出在 EDGE-MESH 链中配置的相关服务目标地址:即不需要 EDGEMESH 代理的服务就会 RETURN 到它的下一行去做代理,即走了 DOCKER->其他的代理服务(kube-proxy、kube-router...),这些服务的声明是要在 k8s 创建 servcie 的时候,增加 noproxy=edgemesh 的标签,就会被 edgemesh-agent 写入到主机 iptables 的这个位置。这些服务其实就是不需要云边通信的服务;
  • 当需要被 edgemesh-agent 代理的流量,即我们认为的需要云边通信、边云通信、边边通信,劫持到流量,流量会被内核劫持转发到 169.254.96.16:40001 上,然后经过 edgemesh-agent 的 20006 端口与远端的 edgemesh-agent:20006 或 edgemesh-server:20004 端口进行转发,最终目标端的 edgemesh 再将流量转发到被访问的业务容器中;
​
[root@edge001 ~]# iptables -t nat -nvL --line
Chain PREROUTING (policy ACCEPT 9734 packets, 833K bytes)
numpkts bytes targetprot opt inoutsourcedestination
132317 3700K EDGE-MESH all-- **0.0.0.0/00.0.0.0/0/* edgemesh root chain */
214436 930K DOCKERall-- **0.0.0.0/00.0.0.0/0ADDRTYPE match dst-type LOCAL
​
Chain INPUT (policy ACCEPT 9734 packets, 833K bytes)
numpkts bytes targetprot opt inoutsourcedestination
​
Chain OUTPUT (policy ACCEPT 278 packets, 18244 bytes)
numpkts bytes targetprot opt inoutsourcedestination
1893 60171 EDGE-MESH all-- **0.0.0.0/00.0.0.0/0/* edgemesh root chain */
2473390 DOCKERall-- **0.0.0.0/0!127.0.0.0/8ADDRTYPE match dst-type LOCAL
​
Chain POSTROUTING (policy ACCEPT 278 packets, 18244 bytes)
numpkts bytes targetprot opt inoutsourcedestination
100 MASQUERADE all-- *!docker0172.17.0.0/160.0.0.0/0
200 RETURNall-- **192.168.122.0/24224.0.0.0/24
300 RETURNall-- **192.168.122.0/24255.255.255.255
400 MASQUERADE tcp-- **192.168.122.0/24!192.168.122.0/24masq ports: 1024-65535
500 MASQUERADE udp-- **192.168.122.0/24!192.168.122.0/24masq ports: 1024-65535
600 MASQUERADE all-- **192.168.122.0/24!192.168.122.0/24
​
Chain DOCKER (2 references)
numpkts bytes targetprot opt inoutsourcedestination
1271896 RETURNall-- docker0 *0.0.0.0/00.0.0.0/0
​
Chain EDGE-MESH (2 references)
numpkts bytes targetprot opt inoutsourcedestination
100 RETURNall-- **0.0.0.0/010.254.165.225/* ignore kubeedge/cloudcore service */
200 RETURNall-- **0.0.0.0/010.242.247.106/* ignore kube-system/traefik-web-ui service */
300 RETURNall-- **0.0.0.0/010.243.116.144/* ignore kube-system/traefik-ingress-service service */
400 RETURNall-- **0.0.0.0/010.255.163.200/* ignore kube-system/prometheus-service service */
500 RETURNall-- **0.0.0.0/010.240.83.24/* ignore kube-system/metrics-server service */
600 RETURNall-- **0.0.0.0/010.254.210.250/* ignore kube-system/kube-dns service */
700 RETURNall-- **0.0.0.0/010.240.0.1/* ignore default/kubernetes service */
800 EDGE-MESH-TCP tcp-- **0.0.0.0/010.240.0.0/12/* tcp service proxy */
​
Chain EDGE-MESH-TCP (1 references)
numpkts bytes targetprot opt inoutsourcedestination
100 DNATtcp-- **0.0.0.0/00.0.0.0/0to:169.254.96.16:40001

正常访问情况分析

直接穿透请求端抓包

环境:

mkedge3 10.136.77.2 北四环办公室边缘盒子 edgemesh-agent role: edgenode kernel: 5.2.14-1.el7.elrepo.x86_64

mkedge2 10.201.82.131 天坛机房 edgemesh-agent role: edgenode kernel: 4.15.6-1.el7.elrepo.x86_64

用例:

在 mkedge2 上启动 tcp-echo-cloud 容器,通过 mkedge3 上的 busybox 容器进行远程 telnet tcp-echo-cloud 2701

说明:

从实际流量抓包看来看,从办公室机房能够直接访问到天坛机房主机,不需要中继;

抓包详情

mkedge3 发起请求 tcp 抓包

1.mkedge3-send.png

mkedge2 上接收载荷 tcp 抓包

2.mkedge2-recv.png

中继穿透请求端抓包

环境:

mkmaster1 10.200.50.118 国贸机房 edgemesh-server, edgemesh-agent role: master kernel: 5.2.14-1.el7.elrepo.x86_64

mkworker3 10.202.42.112 亦庄机房 edgemesh-agent role: cloudnode kernel: 5.2.14-1.el7.elrepo.x86_64

mkedge3 10.136.77.2 北四环办公室边缘盒子 edgemesh-agent role: edgenode kernel: 3.10.0-514.el7.x86_64

用例:

在 mkworker3 上启动 tcp-echo-cloud 容器,通过 mkedge3 上的 busybox 容器进行远程 telnet tcp-echo-cloud 2701

说明:

从实际流量抓包看来看,从办公室机房不能直接访问到亦庄机房主机,需要国贸机房的 edgemesh-server 中继;

抓包详情

mkedge3 发起请求 tcp 抓包

1.mkedge3-send.png

mkmaster1 进行中继 tcp 抓包

2.mkmaster1-relay.png

mkworker3 上接收载荷 tcp 抓包

3.mkworker3-recv.png

失败访问情况分析

先说结论,怀疑主机默认内核4.15.6-1.el7.elrepo.x86_64(centos7) 存在调用系统内核函数 getsockopt 的严重 bug!!!

失败穿透请求抓包

环境:

mkmaster1 10.200.50.118 国贸机房 edgemesh-server, edgemesh-agent role: master kernel: 5.2.14-1.el7.elrepo.x86_64

mkworker1 10.201.82.139 天坛机房 edgemesh-agent role: cloudnode kernel: 5.2.14-1.el7.elrepo.x86_64

mkworker2 10.201.83.74 天坛机房 edgemesh-agent role: cloudnode kernel: 5.2.14-1.el7.elrepo.x86_64

mkedge1 10.201.82.148 天坛机房 edgemesh-agent role: edgenode kernel: 4.15.6-1.el7.elrepo.x86_64

mkedge2 10.201.82.131 天坛机房 edgemesh-agent role: edgenode kernel: 4.15.6-1.el7.elrepo.x86_64

mkedge3 10.136.77.2 北四环办公室边缘盒子 edgemesh-agent role: edgenode kernel: 3.10.0-514.el7.x86_64

用例:

在 mkworker1、mkworker2、mkedge1、mkedge2 上启动同时启动 tcp-echo-cloud 容器,通过 mkedge1 上的 busybox 容器进行远程访问这些服务,发现一个都不通;

现状:

从实际流量抓包看来看,从业务容器发送请求给到 edgemesh-agent:40001 端口,之后流量就不见了踪影;

  1. 在反覆启停这些容器,经过多次测试,的确无法进行正常通信;
  2. 怀疑是 iptables 的问题,因丢失了 DOCKER 链之后,重启 docker 服务也无法创建规则链;
  3. 重启 edgemesh-agent 服务,发现其容器无法被清除,一直是 Running 状态,如果 docker rm -f 强制停止,会导致 edgemesh-agent 在宿主机上留下僵尸进程;
3.png

初始排查阶段

究竟是 Go 的版本不对?容器镜像问题?逐个进行了排查:

给 edgemesh-agent:proxy.go 的 Run()函数中,realServerAddress(&conn)调用之前,增加了打印日志,同时在这个函数调用中,增加了一些日志输出标记位,重新制作容器进行测试,发现输出日志555,但未输出666,同时进程状态异常变为了D

func (proxy *EdgeProxy) Run() {
 // ensure ipatbles
 proxy.Proxier.Start()
​
 // start tcp proxyfor {
  conn, err := proxy.TCPProxy.Listener.Accept()
  if err != nil {
   klog.Warningf("get tcp conn error: %v", err)
   continue
  }
  klog.Info("!!! has workload !!!")
  ip, port, err := realServerAddress(&conn)
...
}
}
​
​
​
// realServerAddress returns an intercepted connection's original destination.
func realServerAddress(conn *net.Conn) (string, int, error) {
 tcpConn, ok := (*conn).(*net.TCPConn)
 if !ok {
  return "", -1, fmt.Errorf("not a TCPConn")
 }
 klog.Info("111")
 file, err := tcpConn.File()
 if err != nil {
  return "", -1, err
 }
 defer file.Close()
 klog.Info("222")
 // To avoid potential problems from making the socket non-blocking.
 tcpConn.Close()
 *conn, err = net.FileConn(file)
 if err != nil {
  return "", -1, err
 }
 klog.Info("333")
 fd := file.Fd()
 klog.Info("444")
 var addr sockAddr
 size := uint32(unsafe.Sizeof(addr))
 err = getSockOpt(int(fd), syscall.SOL_IP, SoOriginalDst, uintptr(unsafe.Pointer(&addr)), &size)
 if err != nil {
  return "", -1, err
 }
 klog.Info("777")
 var ip net.IP
 switch addr.family {
 case syscall.AF_INET:
  ip = addr.data[2:6]
 default:
  return "", -1, fmt.Errorf("unrecognized address family")
 }
 klog.Info("888")
 port := int(addr.data[0])<<8 + int(addr.data[1])
 if err := syscall.SetNonblock(int(fd), true); err != nil {
  return "", -1, nil
 }
​
 return ip.String(), port, nil
}
​
func getSockOpt(s int, level int, name int, val uintptr, vallen *uint32) (err error) {
 klog.Info("555")
 _, _, e1 := syscall.Syscall6(syscall.SYS_GETSOCKOPT, uintptr(s), uintptr(level), uintptr(name), uintptr(val), uintptr(unsafe.Pointer(vallen)), 0)
 if e1 != 0 {
  err = e1
 }
 klog.Info("666")
 return
}
​

然后就开始怀疑是这个内核调用出现了问题,先编写了一个 go 程序:https://gitee.com/Hu-Lyndon/gogetsockopt.git

如下是在 mkedge3 宿主机上,kernel: 3.10.0-514.el7.x86_64,一个终端上先将程序启动监听 18011 端口,然后在另外一个连接 mkedge3 终端上,直接 telnet 这个端口,程序能够正确通信,并输出打印日志address: 192.168.127.1:18011

succ-mkedge3.png

如下是在 mkedge1 上测试,kernel: 4.15.6-1.el7.elrepo.x86_64, 发现 telnet 之后,无法输出打印日志,并且程序在第三个终端窗口里也无法kill -9,并且进程状态变为了D

failed-mkedge1.png

不死心,程序在 mkedge2 上依然如上;

不死心,找了台内核升级为kernel: 4.19.12-1.el7.elrepo.x86_64 的主机,结果是正确的;

succ-kn4.19.12-1.png

再次排查阶段

不死心:虽然严重怀疑了kernel: 4.15.6-1.el7.elrepo.x86_64,那么就近是 Go 的问题,还是系统内核的问题呢?于是又写了一个 C 程序来测试:https://gitee.com/Hu-Lyndon/unix-network-programming,在 compile-on-kernel4.15.6 分支的/sockopt/sockoptchk.c

依然出现了上面出现的问题,说明应该就是 kernel 的问题了,需要推荐运维以后不要安装这个版本的内核了,目前看 4.19.12 以上的版本或者干脆就是 3.10 的内核都是打过 patch 的。

failed-mkedge1.png

相关资料

https://go.dev/play/p/GMAaKucHOr

https://elixir.bootlin.com/linux/latest/C/ident/sys_getsockopt

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/socket.c

https://bugzilla.kernel.org/show_bug.cgi?id=198791

http://patchwork.ozlabs.org/project/netfilter-devel/patch/a4752d0887579496c5db267d9db7ff77719436d8.1518088560.git.pabeni@redhat.com/

https://github.com/cybozu-go/transocks/pull/14

文章为作者独立观点,不代表BOSS直聘立场。未经账号授权,禁止随意转载。