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

mydocker-network-2.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. 概述

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

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

本篇主要在上篇基础上,基于完成剩余工作:

  • mydocker network create/list/delete 命令, 让我们能通过 mydocker 命令实现对容器网络的管理
  • 实现 mydocker run -net,让容器可以加入指定网络

2. Network 实现

基于 IPAM 和 NetworkDriver 实现网络创建、查询、删除、容器加入网络等等功能。

就在之前定义的 Network 对象上增加对应方法即可

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

Driver 注册

暂时使用一个 全局变量 drivers 存储所有的网络驱动。

在 init 方法中进行 driver 注册。

var (defaultNetworkPath = "/var/lib/mydocker/network/network/"drivers            = map[string]Driver{}
)func init() {// 加载网络驱动var bridgeDriver = BridgeNetworkDriver{}drivers[bridgeDriver.Name()] = &bridgeDriver// 文件不存在则创建if _, err := os.Stat(defaultNetworkPath); err != nil {if !os.IsNotExist(err) {logrus.Errorf("check %s is exist failed,detail:%v", defaultNetworkPath, err)return}if err = os.MkdirAll(defaultNetworkPath, constant.Perm0644); err != nil {logrus.Errorf("create %s failed,detail:%v", defaultNetworkPath, err)return}}
}

Network 信息存储

默认将容器网络信息存储在/var/lib/mydocker/network/network/目录。

提供 dump、load、remove 等方法管理文件系统中的网络信息。

func (net *Network) dump(dumpPath string) error {// 检查保存的目录是否存在,不存在则创建if _, err := os.Stat(dumpPath); err != nil {if !os.IsNotExist(err) {return err}if err = os.MkdirAll(dumpPath, constant.Perm0644); err != nil {return errors.Wrapf(err, "create network dump path %s failed", dumpPath)}}// 保存的文件名是网络的名字netPath := path.Join(dumpPath, net.Name)// 打开保存的文件用于写入,后面打开的模式参数分别是存在内容则清空、只写入、不存在则创建netFile, err := os.OpenFile(netPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, constant.Perm0644)if err != nil {return errors.Wrapf(err, "open file %s failed", dumpPath)}defer netFile.Close()netJson, err := json.Marshal(net)if err != nil {return errors.Wrapf(err, "Marshal %v failed", net)}_, err = netFile.Write(netJson)return errors.Wrapf(err, "write %s failed", netJson)
}func (net *Network) remove(dumpPath string) error {// 检查网络对应的配置文件状态,如果文件己经不存在就直接返回fullPath := path.Join(dumpPath, net.Name)if _, err := os.Stat(fullPath); err != nil {if !os.IsNotExist(err) {return err}return nil}// 否则删除这个网络对应的配置文件return os.Remove(fullPath)
}func (net *Network) load(dumpPath string) error {// 打开配置文件netConfigFile, err := os.Open(dumpPath)if err != nil {return err}defer netConfigFile.Close()// 从配置文件中读取网络 配置 json 符串netJson := make([]byte, 2000)n, err := netConfigFile.Read(netJson)if err != nil {return err}err = json.Unmarshal(netJson[:n], net)return errors.Wrapf(err, "unmarshal %s failed", netJson[:n])
}

同时提供了一个 loadNetwork 方法,扫描/var/lib/mydocker/network/network/目录,并加载所有 Network 数据到内存中,便于使用。

// LoadFromFile 读取 defaultNetworkPath 目录下的 Network 信息存放到内存中,便于使用
func loadNetwork() (map[string]*Network, error) {networks := map[string]*Network{}// 检查网络配置目录中的所有文件,并执行第二个参数中的函数指针去处理目录下的每一个文件err := filepath.Walk(defaultNetworkPath, func(netPath string, info os.FileInfo, err error) error {// 如果是目录则跳过if info.IsDir() {return nil}// if strings.HasSuffix(netPath, "/") {// 	return nil// }//  加载文件名作为网络名_, netName := path.Split(netPath)net := &Network{Name: netName,}// 调用前面介绍的 Network.load 方法加载网络的配置信息if err = net.load(netPath); err != nil {logrus.Errorf("error load network: %s", err)}// 将网络的配置信息加入到 networks 字典中networks[netName] = netreturn nil})return networks, err
}

3. mydocker network 命令

mydocker network 命令一共需要实现 3 个 子命令:

  • create:创建网络
  • list:查看当前所有网络信息
  • remove:删除网络

