- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
本文为从零开始写 Docker 系列第十六篇,利用 linux 下的 Veth、Bridge、iptables 等等相关技术,构建容器网络模型,为容器插上”网线“.
完整代码见:https://github.com/lixd/mydocker 欢迎 Star 。
推荐阅读以下文章对 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 用户 。
前面文章中已经实现了容器的大部分功能,不过还缺少了网络部分。现在我们的容器既不能访问外网也不能访问其他容器.
本篇和下一篇文章则会解决该问题,会实现容器网络相关功能,为我们的容器插上”网线“.
本篇主要介绍大致思路以及 IPAM 和 Network Driver 组件的实现过程.
相关内容在这边文章:Docker教程(十)---揭秘 Docker 网络:手动实现 Docker 桥接网络 中已经有了详细记录,感兴趣的可以跳转阅读.
核心如下:
接下来我们要做的就是使用 Go 代码实现这些功能.
首先,将 Bridge 和 Veth 这两个对象进行抽象:网络(Network)和网络端点(Endpoint).
网络(Netowrk)中可以有多个容器,在同一个网络里的容器可以通过这个网络互相通信.
就像挂载到同一个 Linux Bridge 设备上的网络设备一样, 可以直接通过 Bridge 设备实现网络互连;连接到同一个网络中的容器也可以通过这个网络和网络中别的容器互连.
网络中会包括这个网络相关的配置,比如网络的容器地址段、网络操作所调用的网络驱动等信息.
type Network struct {
Name string // 网络名
IPRange *net.IPNet // 地址段
Driver string // 网络驱动名
}
网络端点(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) **是一个网络功能中的组件,不同的驱动对网络的创建、连接、销毁的策略不同,通过在创建网络时指定不同的网络驱动来定义使用哪个驱动做网络的配置.
它的接口定义如下:
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(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 的分配以及释放,因此需要找个地方存储哪些 IP 分配了,哪些 IP 可用.
由于对每个 IP 来说只存在已分配、可用两种状态,因此容器想到使用 bitmap 来存储.
bitmap 在大规模连续且少状态的数据处理中有很高的效率,比如要用到的 IP 地址分配.
一个网段中的某个 IP 地址有两种状态:
那么一个 IP 地址的状态就可以用一位来表示, 并且通过这位相对基础位的偏移也能够迅速定位到数据所在的位.
通过位图的方式实现 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 方法的实现,比较简单 。
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
}
释放则和分配相反,根据 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)
}
}
先执行分配,看一下:
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的.
测试一下释放刚才分配的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 的.
这里实现简单的桥接网络作为容器的网络驱动,因此:
当然,除了 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 实现,需要用到以下几个库 。
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
}
根据子网信息创建 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
}
这部分主要实现下面 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
}
这部分主要实现下面 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)
}
这部分主要实现下面 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
}
最后则是设置 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,从而能正确的送达和接收.
删除就比较简单,删除对应名称的 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 则是将 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
}
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)
}
}
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 是正常的.
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 也是正常的.
需要先创建网桥,在进行绑定测试:
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 方法是正常的.
需要先绑定后再测试解绑.
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 属性不见了,说明解绑成功.
本章实现了容器网络的前置工作,包括:
下一篇会在此基础上,实现容器网络,包括:
最后再次推荐一下 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
最后此篇关于从零开始写Docker(十六)---容器网络实现(上):为容器插上”网线”的文章就讲到这里了,如果你想了解更多关于从零开始写Docker(十六)---容器网络实现(上):为容器插上”网线”的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我需要(我必须)将大量 float 写入 qdatastream 并且我只使用 4 个字节是必要的。setFloatingPointPrecision 或为 float 和 double 写入 4 或
我有一些 C 代码,我用 Python 对其进行了扩展。扩展的 C 代码有一个将一些结构附加到二进制文件的函数: void writefunction(const struct struct1* so
我正在用 C 语言开发一个小软件,用于在布告栏中读取和写入消息。每条消息都是一个以渐进数字命名的 .txt。 软件是多线程的,有很多用户可以并发操作。 用户可以进行的操作有: 阅读整个公告板(所有 .
我有 2 个线程同时访问同一个大文件 (.txt)。 第一个线程正在从文件中读取。第二个线程正在写入文件。 两个线程都访问同一个 block ,例如(开始:0, block 大小:10),但具有不同的
我做了很多谷歌搜索,但我仍然不确定如何继续。 Linux 下最常见的剪贴板读写方式是什么?我想要同时支持 Gnome 和 KDE 桌面。 更新:我是否认为没有简单的解决方案,必须将多个来源(gnome
1. 定义配置文件信息 有时候我们为了统一管理会把一些变量放到 yml 配置文件中 例如 图片 用 @ConfigurationProperties 代替 @Value 使用方法 定义对应字段的实体
在开始之前,我必须先声明我是 FORTRAN 的新手。我正在维护 1978 年的一段遗留代码。它的目的是从文件中读取一些数据值,处理这些值,然后将处理过的值输出到另一个文本文件。 给定以下 FORTR
我正在制作一个应用程序,我需要存储用户提供的一些信息。我尝试使用 .plist 文件来存储信息,我发现: NSString *filePath = @"/Users/Denis/Documents/X
在delphi类中声明属性时是否可能有不同类型的结果? 示例: 属性月份:字符串读取monthGet(字符串)写入monthSet(整数); 在示例中,我希望在属性(property)月份中,当我:读
我正在以二进制形式将文件加载到数组中,这似乎需要一段时间有没有更好更快更有效的方法来做到这一点。我正在使用类似的方法写回文件。 procedure openfile(fname:string); va
我想实现一个运行模拟的C#控制台应用程序。另外,我想给用户机会在控制台上按“+”或“-”来加速/减速模拟的速度。 有没有办法在编写控制台时读取控制台?我相信我可以为此使用多线程,但是我却不怎么做(我对
这是我的代码: use std::fs::File; use std::io::Write; fn main() { let f = File::create("").unwrap();
我有一个应用程序可以访问 csv 文本文件中的单词。由于它们通常不会更改,因此我将它们放置在 .jar 文件中,并使用 .getResourceAsStream 调用读取它们。我真的很喜欢这种方法,因
我使用kubeadm,docker 17.12.1-ce和法兰绒网络安装了Kubernetes 1.13.1集群 但是,我发现Kubernetes主服务器上有许多空文件,权限为666,该文件允许任何用
我的工作区中有一些 java 文件。现在我想编写一个java程序,它可以读取来自不同源的文本文件,一次一个,一行一行,并将这些行插入到工作区中各自的java文件中。 文本文件会告诉我将哪个文件插入到哪
用户A要求系统读取文件foo,同时用户B想要将他或她的数据保存到同一个文件中。在文件系统级别如何处理这种情况? 最佳答案 大多数文件系统(但不是全部)使用锁定来保护对同一文件的并发访问。锁可以是独占的
我对保护移动应用程序的 firebase 数据库有一些疑问。 例如,在反编译Android应用程序后,黑客可以获取firebase api key 然后访问firebase数据库,这是正确的吗? 假设
我想让文件从外部不可删除,并希望使用java从程序对该文件进行读/写操作。 S0,我使用以下代码使用java创建了不可删除的文件: Process pcs = Runtime.getRunti
当 Selector.select() 以阻塞模式等待读/写操作时,是否可以将写消息推送到客户端?如何将选择器从阻塞模式移至写入模式?触发器可以是一个后台线程,用于放置需要写入给定 channel 的
我目前正在学习在 Linux 环境中使用 C 进行套接字编程。作为一个项目,我正在尝试编写一个基本的聊天服务器和客户端。 目的是让服务器为每个连接的客户端派生一个进程。 我遇到的问题是读取一个 chi
我是一名优秀的程序员,十分优秀!