从零开始写 Docker(十六)---容器网络实现(上):为容器插上”网线”

mydocker-network-1.png

本文为从零开始写 Docker 系列第十六篇,利用 linux 下的 Veth、Bridge、iptables 等等相关技术,构建容器网络模型,为容器插上”网线“。


完整代码见:https://github.com/lixd/mydocker
欢迎 Star

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

  • 核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
  • 基于 namespace 的视图隔离:探索 Linux Namespace:Docker 隔离的神奇背后
  • 基于 cgroups 的资源限制
    • 初探 Linux Cgroups:资源控制的奇妙世界
    • 深入剖析 Linux Cgroups 子系统:资源精细管理
    • Docker 与 Linux Cgroups:资源隔离的魔法之旅
  • 基于 overlayfs 的文件系统:Docker 魔法解密:探索 UnionFS 与 OverlayFS
  • 基于 veth pair、bridge、iptables 等等技术的 Docker 网络:揭秘 Docker 网络:手动实现 Docker 桥接网络

开发环境如下:

root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 20.04.2 LTS
Release:	20.04
Codename:	focal
root@mydocker:~# uname -r
5.4.0-74-generic

注意:需要使用 root 用户

1. 概述

前面文章中已经实现了容器的大部分功能,不过还缺少了网络部分。现在我们的容器既不能访问外网也不能访问其他容器。

本篇和下一篇文章则会解决该问题,会实现容器网络相关功能,为我们的容器插上”网线“。

本篇主要介绍大致思路以及 IPAM 和 Network Driver 组件的实现过程。

2. 网络模型

Docker 桥接网络

相关内容在这边文章:Docker教程(十)—揭秘 Docker 网络:手动实现 Docker 桥接网络 中已经有了详细记录,感兴趣的可以跳转阅读。

核心如下:

  • 首先容器就是一个进程,主要利用 Linux Namespace 进行隔离。
  • 那么,为了跨 Namespace 通信,就用到了 Veth pair。
  • 然后多个容器都使用 Veth pair 互相连通的话,不好管理,所以加入了 Linux Bridge,所有 veth 一端在容器中,一端直接和 bridge 连接,这样就好管理多了。
  • 最后容器和外部网络要进行通信,于是又要用到 iptables 的 NAT 规则进行地址转换。

接下来我们要做的就是使用 Go 代码实现这些功能。

设备抽象

首先,将 Bridge 和 Veth 这两个对象进行抽象:网络(Network)网络端点(Endpoint)

Network

网络(Netowrk)中可以有多个容器,在同一个网络里的容器可以通过这个网络互相通信。

就像挂载到同一个 Linux Bridge 设备上的网络设备一样, 可以直接通过 Bridge 设备实现网络互连;连接到同一个网络中的容器也可以通过这个网络和网络中别的容器互连。

网络中会包括这个网络相关的配置,比如网络的容器地址段、网络操作所调用的网络驱动等信息。

type Network struct {
    Name    string     // 网络名
    IPRange *net.IPNet // 地址段
    Driver  string     // 网络驱动名
}
Endpoint

网络端点(Endpoint)是用于连接容器与网络的,保证容器内部与网络的通信。

将 Linux 中的 veth-pair 一端挂载到容器内部,另一端挂载到 Bridge 上,就能打通容器和宿主机网络的通信。

网络端点中会包括连接到网络的一些信息,比如地址、Veth 设备、端口映射、连接的容器和网络等信息。

type Endpoint struct {
    ID          string           `json:"id"`
    Device      netlink.Veth     `json:"dev"`
    IPAddress   net.IP           `json:"ip"`
    MacAddress  net.HardwareAddr `json:"mac"`
    Network     *Network
    PortMapping []string
}

Network Driver

**网络驱动(Network Driver) **是一个网络功能中的组件,不同的驱动对网络的创建、连接、销毁的策略不同,通过在创建网络时指定不同的网络驱动来定义使用哪个驱动做网络的配置。

它的接口定义如下:

type Driver interface {
	Name() string
	Create(subnet string, name string) (*Network, error)
	Delete(name string) error
	Connect(network *Network, endpoint *Endpoint) error
	Disconnect(network Network, endpoint *Endpoint) error
}

IPAM

**IPAM(IP Address Management) **也是网络功能中的一个组件,用于网络 IP 地址的分配和释放,包括容器的IP地址和网络网关的IP地址,它的主要功能如下。

type IPAMer interface {
	Allocate(subnet *net.IPNet) (ip net.IP, err error) // 从指定的 subnet 网段中分配 IP 地址
	Release(subnet *net.IPNet, ipaddr *net.IP) error   //  从指定的 subnet 网段中释放掉指定的 IP 地址。
}

实现思路