Create Command

我们要是实现的通过mydocker network create命令创建一个容器网络,就像下面这样:

mydocker network create --subset 192.168.0.0/24 --deive bridge testbr

通过 Bridge 网络驱动创建一个名为 testbr 的网络,网段则是 192.168.0.0/24。

流程

具体流程如下图所示:

network-create.png

上图中的 IPAM 和 Network Driver 两个组件就是我们前面实现的。

  • IPAM 负责通过传入的IP网段去分配一个可用的 IP 地址给容器和网络的网关,比如网络的网段是 192.168.0.0/16, 那么通过 IPAM 获取这个网段的容器地址就是在这个网段中的一个 IP 地址,然后用于分配给容器的连接端点,保证网络中的容器 IP 不会冲突。

  • 而 Network Driver 是用于网络的管理的,例如在创建网络时完成网络初始化动作及在容器启动时完成网络端点配置。像 Bridge 的驱动对应的动作就是创建 Linux Bridge 和挂载 Veth 设备。

分为以下几步:

  • 首先是使用 IPAM 分配 IP
  • 然后根据 driver 找到对应的 NetworkDriver 并创建网络
  • 最后将网络信息保存到文件
代码实现

首先增加一个 create 子命令,用于需要指定 driver 和 subnet 以及网络名称。

var networkCommand = cli.Command{Name:  "network",Usage: "container network commands",Subcommands: []cli.Command{{Name:  "create",Usage: "create a container network",Flags: []cli.Flag{cli.StringFlag{Name:  "driver",Usage: "network driver",},cli.StringFlag{Name:  "subnet",Usage: "subnet cidr",},},Action: func(context *cli.Context) error {if len(context.Args()) < 1 {return fmt.Errorf("missing network name")}driver := context.String("driver")subnet := context.String("subnet")name := context.Args()[0]err := network.CreateNetwork(driver, subnet, name)if err != nil {return fmt.Errorf("create network error: %+v", err)}return nil},}}

核心实现 在 CreateNetwork 方法中,具体如下:

// CreateNetwork 根据不同 driver 创建 Network
func CreateNetwork(driver, subnet, name string) error {// 将网段的字符串转换成net. IPNet的对象_, cidr, _ := net.ParseCIDR(subnet)// 通过IPAM分配网关IP,获取到网段中第一个IP作为网关的IPip, err := ipAllocator.Allocate(cidr)if err != nil {return err}cidr.IP = ip// 调用指定的网络驱动创建网络,这里的 drivers 字典是各个网络驱动的实例字典 通过调用网络驱动// Create 方法创建网络,后面会以 Bridge 驱动为例介绍它的实现net, err := drivers[driver].Create(cidr.String(), name)if err != nil {return err}// 保存网络信息,将网络的信息保存在文件系统中,以便查询和在网络上连接网络端点return net.dump(defaultNetworkPath)
}

List Command

通过 mydocker network list命令显示当前创建了哪些网络。

扫描网络配置的目录/var/lib/mydocker/network/network/拿到所有的网络配置信息并打印即可。

增加一个 list 子命令

var networkCommand = cli.Command{Name:  "network",Usage: "container network commands",Subcommands: []cli.Command{{Name:  "list",Usage: "list container network",Action: func(context *cli.Context) error {network.ListNetwork()return nil},}}

ListNetwork 方法具体实现:

// ListNetwork 打印出当前全部 Network 信息
func ListNetwork() {networks, err := loadNetwork()if err != nil {logrus.Errorf("load network from file failed,detail: %v", err)return}// 通过tabwriter库把信息打印到屏幕上w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)fmt.Fprint(w, "NAME\tIpRange\tDriver\n")for _, net := range networks {fmt.Fprintf(w, "%s\t%s\t%s\n",net.Name,net.IPRange.String(),net.Driver,)}if err = w.Flush(); err != nil {logrus.Errorf("Flush error %v", err)return}
}

Remove Command

流程

通过使用命令 mydocker network remove命令删除己经创建的网络。

具体流程如下图:

network-delete.png

分为以下几个步骤:

  • 1)先调用 IPAM 去释放网络所占用的网关 IP
  • 2)然后调用网络驱动去删除该网络创建的一些设备与配置
  • 3)最终从网络配置目录中删除网络对应的配置文
