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

2024-06-05 14:12

本文主要是介绍从零开始写 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

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

这篇关于从零开始写 Docker(十七)---容器网络实现(中):为容器插上”网线“的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1033277

相关文章

C++对象布局及多态实现探索之内存布局(整理的很多链接)

本文通过观察对象的内存布局,跟踪函数调用的汇编代码。分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等 文章链接:http://dev.yesky.com/254/2191254.shtml      论C/C++函数间动态内存的传递 (2005-07-30)   当你涉及到C/C++的核心编程的时候,你会无止境地与内存管理打交道。 文章链接:http://dev.yesky

【Altium】查找PCB上未连接的网络

【更多软件使用问题请点击亿道电子官方网站】 1、文档目标: PCB设计后期检查中找出没有连接的网络 应用场景:PCB设计后期,需要检查是否所有网络都已连接布线。虽然未连接的网络会有飞线显示,但是由于布线后期整板布线密度较高,虚连,断连的网络用肉眼难以轻易发现。用DRC检查也可以找出未连接的网络,如果PCB中DRC问题较多,查找起来就不是很方便。使用PCB Filter面板来达成目的相比DRC

LeetCode11. 盛最多水的容器题解

LeetCode11. 盛最多水的容器题解 题目链接: https://leetcode.cn/problems/container-with-most-water 示例 思路 暴力解法 定住一个柱子不动,然后用其他柱子与其围住面积,取最大值。 代码如下: public int maxArea1(int[] height) {int n = height.length;int

通过SSH隧道实现通过远程服务器上外网

搭建隧道 autossh -M 0 -f -D 1080 -C -N user1@remotehost##验证隧道是否生效,查看1080端口是否启动netstat -tuln | grep 1080## 测试ssh 隧道是否生效curl -x socks5h://127.0.0.1:1080 -I http://www.github.com 将autossh 设置为服务,隧道开机启动

通信系统网络架构_2.广域网网络架构

1.概述          通俗来讲,广域网是将分布于相比局域网络更广区域的计算机设备联接起来的网络。广域网由通信子网于资源子网组成。通信子网可以利用公用分组交换网、卫星通信网和无线分组交换网构建,将分布在不同地区的局域网或计算机系统互连起来,实现资源子网的共享。 2.网络组成          广域网属于多级网络,通常由骨干网、分布网、接入网组成。在网络规模较小时,可仅由骨干网和接入网组成

Ubuntu20.04离线安装Docker

1.下载3个docker离线安装包,下载网址: https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/ 2.把3个离线安装包拷贝到ubuntu本地执行以下命令 sudo dpkg -i containerd.io_1.4.6-1_amd64.deb sudo dpkg -i docker-ce-c

时序预测 | MATLAB实现LSTM时间序列未来多步预测-递归预测

时序预测 | MATLAB实现LSTM时间序列未来多步预测-递归预测 目录 时序预测 | MATLAB实现LSTM时间序列未来多步预测-递归预测基本介绍程序设计参考资料 基本介绍 MATLAB实现LSTM时间序列未来多步预测-递归预测。LSTM是一种含有LSTM区块(blocks)或其他的一种类神经网络,文献或其他资料中LSTM区块可能被描述成智能网络单元,因为

亮相WOT全球技术创新大会,揭秘火山引擎边缘容器技术在泛CDN场景的应用与实践

2024年6月21日-22日,51CTO“WOT全球技术创新大会2024”在北京举办。火山引擎边缘计算架构师李志明受邀参与,以“边缘容器技术在泛CDN场景的应用和实践”为主题,与多位行业资深专家,共同探讨泛CDN行业技术架构以及云原生与边缘计算的发展和展望。 火山引擎边缘计算架构师李志明表示:为更好地解决传统泛CDN类业务运行中的问题,火山引擎边缘容器团队参考行业做法,结合实践经验,打造火山

vue项目集成CanvasEditor实现Word在线编辑器

CanvasEditor实现Word在线编辑器 官网文档:https://hufe.club/canvas-editor-docs/guide/schema.html 源码地址:https://github.com/Hufe921/canvas-editor 前提声明: 由于CanvasEditor目前不支持vue、react 等框架开箱即用版,所以需要我们去Git下载源码,拿到其中两个主

React+TS前台项目实战(十七)-- 全局常用组件Dropdown封装

文章目录 前言Dropdown组件1. 功能分析2. 代码+详细注释3. 使用方式4. 效果展示 总结 前言 今天这篇主要讲全局Dropdown组件封装,可根据UI设计师要求自定义修改。 Dropdown组件 1. 功能分析 (1)通过position属性,可以控制下拉选项的位置 (2)通过传入width属性, 可以自定义下拉选项的宽度 (3)通过传入classN