为了给我们的容器插上网线,大致需要做以下工作:

  • 实现 IPAM ,完成对 IP 地址的管理
  • 实现 NetworkDriver,实现对网络的管理
  • 基于 IPAM 和 NetworkDriver 实现 mydocker network create/list/delete 命令, 让我们能通过 mydocker 命令实现对容器网络的管理
  • 实现 mydocker run -net,让容器可以加入指定网络

3. IPAM 实现

IPAM 主要管理 IP 的分配以及释放,因此需要找个地方存储哪些 IP 分配了,哪些 IP 可用。

由于对每个 IP 来说只存在已分配、可用两种状态,因此容器想到使用 bitmap 来存储。

bitmap

bitmap 在大规模连续且少状态的数据处理中有很高的效率,比如要用到的 IP 地址分配。

一个网段中的某个 IP 地址有两种状态:

  • 1 表示已经被分配了,
  • 0表示还未被分配;

那么一个 IP 地址的状态就可以用一位来表示, 并且通过这位相对基础位的偏移也能够迅速定位到数据所在的位。

通过位图的方式实现 IP 地址的管理也比较简单:

  • 分配 IP:在获取 IP 地址时,遍历每一项,找到值为 0 的项的偏移,然后通过偏移和网段的配置计算出分配的 IP 地址,并将该位置元素置为 1,表明 IP 地址已经被分配。
  • 释放 IP:根据 IP 和网段配置计算出偏移,然后将该位置元素置为 0,表示该 IP 地址可用。

数据结构定义

const ipamDefaultAllocatorPath = "/var/lib/mydocker/network/ipam/subnet.json"

type IPAM struct {
    SubnetAllocatorPath string             // 分配文件存放位置
    Subnets             *map[string]string // 网段和位图算法的数组 map, key 是网段, value 是分配的位图数组
}

// 初始化一个IPAM的对象,默认使用/var/lib/mydocker/network/ipam/subnet.json作为分配信息存储位置
var ipAllocator = &IPAM{
    SubnetAllocatorPath: ipamDefaultAllocatorPath,
}

整个定义比较简单,整个 IPAM 对象包括一个 SubnetAllocatorPath 字段用于存放数据的持久化位置,一个 Subnets 字段记录每一个网段中 IP 的分配情况。

注意:在这个定义中,为了代码实现简单和易于阅读,使用一个字符表示一个状态位,实际上可以采用一位表示一个是否分配的状态位,这样资源会有更低的消耗。

配置信息持久化

通过将分配信息序列化成 json 文件或将 json 文件以反序列化的方式保存和读取网段分配的信息到内存。

读取文件数据到内存:

// load 加载网段地址分配信息
func (ipam *IPAM) load() error {
	// 检查存储文件状态,如果不存在,则说明之前没有分配,则不需要加载
	if _, err := os.Stat(ipam.SubnetAllocatorPath); err != nil {
		if !os.IsNotExist(err) {
			return err
		}
		return nil
	}
	// 读取文件,加载配置信息
	subnetConfigFile, err := os.Open(ipam.SubnetAllocatorPath)
	if err != nil {
		return err
	}
	defer subnetConfigFile.Close()
	subnetJson := make([]byte, 2000)
	n, err := subnetConfigFile.Read(subnetJson)
	if err != nil {
		return errors.Wrap(err, "read subnet config file error")
	}
	err = json.Unmarshal(subnetJson[:n], ipam.Subnets)
	return errors.Wrap(err, "err dump allocation info")
}

将内存中的数据持久化到文件:

// dump 存储网段地址分配信息
func (ipam *IPAM) dump() error {
	ipamConfigFileDir, _ := path.Split(ipam.SubnetAllocatorPath)
	if _, err := os.Stat(ipamConfigFileDir); err != nil {
		if !os.IsNotExist(err) {
			return err
		}
		if err = os.MkdirAll(ipamConfigFileDir, constant.Perm0644); err != nil {
			return err
		}
	}
	// 打开存储文件 O_TRUNC 表示如果存在则消空, os O_CREATE 表示如果不存在则创建
	subnetConfigFile, err := os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, constant.Perm0644)
	if err != nil {
		return err
	}
	defer subnetConfigFile.Close()
	ipamConfigJson, err := json.Marshal(ipam.Subnets)
	if err != nil {
		return err
	}
	_, err = subnetConfigFile.Write(ipamConfigJson)
	return err
}

Allocate

这部分为 Allocate 方法的实现,比较简单

  • 1)从文件中加载 IPAM 数据

  • 2)根据子网信息在 map 中找到存储 IP 分配信息的字符串

  • 3)遍历字符串找到其中为 0 的元素,并根据偏移按照算法计算得到本次分配的 IP

  • 4)把对应位置置 1 并写回文件