代码实现
var networkCommand = cli.Command{Name:  "network",Usage: "container network commands",Subcommands: []cli.Command{{Name:  "remove",Usage: "remove container network",Action: func(context *cli.Context) error {if len(context.Args()) < 1 {return fmt.Errorf("missing network name")}err := network.DeleteNetwork(context.Args()[0])if err != nil {return fmt.Errorf("remove network error: %+v", err)}return nil},},},
}

核心实现在 DeleteNetwork 中,具体如下:

// DeleteNetwork 根据名字删除 Network
func DeleteNetwork(networkName string) error {networks, err := loadNetwork()if err != nil {return errors.WithMessage(err, "load network from file failed")}// 网络不存在直接返回一个errornet, ok := networks[networkName]if !ok {return fmt.Errorf("no Such Network: %s", networkName)}// 调用IPAM的实例ipAllocator释放网络网关的IPif err = ipAllocator.Release(net.IPRange, &net.IPRange.IP); err != nil {return errors.Wrap(err, "remove Network gateway ip failed")}// 调用网络驱动删除网络创建的设备与配置 后面会以 Bridge 驱动删除网络为例子介绍如何实现网络驱动删除网络if err = drivers[net.Driver].Delete(net.Name); err != nil {return errors.Wrap(err, "remove Network DriverError failed")}// 最后从网络的配直目录中删除该网络对应的配置文件return net.remove(defaultNetworkPath)
}

4. mydocker run -net

通过创建容器时指定-net 参数,指定容器启动时连接的网络。

mydocker run -it -p 80:80 --net testbridgenet xxxx

这样创建出的容器便可以通过 testbridgenet 这个网络与网络中的其他容器进行通信了。

流程

具体流程图如下:

network-connect.png

在调用创建容器时指定网络,

  • 首先会调用IPAM,通过网络中定义的网段找到未分配的 IP 分配给容器
  • 然后创建容器的网络端点,并调用这个网络的网络驱动连接网络与网络端点,最终完成网络端点的连接和配置。比如在 Bridge 驱动中就会将Veth 设备挂载到 Linux Bridge 网桥上。
  • 最后则是配置端口映射,让用户访问宿主机某端口就能访问到容器中的端口

代码实现

首先先 run 命令增加 net 和 p flag 接收网络和端口映射信息,具体如下:

var runCommand = cli.Command{Name: "run",Usage: `Create a container with namespace and cgroups limitmydocker run -it [command]mydocker run -d -name [containerName] [imageName] [command]`,Flags: []cli.Flag{// 省略...cli.StringFlag{Name:  "net",Usage: "container network,e.g. -net testbr",},cli.StringSliceFlag{Name:  "p",Usage: "port mapping,e.g. -p 8080:80 -p 30336:3306",},},Action: func(context *cli.Context) error {// 省略...network := context.String("net")portMapping := context.StringSlice("p")Run(tty, cmdArray, envSlice, resConf, volume, containerName, imageName,network,portMapping)return nil},
}

然后 Run 方法中增加网络配置逻辑

func Run(tty bool, comArray, envSlice []string, res *subsystems.ResourceConfig, volume, containerName, imageName string,net string, portMapping []string) {containerId := container.GenerateContainerID() // 生成 10 位容器 id// 省略....// 创建cgroup manager, 并通过调用set和apply设置资源限制并使限制在容器上生效cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")defer cgroupManager.Destroy()_ = cgroupManager.Set(res)_ = cgroupManager.Apply(parent.Process.Pid, res)// 如果指定了网络信息则进行配置if net != "" {// config container networkcontainerInfo := &container.Info{Id:          containerId,Pid:         strconv.Itoa(parent.Process.Pid),Name:        containerName,PortMapping: portMapping,}if err = network.Connect(net, containerInfo); err != nil {log.Errorf("Error Connect Network %v", err)return}}// 省略....
}

核心实现在 Connect 方法,完整代码如下:

// Connect 连接容器到之前创建的网络 mydocker run -net testnet -p 8080:80 xxxx
func Connect(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)}// 分配容器IP地址ip, err := ipAllocator.Allocate(network.IPRange)if err != nil {return errors.Wrapf(err, "allocate ip")}// 创建网络端点ep := &Endpoint{ID:          fmt.Sprintf("%s-%s", info.Id, networkName),IPAddress:   ip,Network:     network,PortMapping: info.PortMapping,}// 调用网络驱动挂载和配置网络端点if err = drivers[network.Driver].Connect(network, ep); err != nil {return err}// 到容器的namespace配置容器网络设备IP地址if err = configEndpointIpAddressAndRoute(ep, info); err != nil {return err}// 配置端口映射信息,例如 mydocker run -p 8080:80return configPortMapping(ep)
}

