写点什么

容器网络实现 (下):为容器插上”网线“

  • 2024-06-14
    福建
  • 本文字数:10464 字

    阅读完需:约 34 分钟

推荐阅读以下文章对 docker 基本实现有一个大致认识:



开发环境如下:

root@mydocker:~# lsb_release -aNo LSB modules are available.Distributor ID:	UbuntuDescription:	Ubuntu 20.04.2 LTSRelease:	20.04Codename:	focalroot@mydocker:~# uname -r5.4.0-74-generic
复制代码


注意:需要使用 root 用户


1. 概述


前面两篇文章中已经实现了容器的基本功能,容器之间可以互相访问,同时容器可以访问外网,外部设备也可以通过宿主机端口访问到容器内部服务。


不过还缺少了资源清理逻辑,本篇主要实现在删除网络或者容器时对相关网络资源做一个清理工作


2. 网络清理


创建网络会做以下事情:

  • 创建 bridge 设备

  • 对应子网配置路由规则

  • 对应子网配置 iptables 规则(SNAT)


那么删除时也需要做对应的清理

  • 删除 iptables 规则

  • 删除路由规则

  • 删除 bridge 设备

// Delete 删除网络func (d *BridgeNetworkDriver) Delete(network *Network) error {	// 清除路由规则	err := deleteIPRoute(network.Name, network.IPRange.IP.String())	if err != nil {		return errors.WithMessagef(err, "clean route rule failed after bridge [%s] deleted", network.Name)	}	// 清除 iptables 规则	err = deleteIPTables(network.Name, network.IPRange)	if err != nil {		return errors.WithMessagef(err, "clean snat iptables rule failed after bridge [%s] deleted", network.Name)	}	// 删除网桥	err = d.deleteBridge(network)	if err != nil {		return errors.WithMessagef(err, "delete bridge [%s] failed", network.Name)	}	return nil}
复制代码


路由规则清理


使用 netlink.RouteDel 方法删除路由,具体如下:


// 删除路由,ip addr del xxx命令func deleteIPRoute(name string, rawIP string) error { retries := 2 var iface netlink.Link var err error for i := 0; i < retries; i++ { // 通过LinkByName方法找到需要设置的网络接口 iface, err = netlink.LinkByName(name) if err == nil { break } log.Debugf("error retrieving new bridge netlink link [ %s ]... retrying", name) time.Sleep(2 * time.Second) } if err != nil { return errors.Wrap(err, "abandoning retrieving the new bridge link from netlink, Run [ ip link ] to troubleshoot") } // 查询对应设备的路由并全部删除 list, err := netlink.RouteList(iface, netlink.FAMILY_V4) if err != nil { return err } for _, route := range list { if route.Dst.String() == rawIP { // 根据子网进行匹配 err = netlink.RouteDel(&route) if err != nil { log.Errorf("route [%v] del failed,detail:%v", route, err) continue } } } return nil}
复制代码


SNAT 规则清理


iptables 删除比较简单,就是把添加命令中的 -A 改成 -D即可,相比于按行号删除,这种方式不会删错。

对 configIPTables 方法 进行调整,把 action 作为配置,这样添加删除可以复用同一个方法。

func setupIPTables(bridgeName string, subnet *net.IPNet) error {	return configIPTables(bridgeName, subnet, false)}func deleteIPTables(bridgeName string, subnet *net.IPNet) error {	return configIPTables(bridgeName, subnet, true)}
func configIPTables(bridgeName string, subnet *net.IPNet, isDelete bool) error { action := "-A" if isDelete { action = "-D" } // 拼接命令 iptablesCmd := fmt.Sprintf("-t nat %s POSTROUTING -s %s ! -o %s -j MASQUERADE", action, subnet.String(), bridgeName) cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...) log.Infof("删除 SNAT cmd:%v", cmd.String()) // 执行该命令 output, err := cmd.Output() if err != nil { log.Errorf("iptables Output, %v", output) } return err}
复制代码


网桥设备清理


使用netlink.LinkDel 方法删除 bridge 设备。

// deleteBridge deletes the bridgefunc (d *BridgeNetworkDriver) deleteBridge(n *Network) error {	bridgeName := n.Name
// get the link l, err := netlink.LinkByName(bridgeName) if err != nil { return fmt.Errorf("getting link with name %s failed: %v", bridgeName, err) }
// delete the link if err = netlink.LinkDel(l); err != nil { return fmt.Errorf("failed to remove bridge interface %s delete: %v", bridgeName, err) }
return nil}
复制代码