// Allocate 在网段中分配一个可用的 IP 地址
func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) {
	// 存放网段中地址分配信息的数组
	ipam.Subnets = &map[string]string{}

	// 从文件中加载已经分配的网段信息
	err = ipam.load()
	if err != nil {
		return nil, errors.Wrap(err, "load subnet allocation info error")
	}
	// net.IPNet.Mask.Size函数会返回网段的子网掩码的总长度和网段前面的固定位的长度
	// 比如“127.0.0.0/8”网段的子网掩码是“255.0.0.0”
	// 那么subnet.Mask.Size()的返回值就是前面255所对应的位数和总位数,即8和24
	_, subnet, _ = net.ParseCIDR(subnet.String())
	one, size := subnet.Mask.Size()
	// 如果之前没有分配过这个网段,则初始化网段的分配配置
	if _, exist := (*ipam.Subnets)[subnet.String()]; !exist {
		// /用“0”填满这个网段的配置,uint8(size - one )表示这个网段中有多少个可用地址
		// size - one是子网掩码后面的网络位数,2^(size - one)表示网段中的可用IP数
		// 而2^(size - one)等价于1 << uint8(size - one)
		// 左移一位就是扩大两倍

		(*ipam.Subnets)[subnet.String()] = strings.Repeat("0", 1<<uint8(size-one))
	}
	// 遍历网段的位图数组
	for c := range (*ipam.Subnets)[subnet.String()] {
		// 找到数组中为“0”的项和数组序号,即可以分配的 IP
		if (*ipam.Subnets)[subnet.String()][c] == '0' {
			// 设置这个为“0”的序号值为“1” 即标记这个IP已经分配过了
			// Go 的字符串,创建之后就不能修改 所以通过转换成 byte 数组,修改后再转换成字符串赋值
			ipalloc := []byte((*ipam.Subnets)[subnet.String()])
			ipalloc[c] = '1'
			(*ipam.Subnets)[subnet.String()] = string(ipalloc)
			// 这里的 subnet.IP只是初始IP,比如对于网段192 168.0.0/16 ,这里就是192.168.0.0
			ip = subnet.IP
			/*
				还需要通过网段的IP与上面的偏移相加计算出分配的IP地址,由于IP地址是uint的一个数组,
				需要通过数组中的每一项加所需要的值,比如网段是172.16.0.0/12,数组序号是65555,
				那么在[172,16,0,0] 上依次加[uint8(65555 >> 24)、uint8(65555 >> 16)、
				uint8(65555 >> 8)、uint8(65555 >> 0)], 即[0, 1, 0, 19], 那么获得的IP就
				是172.17.0.19.
			*/
			for t := uint(4); t > 0; t -= 1 {
				[]byte(ip)[4-t] += uint8(c >> ((t - 1) * 8))
			}
			// /由于此处IP是从1开始分配的(0被网关占了),所以最后再加1,最终得到分配的IP 172.17.0.20
			ip[3] += 1
			break
		}
	}
	// 最后调用dump将分配结果保存到文件中
	err = ipam.dump()
	if err != nil {
		log.Error("Allocate:dump ipam error", err)
	}
	return
}

Release

释放则和分配相反,根据 IP 计算出对应的位图数组索引位置并将其置 0,然后保存到文件中。

func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error {
	ipam.Subnets = &map[string]string{}
	_, subnet, _ = net.ParseCIDR(subnet.String())

	err := ipam.load()
	if err != nil {
		return errors.Wrap(err, "load subnet allocation info error")
	}
	// 和分配一样的算法,反过来根据IP找到位图数组中的对应索引位置
	c := 0
	releaseIP := ipaddr.To4()
	releaseIP[3] -= 1
	for t := uint(4); t > 0; t -= 1 {
		c += int(releaseIP[t-1]-subnet.IP[t-1]) << ((4 - t) * 8)
	}
	// 然后将对应位置0
	ipalloc := []byte((*ipam.Subnets)[subnet.String()])
	ipalloc[c] = '0'
	(*ipam.Subnets)[subnet.String()] = string(ipalloc)

	// 最后调用dump将分配结果保存到文件中
	err = ipam.dump()
	if err != nil {
		log.Error("Allocate:dump ipam error", err)
	}
	return nil
}

测试

通过两个单元测试来测试网段中 IP 的分配和释放。

func TestAllocate(t *testing.T) {
    _, ipNet, _ := net.ParseCIDR("192.168.0.1/24")
    ip, err := ipAllocator.Allocate(ipNet)
    if err != nil {
       t.Fatal(err)
    }
    t.Logf("alloc ip: %v", ip)
}

func TestRelease(t *testing.T) {
    ip, ipNet, _ := net.ParseCIDR("192.168.0.1/24")
    err := ipAllocator.Release(ipNet, &ip)
    if err != nil {
       t.Fatal(err)
    }
}
Allocate

先执行分配,看一下:

root@mydocker:~/feat-network-1/mydocker/network# go test -v -run TestAllocate
=== RUN   TestAllocate
    ipam_test.go:14: alloc ip: 192.168.0.1