实现和 Docker教程(十)—揭秘 Docker 网络:手动实现 Docker 桥接网络 中手动操作的步骤类似

  • IPAM 分配 IP
  • 创建 veth 设备,一端移动到容器 network namespace
  • 设置 IP 并启动
  • 宿主机添加 iptables 规则,实现端口转发
// configEndpointIpAddressAndRoute 配置容器网络端点的地址和路由
func configEndpointIpAddressAndRoute(ep *Endpoint, info *container.Info) error {// 根据名字找到对应Veth设备peerLink, err := netlink.LinkByName(ep.Device.PeerName)if err != nil {return fmt.Errorf("fail config endpoint: %v", err)}// 将容器的网络端点加入到容器的网络空间中// 并使这个函数下面的操作都在这个网络空间中进行// 执行完函数后,恢复为默认的网络空间,具体实现下面再做介绍defer enterContainerNetNS(&peerLink, info)()// 获取到容器的IP地址及网段,用于配置容器内部接口地址// 比如容器IP是192.168.1.2, 而网络的网段是192.168.1.0/24// 那么这里产出的IP字符串就是192.168.1.2/24,用于容器内Veth端点配置interfaceIP := *ep.Network.IPRangeinterfaceIP.IP = ep.IPAddress// 设置容器内Veth端点的IPif err = setInterfaceIP(ep.Device.PeerName, interfaceIP.String()); err != nil {return fmt.Errorf("%v,%s", ep.Network, err)}// 启动容器内的Veth端点if err = setInterfaceUP(ep.Device.PeerName); err != nil {return err}// Net Namespace 中默认本地地址 127 的勺。”网卡是关闭状态的// 启动它以保证容器访问自己的请求if err = setInterfaceUP("lo"); err != nil {return err}// 设置容器内的外部请求都通过容器内的Veth端点访问// 0.0.0.0/0的网段,表示所有的IP地址段_, cidr, _ := net.ParseCIDR("0.0.0.0/0")// 构建要添加的路由数据,包括网络设备、网关IP及目的网段// 相当于route add -net 0.0.0.0/0 gw (Bridge网桥地址) dev (容器内的Veth端点设备)defaultRoute := &netlink.Route{LinkIndex: peerLink.Attrs().Index,Gw:        ep.Network.IPRange.IP,Dst:       cidr,}// 调用netlink的RouteAdd,添加路由到容器的网络空间// RouteAdd 函数相当于route add 命令if err = netlink.RouteAdd(defaultRoute); err != nil {return err}return nil
}// configPortMapping 配置端口映射
func configPortMapping(ep *Endpoint) error {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规则// 将宿主机的端口请求转发到容器的地址和端口上iptablesCmd := fmt.Sprintf("-t nat -A PREROUTING -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s",portMapping[0], ep.IPAddress.String(), portMapping[1])cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...)logrus.Infoln("配置端口映射 cmd:", cmd.String())// 执行iptables命令,添加端口映射转发规则output, err := cmd.Output()if err != nil {logrus.Errorf("iptables Output, %v", output)continue}}return err
}

至此,容器网络相关改造就算是完成了。

实际上还有一些收尾工作需要处理,比如

  • 容器停止后需要删除对应的 veth 设备,有端口映射的还需要删除对应的 Iptables 规则等
  • 容器信息需要新增网络相关信息,ps 命令也需要调整

本篇主要实现容器网络功能,让大家加深这块的理解,后续的收尾工作就不赘述了。

5. 测试

测试以下几个场景:

  • 容器与容器互联:主要检测 veth 设备 + bridge 设备 + 路由配置
  • 宿主机访问容器:同上
  • 容器访问外部网络:主要检测 veth 设备 + bridge 设备 + 路由配置+ SNAT 规则
  • 外部机器访问容器,端口映射:主要检测 veth 设备 + bridge 设备 + 路由配置+ DNAT 规则

网络拓扑可以参考下图:

Docker Bridge 网络拓扑

