cni
# 1.集群网络
# 1.1.简介
kebernetes定义了统一的网络模型,基于扁平网络结构的设计,无需将主机端口与网络端口映射或组件转发即可实现高效通讯。这种扁平化的网络模型设计,可以使应用程序很容易由虚机或物理机迁移到集群pod。--- 模型定义 1.podIP唯一,集群内可达 2.pod内容器共享IP及mac地址,容器间可以相互通信(localhost) 3.podIP集群可达,无需NAT 4.网络隔离可以通过策略实现 --- 特性 1.pod地址唯一,同节点pod共享网络和存储 2.pod内container共享同一网络命名空间,相互之间可通过localhost通信,container拥有独立文件系统、CPU、内存和进程空间 3.pod运行在节点上,pod网络命名空间连接到节点命名空间,以打通网络1
2
3
4
5
6
7
8
9
10节点执行
lsns -t net查看当前主机的网络命名空间,可以看到/pause网络空间命令,对应sandbox容器进程。
pod内container共享pause网络空间,container又有独立的进程空间,执行lsns -t pid查看节点进程空间。
执行
ip netns identify PID查看进程所属网络空间,可以看到coredns关联的就是/pause命令对应网络空间。
执行
ip netns exec netns ip a可以进入网络空间,查看podIP绑定接口。
coredns的10.244.0.142地址绑定到eth0接口,eth0链接到主机3号接口上,执行ip link | grep -A1 ^3查看节点网络接口。
veth6dc93995是主机根命名空间的虚拟以太接口(vitual ethernet device),连接pod和node网络隧道,对端是pod命名空间的eth0。
veth6dc93995会链接到主机2号接口,本质上就是cni0网桥,网桥连接多个网络,请求到达时询问所有连接接口,无法处理时跨节点路由。
注意
pod流量出入会经历eth0——veth——cni0——bond
# 1.2.pod内流量
相同
pod内的容器通信最简单,这些容器共享网络命名空间,网络命名空间拥有lo回环接口,可以通过localhost完成通信。
注意
网络命名空间有
lo和eth0网络接口
# 1.3.node内流量
相同节点内的
pod通信时,podA请求根据容器内路由到达eth0接口,通过与eth0相连的隧道veth0到达节点根网络空间。veth0通过网桥cni0与其它vethx相连,cni0将请求转到匹配请求IP的veth1及记录,数据经veth1隧道到达对端pod。
注意
node内流量转发依赖veth虚拟以太接口,不同pod关联的vethx接入cni0网桥,通过匹配目标IP路由转发
# 1.4.cluster内流量
跨节点的
pod通信相对复杂,不同的cni实现处理方式不同。本质上,请求到达cni0后,cni0广播vethx虚拟以太接口没有回应,进入主机路由寻址阶段,根据cni agent维护的路由表(podcidr-->node)将请求发送到node。
注意
不同
cni针对跨节点流量处理策略不同,总体来说都是根据podcidr路由
# 2.cni
# 2.1.简介
cni(container network interface,容器网络接口)是由coreos提出的一个容器网络规范,作为CNCF旗下的项目之一,用于多云集群提供规范的容器网络接口标准协议,以便灵活配置云上容器的网络。本质上,CNI为容器提供网络配置的抽象接口,具体的容器网络配置实现由CNI插件完成,允许插件基于overlay/underlay/route实现容器网络功能。
规范
1.
cni的网络定义以json的格式存储2.
netcfg基于STDIN的方式传递给CNI插件,其他的参数通过环境变量传递3.
cni插件以可执行文件的方式实现4.
cni插件负责连接容器,本质上是network namespace和veth
# 2.2.配置规范
cni规范定义了网络配置的格式,网络配置位于/etc/cni/net.d目录,用于声明cni实现使用的插件,供运行时按序执行进行网络管理。/* flannel */ { "name": "cbr0", "cniVersion": "0.3.1", "plugins": [ { "type": "flannel", "delegate": { "hairpinMode": true, "isDefaultGateway": true } }, { "type": "portmap", "capabilities": { "portMappings": true } } ] }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20注意
1.
cni实现的网络配置位于/etc/cni/net.d目录,用于声明cni工作涉及plugin2.
cni plugin区分为接口插件和链式插件,前者用于网络接口创建,后者用于网络接口调整
# 2.3.插件委托
cni定义了第三种插件(IPAM),专为容器接口IP地址管理设计,以弥补松散的链式插件功能缺失问题。cni插件可以在恰当的时机调用IPAM插件,委托IPAM根据指定的协议(dhcp/staic/host)、本地文件数据或网络配置文件的ipam声明完成地址分配、网关设置、路由修改功能。"ipam": { "type": "host-local", "subnet": "10.1.0.0/16", "gateway": "10.1.0.1", "routes": [ {"dst": "0.0.0.0/0"} ] }1
2
3
4
5
6
7
8注意
cni实现略有差别,flannel网络配置将核心功能委托至flannel plugin,根据/run/flannel/subnet.env渲染真正使用插件
# 2.4.接口定义
cni基于libcni定义接口规范,以集成应用程序实现容器网络配置的生命周期管理,屏蔽不同厂商网络配置实现细节。type CNI interface { AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error) DelNetworkList(net *NetworkConfigList, rt *RuntimeConf) error ... AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) DelNetwork(net *NetworkConfig, rt *RuntimeConf) error } func (c *CNIConfig) addNetwork(...) (types.Result, error) { ... // 调用cni可执行文件 return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec) }1
2
3
4
5
6
7
8
9
10
11
12
13
14注意
1.
CNI的本质是调用插件可执行文件,根据可执行文件运行结果持久化容器网络配置2.
ADD失败的cni行为必须配合DEL,cni插件调用提供gc选项,可根据配置执行无效网络回收
# 2.5.执行流程
cni将容器网络配置ADD/DEL/CHECK作为附加(attachment),根据多个插件顺序协作共同完成。ADD行为会执行interface--->chained,DEL行为会执行chained-->interface,先执行的输出会作为next plugin输入,最终输出作为容器运行时的结果。--- interface插件 1.brige: 宿主机上创建网桥,通过`veth pair`的方式连接到容器 2.macvlan: 虚拟多个macvtap,每个macvtap拥有不同的mac地址 3.ipvlan: 类似macvlan,通过一个主机接口虚拟出多个虚拟网络接口,虚拟网络接口共享mac地址,ip地址不同 4.loopback: lo回环(将回环接口设置为up) 5.ptp: veth pair设备 6.vlan: 分配vlan设备 7.host-device: 移动宿主上已经存在的设备加入容器 --- IPAM插件 1.dhcp: 宿主机上允许的守护进程,代表容器发出DHCP请求 2.host-local: 基于提前分配的子网划分IP 3.static: 用于为容器分配静态地址 --- meta插件 1.flannel: 转为flannel项目提供的插件(v0.9.x版本后移除,合并到kube-flannel) 2.tuning: 通过sysctl调整网络设备参数的二进制文件 3.portmap: 通过iptables配置端口映射的二进制文件 4.bandwidth: 基于Token Bucket Filter(TBF)进行限流的二进制文件 5.firewall: 基于iptables或firewall添加规则控制容器的进出流量1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
注意
容器运行时会根据
plugin list顺序执行ADD,确保先建网络接口再分配IP及调整路由,DEL则相反,确保回收资源后删除网络接口
# 3.bridge插件
# 3.1.网络创建
bridge是较为常见的网桥插件,负责配置桥接器及veth pair设备,同时调用ipam分配IP设置到容器网卡及进行路由、地址伪装配置。func cmdAdd(args *skel.CmdArgs) error { ... // 加载入参配置 n, cniVersion, err := loadNetConf(args.StdinData, args.Args) ... // 创建桥接网卡cni0 br, brInterface, err := setupBridge(n) ... // 获取sandbox网络命名空间 netns, err := ns.GetNS(args.Netns) ... defer netns.Close() // 创建veth pare,host端连接cni0 hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan, n.vlans, n.PreserveDefaultVlan, n.mac) ... // mac地址防伪造检查 if n.MacSpoofChk { ... // 设置host veth防护 sc.Setup() defer func() { if !success { // 操作失败,回滚 sc.Teardown() ... } }() } // L3网络,IPAM逻辑 if isLayer3 { // 执行ipam分配IP r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) ... // release IP in case of failure defer func() { if !success { // 回滚,回收IP ipam.ExecDel(n.IPAM.Type, args.StdinData) } }() ... // 计算的网关 gwsV4, gwsV6, err := calcGateways(result, n) ... // 配置容器网络栈 netns.Do(func(_ ns.NetNS) error { // ipv6 dad配置(重复地址检查) if n.EnableDad { _, _ = sysctl.Sysctl(fmt.Sprintf("/net/ipv6/conf/%s/enhanced_dad", args.IfName), "1") _, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/accept_dad", args.IfName), "1") // ipv4 arp notify } else { _, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/accept_dad", args.IfName), "0") } // ipv4 arp notify _, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv4/conf/%s/arp_notify", args.IfName), "1") // 设置接口IP return ipam.ConfigureIface(args.IfName, result) }) ... // 配置bridge/vlan网关 if n.IsGW { ... // Set the IP address(es) on the bridge and enable forwarding for _, gws := range []*gwInfo{gwsV4, gwsV6} { for _, gw := range gws.gws { // vlan子接口作为三层出口 if n.Vlan != 0 { // 创建vlan子接口 vlanIface, err := ensureVlanInterface(br, n.Vlan, n.PreserveDefaultVlan) ... // vxlan接口配置网关IP err = ensureAddr(vlanIface, gws.family, &gw, n.ForceAddress) ... // bridge接口作为三层出口 } else { // 没有vlan接口,配置bridge网关IP ensureAddr(br, gws.family, &gw, n.ForceAddress) ... } } // 开启IP转发 if gws.gws != nil { enableIPForward(gws.family) ... } } } // 配置sandbox粒度地址伪装 if n.IPMasq { chain := utils.FormatChainName(n.Name, args.ContainerID) comment := utils.FormatComment(n.Name, args.ContainerID) // 创建cni-xxx chain及配置rules for _, ipc := range result.IPs { ip.SetupIPMasq(&ipc.Address, chain, comment) ... } } // L2网络(不分配IP) } else if !n.DisableContainerInterface { netns.Do(func(_ ns.NetNS) error { // 获取容器veth接口 link, err := netlink.LinkByName(args.IfName) ... // 激活veth接口 netlink.LinkSetUp(link) ... return nil }) ... } // 获取宿主机对端veth接口 hostVeth, err := netlink.LinkByName(hostInterface.Name) ... // 未禁用容器接口 if !n.DisableContainerInterface { // 检查宿主机veth是否桥接及激活 retries := []int{0, 50, 500, 1000, 1000} for idx, sleep := range retries { time.Sleep(time.Duration(sleep) * time.Millisecond) // 重新获取对端 hostVeth, err = netlink.LinkByName(hostInterface.Name) ... // 已激活,结束检测 if hostVeth.Attrs().OperState == netlink.OperUp { break } ... } } // 更新CNI结果的对端veth设备MAC(桥接/设置IP后MAC可能重分配) hostInterface.Mac = hostVeth.Attrs().HardwareAddr.String() // 重新获取桥接设备 br, err = bridgeByName(n.BrName) ... // 更新CNI结果桥接设备MAC(veth接入/配置IP可能导致MAC重分配) brInterface.Mac = br.Attrs().HardwareAddr.String() ... // 更新DNS配置(IPAM配置/CNI插件配置指定) if dnsConfSet(n.DNS) { result.DNS = n.DNS } ... return types.PrintResult(result, cniVersion) }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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154注意
bridge支持配置的地址伪装仅涉及本节点子网,未考虑集群容器子网,因此CNI实现一般都要实现地址伪装,避免跨节点流量异常
# 3.2.网络删除
cmdDel()用于回收分配给容器网络命名空间的网络资源,根据原则先清理网卡再回收IP,避免网络资源泄漏。func cmdDel(args *skel.CmdArgs) error { // 加载入参配置 n, _, err := loadNetConf(args.StdinData, args.Args) ... // 地址回收 ipamDel := func() error { if isLayer3 { ipam.ExecDel(n.IPAM.Type, args.StdinData) ... } return nil } // 无容器网络命名空间,仅回收IP if args.Netns == "" { return ipamDel() } ... // 进入容器网络命名空间 err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { ... // 清理容器网络接口 ipnets, err = ip.DelLinkByNameAddr(args.IfName) ... return err }) if err != nil { // 接口不存在 _, ok := err.(ns.NSPathNotExistErr) if ok { // 回收IP return ipamDel() } return err } // 接口清理成功,回收IP ipamDel() ... // mac地址防伪规则回收 if n.MacSpoofChk { sc := link.NewSpoofChecker("", "", uniqueID(args.ContainerID, args.IfName)) sc.Teardown() ... } // masq规则回收 if isLayer3 && n.IPMasq { chain := utils.FormatChainName(n.Name, args.ContainerID) comment := utils.FormatComment(n.Name, args.ContainerID) for _, ipn := range ipnets { ip.TeardownIPMasq(ipn, chain, comment) ... } } return err }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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60注意
网络创建阶段先建网络接口再分
IP,网络删除阶段还是先删网络接口再回收IP
# 4.loopback插件
# 4.1.回环激活
loopback plugin是libcni默认(initCriService阶段)加载的插件,之后才会读取/etc/cni/net.d的第一个.configList获取cni声明插件,两者结合用于后续网络创建。func cmdAdd(args *skel.CmdArgs) error { // 解析lo网络配置 conf, err := parseNetConf(args.StdinData) ... // 回环网卡名称固定lo args.IfName = "lo" // 进入容器网络命名空间 err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { // 找到lo接口 link, err := netlink.LinkByName(args.IfName) ... // lo接口设置为up err = netlink.LinkSetUp(link) ... // 获取回环网卡IPV4地址列表 v4Addrs, err := netlink.AddrList(link, netlink.FAMILY_V4) ... // 检查回环地址 if len(v4Addrs) != 0 { v4Addr = v4Addrs[0].IPNet // sanity check that this is a loopback address for _, addr := range v4Addrs { if !addr.IP.IsLoopback() { return fmt.Errorf("loopback interface found with non-loopback address %q", addr.IP) } } } // 获取回环网卡IPV6地址列表 v6Addrs, err := netlink.AddrList(link, netlink.FAMILY_V6) ... // 检查回环地址 if len(v6Addrs) != 0 { v6Addr = v6Addrs[0].IPNet // sanity check that this is a loopback address for _, addr := range v6Addrs { if !addr.IP.IsLoopback() { return fmt.Errorf("loopback interface found with non-loopback address %q", addr.IP) } } } return nil }) ... // result处理 if conf.PrevResult != nil { // 上一个插件结果,透传不改动 result = conf.PrevResult } else { // 构造新的cni result r := ¤t.Result{ CNIVersion: conf.CNIVersion, Interfaces: []*current.Interface{ { Name: args.IfName, Mac: "00:00:00:00:00:00", // lo没有真实mac Sandbox: args.Netns, }, }, } // lo接口IPV4地址 if v4Addr != nil { r.IPs = append(r.IPs, ¤t.IPConfig{ Interface: current.Int(0), Address: *v4Addr, }) } // lo接口IPV6地址 if v6Addr != nil { r.IPs = append(r.IPs, ¤t.IPConfig{ Interface: current.Int(0), Address: *v6Addr, }) } result = r } return types.PrintResult(result, conf.CNIVersion) }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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83注意
网络命名空间创建默认初始化一个未激活的
lo接口,loopback插件仅负责激活及检查接口地址
# 4.2.回环回收
cmdDel()本质上进入容器网络命名空间,恢复lo接口为down状态,lo接口跟随网络命名空间销毁进而释放。func cmdDel(args *skel.CmdArgs) error { ... // 接口名固定lo args.IfName = "lo" // ignore config, this only works for loopback err := ns.WithNetNSPath(args.Netns, func(ns.NetNS) error { // 获取lo接口 link, err := netlink.LinkByName(args.IfName) ... // 设置为down状态 err = netlink.LinkSetDown(link) ... return nil }) ... return nil }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16注意
由于
lo接口是伴随网络命名空间自动出现的,只是状态为down,因此loopback插件删除行为仅撤销状态修改,不会主动删除lo接口
# 4.host-local插件
# 4.1.地址分配
host-local是常见的IPAM插件,用于向地址范围内分配IPV4和IPV6地址,分配状态会存储在/var/lib/cni/networks/{networkName},确保主机上IP地址唯一性。分配器支持多个地址范围及范围内不相交的子网集合,分配策略会向各范围集合采用松散循环方式申请地址。func cmdAdd(args *skel.CmdArgs) error { // 加载IPAM配置 ipamConf, confVersion, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) ... // 加载dns配置 if ipamConf.ResolvConf != "" { dns, err := parseResolvConf(ipamConf.ResolvConf) ... result.DNS = *dns } // 打开磁盘存储,以持久化已分配IP store, err := disk.New(ipamConf.Name, ipamConf.DataDir) ... defer store.Close() ... // 梳理请求IP for _, ip := range ipamConf.IPArgs { requestedIPs[ip.String()] = ip } // 梳理允许的地址范围 for idx, rangeset := range ipamConf.Ranges { // 当前range创建allocator allocator := allocator.NewIPAllocator(&rangeset, store, idx) ... // 找到属于当前range的申请IP for k, ip := range requestedIPs { if rangeset.Contains(ip) { requestedIP = ip delete(requestedIPs, k) break } } // 尝试申请IP(指定的/自动分配的) ipConf, err := allocator.Get(args.ContainerID, args.IfName, requestedIP) if err != nil { // 分配失败,回收之前range已分配IP for _, alloc := range allocs { _ = alloc.Release(args.ContainerID, args.IfName) } return fmt.Errorf("failed to allocate for range %d: %v", idx, err) } // 记录已分配的range allocator allocs = append(allocs, allocator) // 记录分配的IP结果 result.IPs = append(result.IPs, ipConf) } // 请求IP未全部满足,回退已分配的allocator if len(requestedIPs) != 0 { for _, alloc := range allocs { _ = alloc.Release(args.ContainerID, args.IfName) } ... return fmt.Errorf(errstr) } // 保存路由 result.Routes = ipamConf.Routes return types.PrintResult(result, confVersion) }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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67注意
host-local地址分配遵循循环复用策略,已分配地址会持久化至IP文件及last_reserved_ip.x(记录上一次成功)地址,每次由上一次分配地址作为起点循环range subnets申请地址直到成功,走完一圈未成功说明地址溢出
# 4.2.地址回收
host-local地址回收相对于分配简单很多,根据containerID+ifname组成的匹配项找到所属IP文件,释放即可。func cmdDel(args *skel.CmdArgs) error { // 加载配置 ipamConf, _, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) ... // 持久化文件对象 store, err := disk.New(ipamConf.Name, ipamConf.DataDir) ... defer store.Close() ... // 根据range子网初始化分配器 for idx, rangeset := range ipamConf.Ranges { ipAllocator := allocator.NewIPAllocator(&rangeset, store, idx) // 释放IP ipAllocator.Release(args.ContainerID, args.IfName) ... } ... return nil }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21注意
host-local释放地址仅删除IP文件,无需重置last_reserved_ip.x记录的上一次分配IP,会延迟循环复用
# 5.portMap插件
# 5.1.端口映射
portmap是常用的meta插件,用于容器网络接口就绪后设置端口映射,确保访问容器映射端口流量可正则到达容器服务。func cmdAdd(args *skel.CmdArgs) error { // 加载portmap配置 netConf, _, err := parseConfig(args.StdinData, args.IfName) ... // 记录containerID netConf.ContainerID = args.ContainerID // IPV4逻辑 if netConf.ContIPv4.IP != nil { // 设置iptables规则,映射主机端口与容器端口 forwardPorts(netConf, netConf.ContIPv4) ... // 清理旧的udp conntrack连接跟踪记录,避免udp流量"黑洞"问题 deletePortmapStaleConnections(netConf.RuntimeConfig.PortMaps, unix.AF_INET) ... } // IPV6逻辑 if netConf.ContIPv6.IP != nil { // 设置iptables规则进行端口映射 forwardPorts(netConf, netConf.ContIPv6) ... // 清理旧的udp conntrack连接跟踪记录,避免残留映射导致流量错误丢弃 deletePortmapStaleConnections(netConf.RuntimeConfig.PortMaps, unix.AF_INET6) ... } // Pass through the previous result return types.PrintResult(netConf.PrevResult, netConf.CNIVersion) } // 端口映射 func forwardPorts(config *PortMapConf, containerNet net.IPNet) error { ... // 初始化ipt工具 ipt, err = iptables.NewWithProtocol(iptables.ProtocolXX) ... // SNAT的2种清空: // 1.外部访问宿主机端口DNAT到pod // 2.pod访问宿主机端口DNAT到pod if *config.SNAT { // 外部标记链为空 if config.ExternalSetMarkChain == nil { // 生成mark chain setMarkChain := genSetMarkChain(*config.MarkMasqBit) // chain配置到iptables setMarkChain.setup(ipt) ... // 生成masq chain masqChain := genMarkMasqChain(*config.MarkMasqBit) // masq chain配置到iptables masqChain.setup(ipt) ... } // IPVV处理 if !isV6 { // 设置route_localnet=1,允许127.0.0.0/8的流量被转发,确保主机应用可访问容器 hostIfName := getRoutableHostIF(containerNet.IP) if hostIfName != "" { enableLocalnetRouting(hostIfName) ... } } } // 生成顶层DNAT链 toplevelDnatChain := genToplevelDnatChain() // dnat chain配置到iptables toplevelDnatChain.setup(ipt) ... // 生成容器专用DNAT chain dnatChain := genDnatChain(config.Name, config.ContainerID) // 填充DNAT规则 fillDnatRules(&dnatChain, config, containerNet) // DNAT chain配置到iptables dnatChain.setup(ipt) ... return nil } // 清理旧的udp contrack跟踪连接记录 func deletePortmapStaleConnections(portMappings []PortMapEntry, family netlink.InetFamily) error { for _, pm := range portMappings { ... // 清理宿主机hostPort相关contrack表记录 utils.DeleteConntrackEntriesForDstPort(uint16(pm.HostPort), utils.PROTOCOL_UDP, family) ... } return nil }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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95注意
portmap进行的所谓端口映射本质上还是iptables地址伪装,只是生效范围仅针对podcidr:hostPort和127.0.0.1:hostPort
# 5.2.端口回收
cmdDel()负责进行映射端口回收,主要回收iptables chain及rules,清理contrack条目,避免造成流量异常。func cmdDel(args *skel.CmdArgs) error { netConf, _, err := parseConfig(args.StdinData, args.IfName) ... // 未进行端口映射 if len(netConf.RuntimeConfig.PortMaps) == 0 { return nil } netConf.ContainerID = args.ContainerID // 执行映射规则卸载 return unforwardPorts(netConf) } // 回收端口映射相关的iptables规则 func unforwardPorts(config *PortMapConf) error { // dnat chain(HOSTPORT-DNAT和CNI-DN-XXX) dnatChain := genDnatChain(config.Name, config.ContainerID) // snat chain(HOSTPORT-SNAT和CNI-SN-XXX) oldSnatChain := genOldSnatChain(config.Name, config.ContainerID) ... // ipv4规则回收 if ip4t != nil { ... // dnat chain回收 dnatChain.teardown(ip4t) ... // snat chain回收 oldSnatChain.teardown(ip4t) } // ipv6规则回收 if ip6t != nil { // dnat chain回收 dnatChain.teardown(ip6t) ... // snat chain回收 oldSnatChain.teardown(ip6t) } return nil } // 幂等删除dnat chain func (c *chain) teardown(ipt *iptables.IPTables) error { // CNI-DNAT-XXX chain存在 exists, err := ipt.ChainExists(c.table, c.name) ... // 清理引用链规则(未定义) for _, entryChain := range c.entryChains { for _, rule := range c.entryRules { r := []string{} r = append(r, rule...) r = append(r, "-j", c.name) ipt.Delete(c.table, entryChain, r...) } } // 尝试清理CNI-DNAT-XXX chain(由于被父链引用,删除会失败) ipt.ClearAndDeleteChain(c.table, c.name) ... // 上述未成功,触发第二阶段 // 遍历引用chain(HOSTPORT-DNAT) for _, entryChain := range c.entryChains { // 获取引用链规则 entryChainRules, err := ipt.List(c.table, entryChain) ... // 跳过-A chainName描述 for _, entryChainRule := range entryChainRules[1:] { // 存在跳转CNI-DNAT-XXX chain的规则 if strings.HasSuffix(entryChainRule, "-j "+c.name) { // 规则解析 chainParts, err := shellwords.Parse(entryChainRule) ... // 去掉-A chainName,仅保留规则 chainParts = chainParts[2:] // List results always include an -A CHAINNAME // 清理entryChain rule utils.DeleteRule(ipt, c.table, entryChain, chainParts...) ... } } } // 再次尝试删除CNI-DNAT-XXX chain return ipt.ClearAndDeleteChain(c.table, c.name) }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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90注意
portmap端口回收本质上删除CNI-DN-XXX容器专用链及HOSTPORT-DNAT链对容器专用链的引用规则