--- PASS: TestAllocate (0.00s)
PASS
ok  	mydocker/network	0.006s

查看以下保存的文件是否正常:

root@mydocker:~/feat-network-1/mydocker/network# cat /var/lib/mydocker/network/ipam/subnet.json
{"192.168.0.0/24":"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}

可以看到网段的第 1 位被置为了 1,说明我们的分配功能是ok的。

Release

测试一下释放刚才分配的IP

root@mydocker:~/feat-network-1/mydocker/network# go test -v -run TestRelease
=== RUN   TestRelease
--- PASS: TestRelease (0.00s)
PASS
ok  	mydocker/network	0.005s

再次查看文件

root@mydocker:~/feat-network-1/mydocker/network# cat /var/lib/mydocker/network/ipam/subnet.json
{"192.168.0.0/24":"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}

可以看到网段对应的第 1 位已经被重新置为 0 了,说明释放功能也是 ok 的。

4. NetworkDriver 实现

大致实现

这里实现简单的桥接网络作为容器的网络驱动,因此:

  • Create:创建 Bridge 设备
  • Delete:删除 Bridge 设备
  • Connect:将 veth 关联到网桥
  • Disconnect:将 veth 从网桥解绑

当然,除了 Bridge 设备外还有其他一些配置,,这篇文章Docker教程(十)—揭秘 Docker 网络:手动实现 Docker 桥接网络 有详细信息,这里就不在重复赘述。

文章中网络管理大致包括以下几条命令:

# 创建网桥
sudo brctl addbr br0
# 为bridge分配IP地址,激活上线
sudo ip addr add 172.18.0.1/24 dev br0
sudo ip link set br0 up
# 配置 nat 规则让容器可以访问外网
sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/24 ! -o br0 -j MASQUERADE

我们的网络驱动要做的事情就是把上述命令用 Go 实现,需要用到以下几个库

  • net 库是 Go 语言内置的库,提供了跨平台支持的网络地址处理,以及各种常见协议的IO支持,比如TCP、UDP、DNS、Unix Socket等。
  • netlink库 是Go 语言的操作网络接口、路由表等配置的库 ,使用它的调用相当于我们通过 IP 命令去管理网络接口。
  • netns库 就是 Go 语言版的ip netns exec 命令实现。通过这个库可以让 netlink 库中配置网络接口的代码在某个容器的 Net amespace 中执行。

实现前面定义的 Driver 接口即可。

type BridgeNetworkDriver struct {
}

func (d *BridgeNetworkDriver) Name() string {
	return "bridge"
}

func (d *BridgeNetworkDriver) Create(subnet string, name string) (*Network, error) {
	return nil, err
}

// Delete 删除网络
func (d *BridgeNetworkDriver) Delete(network Network) error {
	return nil
}

func (d *BridgeNetworkDriver) Disconnect(network Network, endpoint *Endpoint) error {
	return nil
}

Create

根据子网信息创建 Bridge 设备并初始化。

func (d *BridgeNetworkDriver) Create(subnet string, name string) (*Network, error) {
    ip, ipRange, _ := net.ParseCIDR(subnet)
    ipRange.IP = ip
    n := &Network{
       Name:    name,
       IPRange: ipRange,
       Driver:  d.Name(),
    }
    err := d.initBridge(n)
    if err != nil {
       return nil, errors.Wrapf(err, "Failed to create bridge network")
    }
    return n, err
}

核心在 initBridge 中,具体如下:

func (d *BridgeNetworkDriver) initBridge(n *Network) error {
    bridgeName := n.Name
    // 1)创建 Bridge 虚拟设备
    if err := createBridgeInterface(bridgeName); err != nil {
       return errors.Wrapf(err, "Failed to create bridge %s", bridgeName)
    }

    // 2)设置 Bridge 设备地址和路由
    gatewayIP := *n.IPRange
    gatewayIP.IP = n.IPRange.IP

    if err := setInterfaceIP(bridgeName, gatewayIP.String()); err != nil {
       return errors.Wrapf(err, "Error set bridge ip: %s on bridge: %s", gatewayIP.String(), bridgeName)
    }
    // 3)启动 Bridge 设备
    if err := setInterfaceUP(bridgeName); err != nil {
       return errors.Wrapf(err, "Failed to set %s up", bridgeName)
    }

    // 4)设置 iptables SNAT 规则
    if err := setupIPTables(bridgeName, n.IPRange); err != nil {
       return errors.Wrapf(err, "Failed to set up iptables for %s", bridgeName)
    }

    return nil
}
创建 bridge 虚拟设备

这部分主要实现下面 ip link add x这个命令,创建一个 Bridge 设备。

// createBridgeInterface 创建Bridge设备
func createBridgeInterface(bridgeName string) error {
    // 先检查是否己经存在了这个同名的Bridge设备
    _, err := net.InterfaceByName(bridgeName)
    // 如果已经存在或者报错则返回创建错
    // errNoSuchInterface这个错误未导出也没提供判断方法,只能判断字符串了。。
    if err == nil || !strings.Contains(err.Error(), "no such network interface") {
       return err
    }

    // create *netlink.Bridge object
    la := netlink.NewLinkAttrs()
    la.Name = bridgeName
    // 使用刚才创建的Link的属性创netlink Bridge对象
    br := &netlink.Bridge{LinkAttrs: la}
    // 调用 net link Linkadd 方法,创 Bridge 虚拟网络设备
    // netlink.LinkAdd 方法是用来创建虚拟网络设备的,相当于 ip link add xxxx
    if err = netlink.LinkAdd(br); err != nil {
       return errors.Wrapf(err, "create bridge %s error", bridgeName)
    }
    return nil
}
设置 Bridge 设备的地址和路由

这部分主要实现下面 ip addr add xxx这个命令,为 Bridge 设备分为 IP 地址以及路由表配置。

func setInterfaceIP(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")
   }
   // 由于 netlink.ParseIPNet 是对 net.ParseCIDR一个封装,因此可以将 net.PareCIDR中返回的IP进行整合
   // 返回值中的 ipNet 既包含了网段的信息,192 168.0.0/24 ,也包含了原始的IP 192.168.0.1
   ipNet, err := netlink.ParseIPNet(rawIP)
   if err != nil {
      return err
   }
   // 通过  netlink.AddrAdd给网络接口配置地址,相当于ip addr add xxx命令
   // 同时如果配置了地址所在网段的信息,例如 192.168.0.0/24
   // 还会配置路由表 192.168.0.0/24 转发到这 testbridge 的网络接口上
   addr := &netlink.Addr{IPNet: ipNet}
   return netlink.AddrAdd(iface, addr)
}
启动 Bridge 设备

这部分主要实现下面 ip link set xxx up这个命令,启动 Bridge 设备。

func setInterfaceUP(interfaceName string) error {
    link, err := netlink.LinkByName(interfaceName)
    if err != nil {
       return errors.Wrapf(err, "error retrieving a link named [ %s ]:", link.Attrs().Name)
    }
    // 等价于 ip link set xxx up 命令
    if err = netlink.LinkSetUp(link); err != nil {
       return errors.Wrapf(err, "nabling interface for %s", interfaceName)
    }
    return nil
}
设置 iptabels 规则

最后则是设置 iptables 规则实现 SNAT,便于容器访问外部网络。

$ iptables -t nat -A POSTROUTING -s 172.18.0.0/24 -o eth0 -j MASQUERADE
# 语法:iptables -t nat -A POSTROUTING -s {subnet} -o {deviceName} -j MASQUERADE
// setupIPTables 设置 iptables 对应 bridge MASQUERADE 规则
func setupIPTables(bridgeName string, subnet *net.IPNet) error {
    // 拼接命令
    iptablesCmd := fmt.Sprintf("-t nat -A POSTROUTING -s %s ! -o %s -j MASQUERADE", subnet.String(), bridgeName)
    cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...)
    // 执行该命令
    output, err := cmd.Output()
    if err != nil {
       log.Errorf("iptables Output, %v", output)
    }
    return err
}