可以简单理解为:整个链路使用 veth 设备和 bridge 设备构成,数据流向则有路由规则 SNAT、DNAT 等规则控制。

首先,创建一个网络,用于让容器挂载。

需要注意,这个网段不能和宿主机的网段冲突,否则可能无法正常使用。

root@mydocker:~/feat-network-2/mydocker# go build .
root@mydocker:~/feat-network-2/mydocker# ./mydocker network create --driver bridge --subnet 10.0.0.1/24 testbridge
root@mydocker:~/feat-network-2/mydocker# ./mydocker network list
NAME         IpRange       Driver
testbridge   10.0.0.1/24   bridge

容器与容器互联

数据流向大致为:容器 1 veth --> bridge --> 容器 2 veth。

开启两个终端,分别执行以下命令,在testbridge网络上启动两个容器。

./mydocker run -it -net testbridge busybox sh

查看容器1的IP地址:

/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00inet 127.0.0.1/8 scope host lovalid_lft forever preferred_lft foreverinet6 ::1/128 scope hostvalid_lft forever preferred_lft forever
15: cif-86416@if16: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000link/ether fe:44:02:8a:07:1f brd ff:ff:ff:ff:ff:ffinet 10.0.0.2/24 brd 10.0.0.255 scope global cif-86416valid_lft forever preferred_lft foreverinet6 fe80::fc44:2ff:fe8a:71f/64 scope linkvalid_lft forever preferred_lft forever

可以看到,地址是,10.0.0.2。

然后去另一个容器中尝试连接这个地址。

/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00inet 127.0.0.1/8 scope host lovalid_lft forever preferred_lft foreverinet6 ::1/128 scope hostvalid_lft forever preferred_lft forever
17: cif-12201@if18: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000link/ether 42:da:b3:b8:6f:6a brd ff:ff:ff:ff:ff:ffinet 10.0.0.3/24 brd 10.0.0.255 scope global cif-12201valid_lft forever preferred_lft foreverinet6 fe80::40da:b3ff:feb8:6f6a/64 scope linkvalid_lft forever preferred_lft forever

另一个容器地址是 10.0.0.3。

另外容器中都会自动添加以下路由:

/ # ip r
default via 10.0.0.1 dev cif-81260
10.0.0.0/24 dev cif-81260 scope link  src 10.0.0.3

因此,整个数据流向应该是没什么问题的。

ping 一下看看

/ # ping 10.0.0.2 -c 4
PING 10.0.0.2 (10.0.0.2): 56 data bytes
64 bytes from 10.0.0.2: seq=0 ttl=64 time=0.387 ms
64 bytes from 10.0.0.2: seq=1 ttl=64 time=0.115 ms
64 bytes from 10.0.0.2: seq=2 ttl=64 time=0.129 ms
64 bytes from 10.0.0.2: seq=3 ttl=64 time=0.090 ms--- 10.0.0.2 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.090/0.180/0.387 ms

由以上结果可以看到,两个容器可以通过这个网络互相连通。

宿主机访问容器

数据流向:bridge --> 容器 veth。

由于创建网络(bridge)时会在宿主机上添加下面这样的路由规则

root@mydocker:~/feat-network-2/mydocker# ip r
10.0.0.0/24 dev testbridge proto kernel scope link src 10.0.0.1

因此,宿主机上访问容器最终会交给 Bridge 设备然后进入到容器中。

试一下

root@mydocker:~/feat-network-2/mydocker# ping 10.0.0.5 -c 4
PING 10.0.0.5 (10.0.0.5) 56(84) bytes of data.
64 bytes from 10.0.0.5: icmp_seq=1 ttl=64 time=0.474 ms
64 bytes from 10.0.0.5: icmp_seq=2 ttl=64 time=0.099 ms
64 bytes from 10.0.0.5: icmp_seq=3 ttl=64 time=0.119 ms
64 bytes from 10.0.0.5: icmp_seq=4 ttl=64 time=0.114 ms--- 10.0.0.5 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3032ms
rtt min/avg/max/mdev = 0.099/0.201/0.474/0.157 ms

一切正常。

容器访问外部网络

数据流向大概是这样的:veth -> bridge -> eth0 -> public net。

数据包使用 veth 从容器中跑到宿主机 Bridge,然后使用宿主机网卡 eth0(或者其他网卡)发送出去。

为了让数据包能够正常回来,因此需要进行 SNAT,把数据包原地址从容器 IP 改成宿主机 IP,对应 Iptables 规则是这样的:

