edgemesh 利用 iptables 劫持流量时遇到的问题分析
概念
- 云边通信:云端容器,通过 serviceName/clusterIP,访问边缘容器;
- 边云通信:相对上面而言,反过来,边缘容器,通过 serviceName/clusterIP,访问云端容器;
- 边边通信:边缘主机上的容器,通过 serviceName/clusterIP,访问该边缘主机或其他边缘主机的容器;
edgemesh 架构
kubeedge 相比 kubernetes,主要的提供了跨子网构建集群的方案;其最强大的特点就是让 kubernetes 不在局限于一个物理机房之内;集群中的主机节点可以在不同的私有局域网内。
不同私有网络之下的主机要进行相互通信,最主要的就是受限于防火墙 NAT 形态:
- 全锥形 NAT
- 受限锥形 NAT
- 端口受限锥型 NAT
- 对称型 NAT
上面的各组网方案之中,依次安全性增强,在对称锥形的网络中,所有主机都在各自的防火墙背后,直接穿透访问就非常困难,所以直接 edgemesh 提供了两种穿透访问方案:
部署
edgemesh-server: Deployment 云端
edgemesh-agent: DaemonSet 云端+边缘
直接穿透访问
对于全锥形 NAT、受限性 NAT、部分端口受限锥形 NAT 访问可以直接采用如下方案;

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

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 抓包

mkedge2 上接收载荷 tcp 抓包

中继穿透请求端抓包
环境:
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 抓包

mkmaster1 进行中继 tcp 抓包

mkworker3 上接收载荷 tcp 抓包

失败访问情况分析
先说结论,怀疑主机默认内核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 端口,之后流量就不见了踪影;
- 在反覆启停这些容器,经过多次测试,的确无法进行正常通信;
- 怀疑是 iptables 的问题,因丢失了 DOCKER 链之后,重启 docker 服务也无法创建规则链;
- 重启 edgemesh-agent 服务,发现其容器无法被清除,一直是 Running 状态,如果 docker rm -f 强制停止,会导致 edgemesh-agent 在宿主机上留下僵尸进程;

初始排查阶段
究竟是 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

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

不死心,程序在 mkedge2 上依然如上;
不死心,找了台内核升级为kernel: 4.19.12-1.el7.elrepo.x86_64 的主机,结果是正确的;

再次排查阶段
不死心:虽然严重怀疑了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 的。

相关资料
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