通过直接执行 iptables 命令,创建 SNAT 规则,只要是从这个网桥上出来的包,都会对其做源 IP 的转换,保证了容器经过宿主机访问到宿主机外部网络请求的包转换成机器的 IP,从而能正确的送达和接收。

Delete

删除就比较简单,删除对应名称的 Bridge 设备即可。

// Delete 删除网络
func (d *BridgeNetworkDriver) Delete(network Network) error {
    // 根据名字找到对应的Bridge设备
    br, err := netlink.LinkByName(network.Name)
    if err != nil {
       return err
    }
    // 删除网络对应的 Linux Bridge 设备
    return netlink.LinkDel(br)
}

Connect

connect 则是将 Endpoint 连接到当前指定网络。

类似于使用以下命令将 veth 设备添加到网桥设备上。

sudo brctl addif br0 veth1

实现如下:

// Connect 连接一个网络和网络端点
func (d *BridgeNetworkDriver) Connect(network *Network, endpoint *Endpoint) error {
	bridgeName := network.Name
	// 通过接口名获取到 Linux Bridge 接口的对象和接口属性
	br, err := netlink.LinkByName(bridgeName)
	if err != nil {
		return err
	}
	// 创建 Veth 接口的配置
	la := netlink.NewLinkAttrs()
	// 由于 Linux 接口名的限制,取 endpointID 的前
	la.Name = endpoint.ID[:5]
	// 通过设置 Veth 接口 master 属性,设置这个Veth的一端挂载到网络对应的 Linux Bridge
	la.MasterIndex = br.Attrs().Index
	// 创建 Veth 对象,通过 PeerNarne 配置 Veth 另外 端的接口名
	// 配置 Veth 另外 端的名字 cif {endpoint ID 的前 位}
	endpoint.Device = netlink.Veth{
		LinkAttrs: la,
		PeerName:  "cif-" + endpoint.ID[:5],
	}
	// 调用netlink的LinkAdd方法创建出这个Veth接口
	// 因为上面指定了link的MasterIndex是网络对应的Linux Bridge
	// 所以Veth的一端就已经挂载到了网络对应的LinuxBridge.上
	if err = netlink.LinkAdd(&endpoint.Device); err != nil {
		return fmt.Errorf("error Add Endpoint Device: %v", err)
	}
	// 调用netlink的LinkSetUp方法,设置Veth启动
	// 相当于ip link set xxx up命令
	if err = netlink.LinkSetUp(&endpoint.Device); err != nil {
		return fmt.Errorf("error Add Endpoint Device: %v", err)
	}
	return nil
}