iptables -t nat -A POSTROUTING -s 10.0.0.1/24 -o testbridge -j MASQUERADE

在代码实现上,创建网络(bridge) 时我们就添加了该规则,因此正常情况下容器时可以直接访问外网的。

测试一下

/ # ping 114.114.114.114 -c 4
PING 114.114.114.114 (114.114.114.114): 56 data bytes
64 bytes from 114.114.114.114: seq=0 ttl=88 time=19.744 ms
64 bytes from 114.114.114.114: seq=1 ttl=69 time=19.639 ms
64 bytes from 114.114.114.114: seq=2 ttl=69 time=19.517 ms
64 bytes from 114.114.114.114: seq=3 ttl=84 time=19.620 ms--- 114.114.114.114 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 19.517/19.630/19.744 ms

能 ping 通则说明可以访问外部网络,如果 ping 没反应,则需要检查宿主机配置。

1)需要宿主机进行转发功能

# 检查是否开启
root@mydocker:~/feat-network-2/mydocker# sysctl net.ipv4.conf.all.forwarding
net.ipv4.conf.all.forwarding = 0 # 为 0 则说明未开启
# 执行以下命令设置为 1 开启转发功能
sysctl net.ipv4.conf.all.forwarding=1

2)检查 iptables FORWARD 规则

$ iptables -t filter -L 
FORWARD Chain FORWARD (policy ACCEPT) target     prot opt source               destination 

一般缺省策略都是 ACCEPT,不需要改动,如果如果缺省策略是DROP,需要设置为ACCEPT

iptables -t filter -P FORWARD ACCEPT

开启后基本上就可以正常运行了。

容器映射端口到宿主机上供外部访问

数据流向:SNAT --> Bridge -> Veth。

访问宿主机 IP:8080 SNAT 后变成容器 IP:80,然后进入到 Bridge,接着进入容器内 Veth。

对应的 DNAT 就是下面这条 Iptables 规则

iptables -t nat -A PREROUTING ! -i testbridge -p tcp -m tcp --dport 8080 -j DNAT --to-destination 10.0.0.2:80

通过 mydocker run -p 8080:80 的方式将容器中的 80 端口映射到宿主机 8080 端口。

./mydocker run -it -p 8080:80 -net testbridge busybox sh
# 容器中使用nc命令监听 80 端口
/ # nc -lp 80

然后访问宿主机的 8080 端口,看是否能转发到容器中。

注意:需要到和宿主机同一个局域网的远程主机上访问,而且要使用 宿主机 IP 进行访问。

[root@kc-jumper ~]# telnet 192.168.10.144 8080
Trying 192.168.10.144...
Connected to 192.168.10.144.
Escape character is '^]'.

连上则说明是可以的。

因为只在 PREROUTING 链上做了 DNAT,因此需要同局域网的其他主机访问才行,要当前宿主机访问则需要再 OUTPUT 链也做 DNAT。

因为访问本地服务不会走 PREROUTING、INPUT 链,直接走的是 OUTPUT 链,因此要在 OUTPUT 链也增加 DNAT。

iptables 规则如下:

iptables -t nat -A OUTPUT -p tcp -m tcp --dport 8080 -j DNAT --to-destination 10.0.0.2:80

添加后就可以连上了

root@mydocker:~/feat-network-2/mydocker# telnet 192.168.10.144 8080
Trying 192.168.10.144...
Connected to 192.168.10.144.
Escape character is '^]'.

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


6. 小结

本章实现了容器网络模型构建,为容器插上了“网线”,实现了容器访问外网以及容器间访问。

包括以下工作:

  • 负载 IP 管理的 IPAM 组件

  • 以及网络管理的 NetworkDriver 组件。

  • mydocker network create/delete 命令,实现网络管理

  • mydocker run -net 参数,将容器加入到指定网络中

实际上是使用 Linux Veth、Bridge、Iptables 等相关技术实现,具体原理:

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

现在再看一下这个网络拓扑应该就比较清晰了:

Docker Bridge 网络拓扑

最后再次推荐一下 Docker教程(十)—揭秘 Docker 网络:手动实现 Docker 桥接网络,可以和本文对照着看。


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

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

需要提前在 /var/lib/mydocker/image 目录准备好 busybox.tar 文件,具体见第四篇第二节。

