本文为从零开始写 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 用户

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] = net
return 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。

流程

具体流程如下图所示:

上图中的 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作为网关的IP
ip, 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命令删除己经创建的网络。

具体流程如下图:

分为以下几个步骤:

  • 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")
}
// 网络不存在直接返回一个error
net, ok := networks[networkName]
if !ok {
return fmt.Errorf("no Such Network: %s", networkName)
}
// 调用IPAM的实例ipAllocator释放网络网关的IP
if 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 这个网络与网络中的其他容器进行通信了。

流程

具体流程图如下:

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

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

代码实现

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

var runCommand = cli.Command{
Name: "run",
Usage: `Create a container with namespace and cgroups limit
mydocker 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 network
containerInfo := &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:80
return 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.IPRange
interfaceIP.IP = ep.IPAddress
// 设置容器内Veth端点的IP
if 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 规则

网络拓扑可以参考下图:

可以简单理解为:整个链路使用 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 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
15: cif-86416@if16: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
link/ether fe:44:02:8a:07:1f brd ff:ff:ff:ff:ff:ff
inet 10.0.0.2/24 brd 10.0.0.255 scope global cif-86416
valid_lft forever preferred_lft forever
inet6 fe80::fc44:2ff:fe8a:71f/64 scope link
valid_lft forever preferred_lft forever

可以看到,地址是,10.0.0.2。

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

/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
17: cif-12201@if18: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
link/ether 42:da:b3:b8:6f:6a brd ff:ff:ff:ff:ff:ff
inet 10.0.0.3/24 brd 10.0.0.255 scope global cif-12201
valid_lft forever preferred_lft forever
inet6 fe80::40da:b3ff:feb8:6f6a/64 scope link
valid_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教程(十)---揭秘 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(十七)---容器网络实现(中):为容器插上”网线“的更多相关文章

  1. Docker学习笔记 - 在运行中的容器内启动新进程

    docker psdoker top dc1 # 容器情况# 在运行中的容器内启动新进程docker exec [-d] [-i] [-t] 容器名 [command] [args]docker ex ...

  2. 用java网络编程中的TCP方式上传文本文件及出现的小问题

    自己今天刚学java网络编程中的TCP传输,要用TCP传输文件时,自己也是遇到了一些问题,抽空把它整理了一下,供自己以后参考使用. 首先在这个程序中,我用一个客户端,一个服务端,从客户端上传一个文本文 ...

  3. 一文读懂 Kubernetes 容器网络

    点击上方"开源Linux",选择"设为星标" 回复"学习"获取独家整理的学习资料! 在Kubernetes中要保证容器之间网络互通,网络至关 ...

  4. Docker学习笔记之运行和管理容器

    0x00 概述 容器是基于容器技术所建立和运行的轻量级应用运行环境,它是 Docker 封装和管理应用程序或微服务的“集装箱”.在 Docker 中,容器算是最核心的部分了,掌握容器的操作也是 Doc ...

  5. Docker系列02: 容器生命周期管理 镜像&容器

    A) Docker信息1. 查看docker运行状态 systemctl status docker docker.service - Docker Application Container Eng ...

  6. 为啥Underlay才是容器网络的最佳落地选择

    导语: 几年前,当博云启动自研容器网络研发的时候,除了技术选型的考虑,我们对于先做 Underlay 还是 Overlay 网络也有过深度的讨论.当时的开源社区以及主流容器厂商,多数还是以 Overl ...

  7. 腾讯云容器服务 TKE 推出新一代零损耗容器网络

    随着容器技术的发展成熟,越来越多的组件迁移到容器,在技术迁移过程中,数据库,游戏,AI 这些组件对容器网络性能(时延,吞吐,稳定性)提出了更高的要求.为了得到更优的时延和吞吐表现,各大云厂商都在致力于 ...

  8. 004-docker命令-容器生命周期管理、容器操作

    1.容器生命周期管理 docker run :创建一个新的容器并运行一个命令 语法:docker run [OPTIONS] IMAGE [COMMAND] [ARG...] OPTIONS说明: - ...

  9. 宿主机网络中其它机器与Docker容器网络互通配置

    前言 目前项目采用微服务架构进行开发,Nacos和其它服务部署到Docker中,Docker中容器采用的网络默认是桥接模式(Bridge),默认的子网码是172.17.0.1/16:宿主机是192.1 ...

  10. 理解Docker(6):若干企业生产环境中的容器网络方案

    本系列文章将介绍 Docker的相关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 Linux namespace 隔离容器的运行环境 ...

随机推荐

  1. 物联网浏览器(IoTBrowser)-整合机器学习yolo框架实现车牌识别

    最近一段时间在研究AI技术在.Net平台的使用,目前AI绝大部分是使用Python开发,偶然一次在头条看到一篇ML.NET的介绍,是Net平台下开放源代码的跨平台机器学习框架.ML.NET详细介绍 h ...

  2. 【GDKOI 2024 TG Day2】不休陀螺(top) 题解

    考虑一个卡牌区间怎样才不是"陀螺无限". 一个是费用在打到一半时费用就不够了.考虑构造一个卡牌序列使其尽量能够在打到一半时费用就不够,如何构造呢? 把 \(a_i > b_i ...

  3. SQL case when then end

    SQL case when then end sql中的返回值可以使用case when then end来进行实现 SELECT s.s_id, CASE s.s_name WHEN '1' THE ...

  4. 了解3D世界的黑魔法-纯Java构造一个简单的3D渲染引擎

    简介: 对于非渲染引擎相关工作的开发者来说,可能认为即使构建最简单的3D程序也非常困难,但事实上并非如此,本篇文章将通过简单的200多行的纯 Java代码,去实践正交投影.简单三角形光栅化.z缓冲(深 ...

  5. 入门即享受!coolbpf 硬核提升 BPF 开发效率 | 龙蜥技术

    简介: 干货必备!何为享受式开发? 编者按:BPF 技术还在如火如荼的发展着,本文先通过对 BPF 知识的介绍,带领大家入门 BPF,然后介绍 coolbpf 的远程编译(原名 LCC,LibbpfC ...

  6. 【全观测系列】Elasticsearch应用性能监控最佳实践

    ​简介:本文介绍了应用性能监控的应用价值以及解决方案等. 1.什么是全观测? 要了解全观测,我们先看看传统运维存在哪些问题. 数据孤岛,分散在不同部门,分析排查故障困难: 多个厂商的多种工具,无法自动 ...

  7. 5分钟搞定AlertManager接入短信、语音等10+种通知渠道

    ​简介: Alert Manager是开源监控系统Prometheus中用于处理告警信息的服务,通过将日志服务开放告警配置为Alert Manager中的一个Receiver,可以将Alert Man ...

  8. IIncrementalGenerator 获取引用程序集的所有类型

    本文告诉大家如何在使用 IIncrementalGenerator 进行增量的 Source Generator 生成代码时,如何获取到当前正在分析的程序集所引用的所有的程序集,以及引用的程序集里面的 ...

  9. WPF 通过 RawInput 获取触摸消息

    触摸在 Windows 下属于比较特殊的输入,不同于键盘和鼠标,键盘和鼠标可以通过全局 Hook 的方式获取到鼠标和键盘的输入消息.而触摸则没有直接的 Hook 的方法.如果期望自己的应用,可以在没有 ...

  10. win10 uwp 使用 XamlTreeDump 获取 XAML 树元素内容

    本文来安利大家 XamlTreeDump 库,通过这个库可以将 XAML 树上的元素转换为 json 字符串,可以用来进行 UI 单元测试 开始之前先通过 NuGet 工具安装 XamlTreeDum ...