Disconnct

Disconnect 就是把 veth 从 Bridge 上解绑,比较少用到,暂不实现。

func (d *BridgeNetworkDriver) Disconnect(network Network, endpoint *Endpoint) error {
	return nil
}

测试

同样先通过几个简单的单元测试来测试一下功能是否正常。

var testName = "testbridge"

func TestBridgeCreate(t *testing.T) {
	d := BridgeNetworkDriver{}
	n, err := d.Create("192.168.0.1/24", testName)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("create network :%v", n)
}

func TestBridgeDelete(t *testing.T) {
	d := BridgeNetworkDriver{}
	err := d.Delete(testName)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("delete network :%v", testName)
}

func TestBridgeConnect(t *testing.T) {
	ep := Endpoint{
		ID: "testcontainer",
	}

	n := Network{
		Name: testName,
	}

	d := BridgeNetworkDriver{}
	err := d.Connect(&n, &ep)
	if err != nil {
		t.Fatal(err)
	}
}

func TestBridgeDisconnect(t *testing.T) {
	ep := Endpoint{
		ID: "testcontainer",
	}

	n := Network{
		Name: testName,
	}

	d := BridgeNetworkDriver{}
	err := d.Disconnect(n, &ep)
	if err != nil {
		t.Fatal(err)
	}
}
Create
root@mydocker:~/feat-network-1/mydocker/network# go test -v -run TestBridgeCreate
=== RUN   TestBridgeCreate
    bridge_driver_test.go:15: create network :&{testbridge 192.168.0.1/24 bridge}
--- PASS: TestBridgeCreate (1.80s)
PASS
ok  	mydocker/network	1.804s

然后查看是否真正创建出了网桥

root@mydocker:~/feat-network-1/mydocker/network# ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether fa:16:3e:58:62:ef brd ff:ff:ff:ff:ff:ff
3: testbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether b6:f9:fe:f3:f7:16 brd ff:ff:ff:ff:ff:ff

可以看到,第三个就是我们刚创建出的 testbridge 网桥,说明 create 是正常的。

Delete
root@mydocker:~/feat-network-1/mydocker/network# go test -v -run TestBridgeDelete
=== RUN   TestBridgeDelete
    bridge_driver_test.go:24: delete network :testbridge
--- PASS: TestBridgeDelete (0.02s)
PASS
ok  	mydocker/network	0.019s

检查是否真正删除了

root@mydocker:~/feat-network-1/mydocker/network# ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether fa:16:3e:58:62:ef brd ff:ff:ff:ff:ff:ff

testbridge 网桥已经不存在了,说明 Delete 也是正常的。

Connect

需要先创建网桥,在进行绑定测试:

root@mydocker:~/feat-network-1/mydocker/network# go test -v -run TestBridgeConnect
=== RUN   TestBridgeConnect
--- PASS: TestBridgeConnect (0.10s)
PASS
ok  	mydocker/network	0.104s

查看是否新建了 veth 并绑定到该网桥上了

root@mydocker:~/feat-network-1/mydocker/network# ip link show type veth
5: cif-testc@testc: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 2a:fb:68:92:7e:59 brd ff:ff:ff:ff:ff:ff
6: testc@cif-testc: <NO-CARRIER,BROADCAST,MULTICAST,UP,M-DOWN> mtu 1500 qdisc noqueue master testbridge state LOWERLAYERDOWN mode DEFAULT group default qlen 1000
    link/ether 06:d6:04:62:13:eb brd ff:ff:ff:ff:ff:ff

可以看到,确实创建出了指定的 veth 设备(testc),由于只取了名称前 5 位,因此为 testc。

根据 master testbridge 属性可以知道,该 veth 关联到了前面创建的 testbridge 网桥上。

说明 Connect 方法是正常的。

Disconnect

需要先绑定后再测试解绑。