# 克隆代码
git clone -b feat-network-2 https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试 
# 创建网络
./mydocker network create --driver bridge --subnet 10.0.0.1/24 testbridge
# 指定网络创建容器
./mydocker run -it -net testbridge busybox sh

最后,本文仅为个人理解,可能存在一些错误或者不完善之处。如果大家发现了任何问题或者有任何建议,一定帮忙指正,一起探讨,共同提高。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

BERT应用——文本间关联性分析

本文结合了自然语言处理&#xff08;NLP&#xff09;和深度学习技术&#xff0c;旨在分析一段指定的任务文本中的动词&#xff0c;并进一步探讨这个动词与一系列属性之间的关联性。具体技术路径包括文本的词性标注、语义编码和模型推断。 一、技术思路 NLP和词性标注 在自然…

独著出书的出版流程是怎样的?

独著出书的出版流程一般包括以下几个步骤&#xff1a; 1. 准备书稿&#xff1a;确保书稿内容完整、准确&#xff0c;并符合出版社的要求。 2. 选择出版社&#xff1a;根据书稿的主题和内容&#xff0c;选择合适的出版社。可以考虑出版社的专业性、声誉和出版范围等因素。 3.…

MySQL Shell 使用指南

前言&#xff1a; MySQL Shell 是官方提供的 MySQL 周边适配组件&#xff0c;是新一代的高级客户端&#xff0c;在 MySQL 8.0 及其以后的版本得以慢慢推广应用。之前笔者因为 MySQL 8.0 用得比较少&#xff0c;一直没有详细使用过这个工具&#xff0c;近期在捣鼓 MySQL 8.0&am…

如何去掉IDEA中烦人的警告波浪线

有时候想去掉idea中那些黄色的红色的warning波浪线&#xff0c;这些不是错误&#xff0c;并不影响执行&#xff0c;一直显示显得让人很烦躁&#xff0c;去"Editor" -> "Inspections"中一个个设置很麻烦。 可以通过设置代码检测级别来降低代码检查的严格…

ChatGPT Prompt技术全攻略-入门篇:AI提示工程基础

系列篇章&#x1f4a5; No.文章1ChatGPT Prompt技术全攻略-入门篇&#xff1a;AI提示工程基础2ChatGPT Prompt技术全攻略-进阶篇&#xff1a;深入Prompt工程技术3ChatGPT Prompt技术全攻略-高级篇&#xff1a;掌握高级Prompt工程技术4ChatGPT Prompt技术全攻略-应用篇&#xf…

文献解读-肿瘤测序-第六期|《基于CRISPR/Cas9技术的肿瘤突变负荷测量新参考物质的开发》

关键词&#xff1a;肿瘤测序&#xff1b;基因组变异检测&#xff1b; 文献简介 标题&#xff08;英文&#xff09;&#xff1a;Development of a Novel Reference Material for Tumor Mutational Burden Measurement Based on CRISPR/Cas9 Technolog标题&#xff08;中文&…

【协同感知】Collaborative Perception in Autonomous Driving数据集与论文整理

Collaborative Perception in Autonomous Driving 目前最全的Collaborative Perception整理数据集协同感知论文-【三维目标检测】现实世界下的协同感知理想条件下的协同感知 目前最全的Collaborative Perception整理 https://github.com/Little-Podi/Collaborative_Perception…

【探索全球精彩瞬间,尽享海外短剧魅力!海外短剧系统,您的专属观影平台】

&#x1f31f; 海外短剧系统&#xff0c;带您走进一个全新的视界&#xff0c;让您随时随地欣赏到来自世界各地的精选短剧。在这里&#xff0c;您可以感受到不同文化的碰撞&#xff0c;品味到各种题材的精髓&#xff0c;让您的生活更加丰富多彩&#xff01; &#x1f3ac; 精选…

【Python】【Pyinstaller】打包过程问题记录及解决

一、写在前面 将python脚本打包成.exe可执行文件&#xff0c;使用windows电脑运行。 所需库&#xff1a;pyinstaller 官网链接 命令格式&#xff1a; pyinstaller -F -w (需要打包的文件&#xff0c;文件名之间用空格分隔&#xff09;二、打包步骤&#xff08;见图片&#x…

自费出书一般需要多少钱?