3. 记录容器网络信息


容器启动时需要记录网络相关信息,便于后续查询或者删除时使用。


RecordContainerInfo


ContainerInfo 中新增网络相关字段

type Info struct {	NetworkName string   `json:"networkName"` // 容器所在的网络	IP          string   `json:"ip"`          // 容器IP	PortMapping []string `json:"portmapping"` // 端口映射}
复制代码


记录容器信息逻辑后移到绑定网络后,便于记录 IP 信息。

func Run(tty bool, comArray, envSlice []string, res *subsystems.ResourceConfig, volume, containerName, imageName string,	net string, portMapping []string) {	containerId := container.GenerateContainerID() // 生成 10 位容器 id
// 省略...
var containerIP string // 如果指定了网络信息则进行配置 if net != "" { // config container network containerInfo := &container.Info{ Id: containerId, Pid: strconv.Itoa(parent.Process.Pid), Name: containerName, PortMapping: portMapping, } ip, err := network.Connect(net, containerInfo) if err != nil { log.Errorf("Error Connect Network %v", err) return } containerIP = ip.String() }
// 在分配 IP 后在记录,便于存储网络相关信息 containerInfo, err := container.RecordContainerInfo(parent.Process.Pid, comArray, containerName, containerId, volume, net, containerIP, portMapping) if err != nil { log.Errorf("Record container info error %v", err) return }}
复制代码


mydockerps


mydocker ps 命令增加 ip 信息展示

func ListContainers() {	// 读取存放容器信息目录下的所有文件	files, err := os.ReadDir(container.InfoLoc)	if err != nil {		log.Errorf("read dir %s error %v", container.InfoLoc, err)		return	}	containers := make([]*container.Info, 0, len(files))	for _, file := range files {		tmpContainer, err := getContainerInfo(file)		if err != nil {			log.Errorf("get container info error %v", err)			continue		}		containers = append(containers, tmpContainer)	}	// 使用tabwriter.NewWriter在控制台打印出容器信息	// tabwriter 是引用的text/tabwriter类库,用于在控制台打印对齐的表格	w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)	_, err = fmt.Fprint(w, "ID\tNAME\tPID\tIP\tSTATUS\tCOMMAND\tCREATED\n")	if err != nil {		log.Errorf("Fprint error %v", err)	}	for _, item := range containers {		_, err = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",			item.Id,			item.Name,			item.IP,			item.Pid,			item.Status,			item.Command,			item.CreatedTime)		if err != nil {			log.Errorf("Fprint error %v", err)		}	}	if err = w.Flush(); err != nil {		log.Errorf("Flush error %v", err)	}}
复制代码


4. 容器网络设备清理


容器加入某个网络时需要做以下工作:

  • 创建 veth-pair 设备对,并将一段绑定到 bridge 设备上

  • 容器中添加路由,将 bridge 作为网关

  • 指定端口映射时添加 iptables 规则(DNAT)


在容器退出后,需要做网络信息清理。

  • iptables 规则清理

  • veth 设备从 birdge 设备解绑

  • veth pair 设备清理


都是根据启动时存储的信息,调用 network.Disconnect 方法清理即可。

// Disconnect 将容器中指定网络中移除func Disconnect(networkName string, info *container.Info) error {	networks, err := loadNetwork()	if err != nil {		return errors.WithMessage(err, "load network from file failed")	}	// 从networks字典中取到容器连接的网络的信息,networks字典中保存了当前己经创建的网络	network, ok := networks[networkName]	if !ok {		return fmt.Errorf("no Such Network: %s", networkName)	}	// veth 从 bridge 解绑并删除 veth-pair 设备对	drivers[network.Driver].Disconnect(fmt.Sprintf("%s-%s", info.Id, networkName))
// 清理端口映射添加的 iptables 规则 ep := &Endpoint{ ID: fmt.Sprintf("%s-%s", info.Id, networkName), IPAddress: net.ParseIP(info.IP), Network: network, PortMapping: info.PortMapping, } return deletePortMapping(ep)}
复制代码


Veth 设备清理


将 veth 设备从 bridge 解绑,并根据命名规则找到另一端的 veth 一起删除。

func (d *BridgeNetworkDriver) Disconnect(endpointID string) error {	// 根据名字找到对应的 Veth 设备	vethNme := endpointID[:5] // 由于 Linux 接口名的限制,取 endpointID 的前 5 位	veth, err := netlink.LinkByName(vethNme)	if err != nil {		return err	}	// 从网桥解绑	err = netlink.LinkSetNoMaster(veth)	if err != nil {		return errors.WithMessagef(err, "find veth [%s] failed", vethNme)	}	// 删除 veth-pair	// 一端为 xxx,另一端为 cif-xxx	err = netlink.LinkDel(veth)	if err != nil {		return errors.WithMessagef(err, "delete veth [%s] failed", vethNme)	}	veth2Name := "cif-" + vethNme	veth2, err := netlink.LinkByName(veth2Name)	if err != nil {		return errors.WithMessagef(err, "find veth [%s] failed", veth2Name)	}	err = netlink.LinkDel(veth2)	if err != nil {		return errors.WithMessagef(err, "delete veth [%s] failed", veth2Name)	}
return nil}
复制代码


DNAT 规则删除


iptables 清理和 前面 SNAT 清理类似,只需要控制 action 即可,把添加的 -A 替换为-D 即可。

func addPortMapping(ep *Endpoint) error {	return configPortMapping(ep, false)}
func deletePortMapping(ep *Endpoint) error { return configPortMapping(ep, true)}
// configPortMapping 配置端口映射func configPortMapping(ep *Endpoint, isDelete bool) error { action := "-A" if isDelete { action = "-D" }
var err error // 遍历容器端口映射列表 for _, pm := range ep.PortMapping { // 分割成宿主机的端口和容器的端口 portMapping := strings.Split(pm, ":") if len(portMapping) != 2 { logrus.Errorf("port mapping format error, %v", pm) continue } // 由于iptables没有Go语言版本的实现,所以采用exec.Command的方式直接调用命令配置 // 在iptables的PREROUTING中添加DNAT规则 // 将宿主机的端口请求转发到容器的地址和端口上 // iptables -t nat -A PREROUTING ! -i testbridge -p tcp -m tcp --dport 8080 -j DNAT --to-destination 10.0.0.4:80 iptablesCmd := fmt.Sprintf("-t nat %s PREROUTING ! -i %s -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s", action, ep.Network.Name, portMapping[0], ep.IPAddress.String(), portMapping[1]) cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...) logrus.Infoln("配置端口映射 DNAT cmd:", cmd.String()) // 执行iptables命令,添加端口映射转发规则 output, err := cmd.Output() if err != nil { logrus.Errorf("iptables Output, %v", output) continue } } return err}
复制代码


具体实现


根据前台容器和后台容器,需要在不同地方调用前面的清理逻辑。


前台容器


前台容器直接在 Run 方法中清理即可。

func Run(tty bool, comArray, envSlice []string, res *subsystems.ResourceConfig, volume, containerName, imageName string,	net string, portMapping []string) {	containerId := container.GenerateContainerID() // 生成 10 位容器 id
// 省略... if tty { // 如果是tty,那么父进程等待,就是前台运行,否则就是跳过,实现后台运行 _ = parent.Wait() container.DeleteWorkSpace(containerId, volume) container.DeleteContainerInfo(containerId) if net != "" { // 如果指定了网络则在退出时做清理工作 network.Disconnect(net, containerInfo) } }}
复制代码


后台容器删除


后台容器则在 mydocker stop 命令中做清理。

func removeContainer(containerId string, force bool) {	containerInfo, err := getInfoByContainerId(containerId)	if err != nil {		log.Errorf("Get container %s info error %v", containerId, err)		return	}
switch containerInfo.Status { case container.STOP: // STOP 状态容器直接删除即可 // 先删除配置目录,再删除rootfs 目录 if err = container.DeleteContainerInfo(containerId); err != nil { log.Errorf("Remove container [%s]'s config failed, detail: %v", containerId, err) return } container.DeleteWorkSpace(containerId, containerInfo.Volume) if containerInfo.NetworkName != "" { // 清理网络资源 if err = network.Disconnect(containerInfo.NetworkName, containerInfo); err != nil { log.Errorf("Remove container [%s]'s config failed, detail: %v", containerId, err) return } } case container.RUNNING: // RUNNING 状态容器如果指定了 force 则先 stop 然后再删除 if !force { log.Errorf("Couldn't remove running container [%s], Stop the container before attempting removal or"+ " force remove", containerId) return } log.Infof("force delete running container [%s]", containerId) stopContainer(containerId) removeContainer(containerId, force) default: log.Errorf("Couldn't remove container,invalid status %s", containerInfo.Status) return }}
复制代码


5. 测试


测试以下几部分:

  • 网络资源清理测试

  • 容器信息记录测试

  • 容器网络资源清理测试


网络资源清理测试


测试网络删除后,对应的 bridge、路由、iptables 等资源是否清理干净。

root@mydocker:~/feat-network-2/mydocker# go build .root@mydocker:~/feat-network-2/mydocker# ./mydocker network create --driver bridge --subnet 10.10.10.1/24 testbrroot@mydocker:~/feat-network-3/mydocker# ./mydocker network listNAME         IpRange        Drivertestbr1      10.10.0.1/24   bridge
复制代码


查看对应设备

# brigde 设备root@mydocker:~/feat-network-3/mydocker# ip link show type bridge15: testbr: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/ether 82:80:36:11:30:2f brd ff:ff:ff:ff:ff:ff# 路由root@mydocker:~/feat-network-3/mydocker# ip r10.10.10.0/24 dev testbr proto kernel scope link src 10.10.10.1# iptablesroot@mydocker:~/feat-network-3/mydocker# iptables -t nat -L POSTROUTINGChain POSTROUTING (policy ACCEPT)target     prot opt source               destinationMASQUERADE  all  --  10.10.0.0/24         anywhere
复制代码


接下来进行删除

root@mydocker:~/feat-network-3/mydocker# ./mydocker network remove testbr
复制代码


分别检查路由、iptables、bridge 设备是否真的删除的。

# 网桥设备root@mydocker:~/feat-network-3/mydocker# ip link show type bridge# 路由规则root@mydocker:~/feat-network-3/mydocker# ip r# iptablesroot@mydocker:~/feat-network-3/mydocker# iptables -t nat -L POSTROUTINGChain POSTROUTING (policy ACCEPT)target     prot opt source               destination
复制代码


可以看到都删除了,说明网络资源清理是正常的。


容器信息记录测试


测试容器信息是否能够正常记录和展示。


创建一个网络

root@mydocker:~/feat-network-2/mydocker# ./mydocker network create --driver bridge --subnet 10.10.10.1/24 testbrroot@mydocker:~/feat-network-3/mydocker# ./mydocker network listNAME         IpRange        Drivertestbr1      10.10.0.1/24   bridge
复制代码


然后启动容器指定使用该网络

./mydocker run -it -net testbr busybox sh
复制代码


另一个终端查看容器信息

root@mydocker:~/feat-network-3/mydocker# ./mydocker psID           NAME         PID          IP          STATUS      COMMAND     CREATED3994300986   3994300986   10.10.10.2   103669      running     sh          2024-03-07 13:24:34
复制代码


退出前台容器

/ # exit
复制代码


再次查看容器信息

root@mydocker:~/feat-network-3/mydocker# ./mydocker psID          NAME        PID         IP          STATUS      COMMAND     CREATED
复制代码


删除了,一切正常。


容器网络资源清理测试


前台容器

./mydocker run -it -p 8080:80 -net testbr busybox sh
复制代码


查看容器中的网络设备

/ # ip a1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00    inet 127.0.0.1/8 scope host lo       valid_lft forever preferred_lft forever    inet6 ::1/128 scope host       valid_lft forever preferred_lft forever19: cif-09561@if20: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000    link/ether a6:a3:16:22:c1:98 brd ff:ff:ff:ff:ff:ff    inet 10.10.10.3/24 brd 10.10.10.255 scope global cif-09561       valid_lft forever preferred_lft forever    inet6 fe80::a4a3:16ff:fe22:c198/64 scope link       valid_lft forever preferred_lft forever
复制代码


可以看到,其中 19 号设备叫做cif-09561@if20,这就是我们放入容器中的 veth 设备。


查看宿主机

root@mydocker:~/feat-network-3/mydocker# ip a1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00    inet 127.0.0.1/8 scope host lo       valid_lft forever preferred_lft forever    inet6 ::1/128 scope host       valid_lft forever preferred_lft forever2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000    link/ether fa:16:3e:58:62:ef brd ff:ff:ff:ff:ff:ff    inet 192.168.10.144/24 brd 192.168.10.255 scope global dynamic ens3       valid_lft 61063sec preferred_lft 61063sec    inet6 fe80::f816:3eff:fe58:62ef/64 scope link       valid_lft forever preferred_lft forever16: testbr: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000    link/ether 66:9e:1c:28:c2:40 brd ff:ff:ff:ff:ff:ff    inet 10.10.10.1/24 brd 10.10.10.255 scope global testbr       valid_lft forever preferred_lft forever    inet6 fe80::74fa:43ff:feac:74f3/64 scope link       valid_lft forever preferred_lft forever20: 09561@if19: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master testbr state UP group default qlen 1000    link/ether 66:9e:1c:28:c2:40 brd ff:ff:ff:ff:ff:ff link-netnsid 0    inet6 fe80::649e:1cff:fe28:c240/64 scope link       valid_lft forever preferred_lft forever
复制代码


testbr 为网桥,09561@if19 这个就是容器中 veth 的另一端。


查看 iptables 规则

root@mydocker:~/feat-network-3/mydocker# iptables -t nat -L PREROUTINGChain PREROUTING (policy ACCEPT)target     prot opt source               destinationDNAT       tcp  --  anywhere             anywhere             tcp dpt:http-alt to:10.10.10.4:80
复制代码


确实有一个 DNAT 规则用于处理端口映射。


测试停止容器后,这两个 veth 设备是否会删除。


退出容器

/ # exit
复制代码


查看宿主机

root@mydocker:~/feat-network-3/mydocker# ip a1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00    inet 127.0.0.1/8 scope host lo       valid_lft forever preferred_lft forever    inet6 ::1/128 scope host       valid_lft forever preferred_lft forever2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000    link/ether fa:16:3e:58:62:ef brd ff:ff:ff:ff:ff:ff    inet 192.168.10.144/24 brd 192.168.10.255 scope global dynamic ens3       valid_lft 60895sec preferred_lft 60895sec    inet6 fe80::f816:3eff:fe58:62ef/64 scope link       valid_lft forever preferred_lft forever16: testbr: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff    inet 10.10.10.1/24 brd 10.10.10.255 scope global testbr       valid_lft forever preferred_lft forever    inet6 fe80::74fa:43ff:feac:74f3/64 scope link       valid_lft forever preferred_lft forever
复制代码


09561@if19 被删除了。

root@mydocker:~/feat-network-3/mydocker# iptables -t nat -L PREROUTINGChain PREROUTING (policy ACCEPT)target     prot opt source               destination
复制代码


DNAT 规则也清理了。


说明前台容器资源清理一切正常。


后台容器


./mydocker run -d -p 8080:80 -net testbr busybox top
复制代码


查看运行情况

root@mydocker:~/feat-network-3/mydocker# ./mydocker psID           NAME         PID          IP          STATUS      COMMAND     CREATED1891821312   1891821312   10.10.10.5   103828      running     top         2024-03-07 13:32:19
复制代码


同样的,查看 veth 设备和 DNAT 规则

root@mydocker:~/feat-network-3/mydocker# ip a26: 18918@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master testbr state UP group default qlen 1000    link/ether ba:ab:aa:cb:21:70 brd ff:ff:ff:ff:ff:ff link-netnsid 0    inet6 fe80::b8ab:aaff:fecb:2170/64 scope link       valid_lft forever preferred_lft foreverroot@mydocker:~/feat-network-3/mydocker# iptables -t nat -L PREROUTINGChain PREROUTING (policy ACCEPT)target     prot opt source               destinationDNAT       tcp  --  anywhere             anywhere             tcp dpt:http-alt to:10.10.10.5:80
复制代码


删除容器

root@mydocker:~/feat-network-3/mydocker# ./mydocker rm 1891821312 -f
复制代码


查看 veth 设备和 DNAT 规则是否被清理

root@mydocker:~/feat-network-3/mydocker# ip a1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00    inet 127.0.0.1/8 scope host lo       valid_lft forever preferred_lft forever    inet6 ::1/128 scope host       valid_lft forever preferred_lft forever2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000    link/ether fa:16:3e:58:62:ef brd ff:ff:ff:ff:ff:ff    inet 192.168.10.144/24 brd 192.168.10.255 scope global dynamic ens3       valid_lft 60557sec preferred_lft 60557sec    inet6 fe80::f816:3eff:fe58:62ef/64 scope link       valid_lft forever preferred_lft forever16: testbr: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff    inet 10.10.10.1/24 brd 10.10.10.255 scope global testbr       valid_lft forever preferred_lft forever    inet6 fe80::74fa:43ff:feac:74f3/64 scope link       valid_lft forever preferred_lft foreverroot@mydocker:~/feat-network-3/mydocker# iptables -t nat -L PREROUTINGChain PREROUTING (policy ACCEPT)target     prot opt source               destination
复制代码


都清理了,说明后台容器清理也是正常的。


文章转载自:探索云原生

原文链接:https://www.cnblogs.com/KubeExplorer/p/18245717

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
容器网络实现(下):为容器插上”网线“_Docker_不在线第一只蜗牛_InfoQ写作社区