root@mydocker:~/feat-network-1/mydocker/network# go test -v -run TestBridgeDisconnect
=== RUN   TestBridgeDisconnect
--- PASS: TestBridgeDisconnect (0.01s)
PASS
ok  	mydocker/network	0.013s

查看是否接触绑定

root@mydocker:~/feat-network-1/mydocker/network# ip link show type veth
5: cif-testc@testc: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 2a:fb:68:92:7e:59 brd ff:ff:ff:ff:ff:ff
6: testc@cif-testc: <NO-CARRIER,BROADCAST,MULTICAST,UP,M-DOWN> mtu 1500 qdisc noqueue state LOWERLAYERDOWN mode DEFAULT group default qlen 1000
    link/ether 06:d6:04:62:13:eb brd ff:ff:ff:ff:ff:ff

可以看到,之前的 master testbridge 属性不见了,说明解绑成功。

5. 小结

本章实现了容器网络的前置工作,包括:

  • 负载 IP 管理的 IPAM 组件
  • 以及网络管理的 NetworkDriver 组件。

下一篇会在此基础上,实现容器网络,包括:

  • mydocker network create/delete 命令,实现网络管理
  • mydocker run -net 参数,将容器加入到指定网络中

最后再次推荐一下 Docker教程(十)—揭秘 Docker 网络:手动实现 Docker 桥接网络


**【从零开始写 Docker 系列】**持续更新中,搜索公众号【探索云原生】订阅,文章。



完整代码见:https://github.com/lixd/mydocker
欢迎关注~

相关代码见 feat-network-1 分支,测试脚本如下:

# 克隆代码
git clone -b feat-run-e https://github.com/lixd/mydocker.git
cd mydocker
# 进入 Network 目录
cd network
# 运行测试
go test -v -run TestAllocate
# 查看结果
cat /var/lib/mydocker/network/ipam/subnet.json

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/657240.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

adb的常见操作和命令

最近学习adb的时候&#xff0c;整理了一些adb的使用场景&#xff0c;如&#xff1a;adb与设备交互&#xff0c;adb的安装、卸载&#xff0c;adb命令启动&#xff0c;通过命令清除缓存&#xff0c;文件传输和日志操作。 adb的两大作用&#xff1a;在app测试的时候可以提供监控日…

[AIGC] Nginx常用变量详解

Nginx非常强大&#xff0c;其主要功能包括HTTP服务器、反向代理、负载均衡等。Nginx的配置中有许多内置的变量&#xff0c;你可以在配置文件中使用这些变量进行灵活的配置。在本篇文章中&#xff0c;我们将介绍一些Nginx中常见的变量&#xff0c;包括proxy_add_header。 常见变…

基于BP神经网络和小波变换特征提取的烟草香型分类算法matlab仿真,分为浓香型,清香型和中间香型

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 ...................................................................................... …

文章结尾,铺垫下一章带来的期待

你是否容易在阅读时打瞌睡? 是否有很多买回来的书,放在书架上一年甚至几年都未读完,积满了灰尘? 但是,对于小说和电视剧,你却完全停不下来。每集片尾的预告激发了你持续观看下一集的渴望,带来了无限的期待…… 当你撰写文章或编写工具书时,内容可能呈现出乏味的面貌…

轻松放大图片600%,Topaz Gigapixel AI图片无损清晰放大软件下载安装

Topaz Gigapixel AI 该软件拥有卓越的性能和先进的技术&#xff0c;能够轻松实现图像的精细放大&#xff0c;最多可将图像放大至原始尺寸的六倍&#xff0c;而无需担心图像质量的损失。 相较于传统的图像放大软件&#xff0c;Topaz Gigapixel AI 表现出了明显的优势。传统软件…

基于双PI结构FOC闭环控制的永磁同步电机控制系统simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 5.完整工程文件 1.课题概述 基于双PI结构FOC闭环控制的永磁同步电机控制系统simulink建模与仿真。 2.系统仿真结果 3.核心程序与模型 版本&#xff1a;MATLAB2022a 64 4.系统原理简介 永磁同步电机&a…

Java基础:异常(三)

Java基础&#xff1a;异常&#xff08;三&#xff09; 文章目录 Java基础&#xff1a;异常&#xff08;三&#xff09;1. Java异常体系1.1 错误的分类1.2 异常的分类 2. 异常的捕获与处理2.1 try-catch2.2 finally 3. 异常的抛出4. 自定义异常 1. Java异常体系 Java的异常体系是…

解决Springboot服务启动报错:“Reason: Failed to determine suitable jdbc url”

1、错误详情 *************************** APPLICATION FAILED TO START *************************** Description: Failed to configure a DataSource: url attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine sui…

【Qt秘籍】[004]-Qt中的重要工具-介绍

QtCreator概览 当我们打开系统的菜单翻到刚刚下载的Qt文件&#xff0c;里面的内容却让我们眼花缭乱。 不过别急&#xff0c;下面我们将一一解析。 1.Assistant Qt自带的离线版本官方文档 2.Designer Qt图形化设计界面的工具&#xff0c;通过拖拽控件快速生成界面&#xff0c…