自费出书的费用因多种因素而异&#xff0c;包括书号费、审稿费、排版费、封面设计费、纸张及印刷费、仓储物流费等。 以下是一些常见的费用项目和大致价格范围&#xff1a; - 书号费&#xff1a;书号是出版社的特有资源&#xff0c;书号费用在2023年又进一步上涨。目前&#…

Windows文件管理器导航窗口怎么删除第三方生成的无效导航【笔记】

Windows文件管理器导航窗口怎么删除第三方生成的无效导航【笔记】 导航窗口对应项目没有右击删除选项。 提示&#xff1a; 位置不可用 C:\Users\superman…不可用&#xff0c;如果该位置位于这台电脑上&#xff0c;请确保设备或驱动器连接&#xff0c;或者光盘已插入&#xf…

【恶补计算机基础】定点数和浮点数

在计算机中&#xff0c;小数点及其位置并不是显式表示出来的&#xff0c;而是隐含规定的。根据小数点的位置&#xff0c;可以分为两类&#xff1a;定点数和浮点数。 1 定点数 小数点的位置是固定不变的。根据小数点的具体位置&#xff0c;又可以分为两类&#xff1a;定点小数…

Linux-vi编辑器命令使用

一、初始-vi 1、 vi-打开文件并且定位行 有可能会遇到打开一个文件&#xff0c;并定位到指定行的情况 例如&#xff0c;知道某一行代码有错误&#xff0c;可以快速定位到出错代码的位置 可以使用以下命令打开文件$ vi 文件名 行数 提示&#xff1a;如果只带上 而不指定行号&…

Python邮件群发有哪些步骤?如何批量发送?

Python邮件群发的注意事项&#xff1f;怎么使用Python群发邮件&#xff1f; 使用Python进行邮件群发&#xff0c;不仅可以自动化流程&#xff0c;还可以节省大量的时间和精力。AokSend将详细介绍使用Python进行邮件群发的步骤&#xff0c;并在过程中提供实用的建议和注意事项。…

windows下使用命令清空U盘

1、CMD命令打开后输入diskpart命令打开磁盘分区管理工具 diskpart打开如下窗口 Microsoft DiskPart 版本 10.0.19041.3636 Copyright (C) Microsoft Corporation. 在计算机上: DESKTOP-TR9HQRP 2、输入查看所有磁盘命令 list disk打印如下windows 磁盘 ###  状态    …

机械臂码垛机:解读其高效作业与灵活性

在当今高度自动化的工业时代&#xff0c;机械臂码垛机以其高效作业和灵活性&#xff0c;成为了生产线上的得力助手。这款设备不仅大幅提升了生产效率&#xff0c;还显著降低了人工操作的强度和风险&#xff0c;为现代工业发展注入了强大的动力。 机械臂码垛机的高效作业能力令人…

【机器学习】必会降维算法之:奇异值分解(SVD)

奇异值分解&#xff08;SVD&#xff09; 1、引言2、奇异值分解&#xff08;SVD&#xff09;2.1 定义2.2 应用场景2.3 核心原理2.4 算法公式2.5 代码示例 3、总结 1、引言 一转眼&#xff0c; 小屌丝&#xff1a;鱼哥&#xff0c;就要到每年最开心的节日了&#xff1a;六一儿童…

搭建Vulnhub靶机网络问题(获取不到IP)

搭建好靶场后&#xff0c;在攻击机运行arp-scan -l无法发现靶机IP。 这时候去看下靶机网络有没有问题。 重新启动客户机&#xff0c;一直按e进入安全模式&#xff08;要是直接开机了就先按shift进入grub界面&#xff0c;再按e&#xff09;找到ro&#xff0c;将ro改为rw signie…

XM平台的交易模式模式是什么?

外汇交易平台的盈利模式主要分为两种&#xff1a;有交易员平台和无交易员平台。 有交易员平台&#xff0c;也称为做市商平台&#xff0c;为客户提供交易市场&#xff0c;并在需要时与客户持相反方向的交易&#xff0c;从中赚取利润。交易者看到的买入卖出价可能与实际价格不同&…

python 巡检报告中的邮件处理

00.创作背景,在每天的巡检报告中要 要检查oa相关服务器的备份作业是否备份成功 那个备份软件有个功能&#xff0c;就是完成备份作业后&#xff0c;可以发送信息到我的邮箱。 01.通过检查我邮箱的信息&#xff0c;就可以了解那个备份作业的情况。 通过解释邮件的名称可以了解备…