当消费遇上AI:大模型如何成为行业“网红”?

在一个繁忙过后的周五晚上&#xff0c;美食发烧友Melissa和朋友痛快的享受了一顿海底捞火锅&#xff0c;餐毕&#xff0c;她像往常一样留下了服务评价&#xff0c;及时反馈是一位美食家的基本素养。 每天如同Melissa一样留下评价的客人不在少数&#xff0c;他们的真实体验反馈…

期权具体怎么交易详细的操作流程?

期权就是股票&#xff0c;唯一区别标的物上证指数&#xff0c;会看大盘吧&#xff0c;交易两个方向认购做多&#xff0c;认沽做空&#xff0c;双向t0交易&#xff0c;期权具体交易流程可以理解选择方向多和空&#xff0c;选开仓的合约&#xff0c;买入开仓和平仓没了&#xff0…

【小呆的力学笔记】连续介质力学的知识点回顾二:应变度量

文章目录 3. 格林应变与阿尔曼西应变 3. 格林应变与阿尔曼西应变 变形体在变形前的线元 O A → \overrightarrow{OA} OA &#xff0c;在变形后变成 o a → \overrightarrow{oa} oa &#xff0c;那么应变应该度量这种线元变形前后的差别。 ∣ o a → ∣ 2 − ∣ O A → ∣ 2 …

计算机体系结构-2024期末考试

前言 最后一个字落笔&#xff0c;虽然知道并没有发挥到最好&#xff0c;内心还是感慨良多。 真正意义上本科阶段的课程考试&#xff0c;到此就结束了。 正如青春总有不完美的地方&#xff0c;此刻思绪竟飘到了三年前的盛夏&#xff0c;那个骄傲的少年。 扯远了&#xff0c;…

物联网应用系统与网关

一. 传感器底板相关设计 1. 传感器设计 立创EDA传感器设计举例。 2. 传感器实物图 3. 传感器测试举例 测试激光测距传感器 二. 网关相关设计 1. LORA&#xff0c;NBIOT等设计 2. LORA&#xff0c;NBIOT等实物图 3. ZigBee测试 ZigBee测试 4. NBIoT测试 NBIoT自制模块的测试…

kafka-主题创建(主题操作的命令)

文章目录 1、topic主题操作的命令1.1、创建一个3分区1副本的主题1.1.1、获取 kafka-topics.sh 的帮助信息1.1.2、副本因子设置不能超过集群中broker的数量1.1.3、创建一个3分区1副本的主题1.1.4、查看所有主题1.1.5、查看主题详细描述 1、topic主题操作的命令 kafka发送消息会存…

mysql连接池的实现

概要&#xff1a;本文介绍mysql连接池的实现&#xff0c;要求读者了解线程池 一、为什么需要mysql连接池&#xff1f; 资源复用 &#xff1a;不使用连接池&#xff0c;每次数据库请求都新建一条连接&#xff0c;将耗费系 统资源。 流程如下&#xff1a; 通过三次握手建立 TC…

海博思创储能系统产品再获认可,获得杰出项目类别入围资格

近日&#xff0c;2024年欧洲智慧能源展览会大奖&#xff08;The smarter E AWARD 2024&#xff09;公布了入围名单&#xff0c;该奖项设有五个类别&#xff1a;光伏、储能、电动出行、智能综合能源和杰出项目奖&#xff0c;旨在表彰能源领域中的卓越创新成果。 在入围项目中&a…

linux线程,线程控制与线程相关概念

线程概念 线程这个词或多或少大家都听过&#xff0c;今天我们正式的来谈一下线程&#xff1b; 在我一开始的概念中线程就是进程的一部分&#xff0c;一个进程中有很多个线程&#xff0c;这个想法基本是正确的&#xff0c;但细节部分呢我们需要细细讲解一下&#xff1b; 什么…

Web渗透-MySql-Sql注入:联合查询注入

SQL注入&#xff08;SQL Injection&#xff09;是一种网络攻击技术&#xff0c;攻击者通过将恶意的SQL代码插入到应用程序的输入字段&#xff0c;从而欺骗应用程序执行未经授权的操作。这种攻击方式可以导致严重的安全问题&#xff0c;包括&#xff1a; 数据泄露&#xff1a;攻…

HCIP-Datacom-ARST自选题库__BGP多选【22道题】

1.BGP认证可以防止非法路由器与BGP路由器建立邻居&#xff0c;BGP认证可以分为MD5认证和Keychain认证&#xff0c;请问以下哪些BGP报文会携带BCGP Keychain认证信息?(报头携带) open Update Notication Keepalive 2.传统的BGP-4只能管理IPv4单播路由信息&#xff0c;MP-B…