本文将带领读者探索 Docker 桥接网络模型的内部机制,通过手动实现 veth pair、bridge、iptables 等关键技术,揭示网络背后的运作原理。


如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【探索云原生】即可订阅


1. 概述

Docker 有多种网络模型,对于单机上运行的多个容器,可以使用缺省的 bridge 网络驱动

我们按照下图创建网络拓扑,让容器之间网络互通,从容器内部可以访问外部资源,同时,容器内可以暴露服务让外部访问。

桥接网络的一个拓扑结构如下:

![Docker Bridge 网络拓扑][docker bridge 网络拓扑]

上述网络拓扑实现了:让容器之间网络互通,从容器内部可以访问外部资源,同时,容器内可以暴露服务让外部访问。

根据网络拓扑图可以看到,容器内的数据通过 veth pair 设备传递到宿主机上的网桥上,最终通过宿主机的 eth0 网卡发送出去(或者再通过 veth pair 进入到另外的容器),而接收数据的流程则恰好相反。

2. 预备知识

这里对本文会用到的相关网络知识做一个简单介绍。

veth pair

相关笔记: veth-pair

Veth是成对出现的两张虚拟网卡,从一端发送的数据包,总会在另一端接收到

利用Veth的特性,我们可以将一端的虚拟网卡"放入"容器内,另一端接入虚拟交换机。这样,接入同一个虚拟交换机的容器之间就实现了网络互通。

即:通过 veth 来突破 network namespace 的封锁

bridge

相关笔记:Linux bridge

我们可以认为Linux bridge就是虚拟交换机,连接在同一个bridge上的容器组成局域网,不同的bridge之间网络是隔离的。

docker network create [NETWORK NAME]实际上就是创建出虚拟交换机。

交换机是工作在数据链路层的网络设备,它转发的是二层网络包。最简单的转发策略是将到达交换机输入端口的报文,广播到所有的输出端口。当然更好的策略是在转发过程中进行学习,记录交换机端口和 MAC 地址的映射关系,这样在下次转发时就能够根据报文中的 MAC 地址,发送到对应的输出端口。

NAT

相关笔记:iptables

NAT(Network Address Translation),是指网络地址转换。

因为容器中的 IP 和宿主机的 IP 是不一样的,为了保证发出去的数据包能正常回来,需要对 IP 层的源 IP/目的 IP 进行转换。

  • SNAT:源地址转换
  • DNAT:目的地址转换

SNAT

Source Network Address Translation,源地址转换,用于修改数据包中的源地址。

比如上图中的 eth0 ip 是 183.69.215.18,而容器 dockerA 的 IP 却是 172.187.0.2

因此容器中发出来的数据包,源IP肯定是 172.187.0.2,如果就这样不处理直接发出去,那么接收方处理后发回来的响应数据包的 目的IP 自然就会填成 172.187.0.2,那么我们肯定接收不到这个响应了。

因此在将容器中的数据包通过 eth0 网卡发送出去之前,需要进行 SNAT 把源 ip 改为 eth0 的 ip,也就是 183.69.215.18

这样接收方响应时将源 IP 183.69.215.18 作为目的 IP,这样我们才能收到返回的数据。

DNAT

Destination Network Address Translation:目的地址转换,用于修改数据包中的目的地址。

如果发出去做了 SNAT,源 IP 改成了宿主机的 183.69.215.18,那么回来的响应数据包目的 IP 自然就是183.69.215.18,我们(宿主机)可以成功收到这个响应。

但是如果直接把源 IP 是183.69.215.18的数据包发到容器里面去,由于容器 IP 是172.187.0.2,那肯定不会处理这个包,所以宿主机收到响应包需要进行 DNAT,将目的 IP 地址从 183.69.215.18 改成容器中的172.187.0.2

这样容器才能正常处理该数据。

3. 演示

实验环境 Ubuntu 20.04

环境准备

首先需要创建对应的容器,veth pair 设备以及 bridge 设备 并分配对应 IP。

创建“容器”

todo

从前面的背景知识了解到,容器的本质是 Namespace + Cgroups + rootfs。因此本实验我们可以仅仅创建出Namespace网络隔离环境来模拟容器行为:

$ sudo ip netns add ns1
$ sudo ip netns add ns2
$ sudo ip netns show
ns2
ns1

创建 Veth pairs

sudo ip link add veth0 type veth peer name veth1
sudo ip link add veth2 type veth peer name veth3

查看一下:

$ 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:9b:9b:33 brd ff:ff:ff:ff:ff:ff
3: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 9a:45:4c:f9:77:eb brd ff:ff:ff:ff:ff:ff
4: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether fe:5a:a1:3b:94:9b brd ff:ff:ff:ff:ff:ff
5: veth3@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 96:d2:e4:ea:9a:1d brd ff:ff:ff:ff:ff:ff
6: veth2@veth3: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000

将 Veth 的一端放入“容器”

将 veth 的一端移动到对应的 Namespace 就相当于把这张网卡加入到’容器‘里了。

sudo ip link set veth0 netns ns1
sudo ip link set veth2 netns ns2

查看宿主机上的网卡

$ 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:9b:9b:33 brd ff:ff:ff:ff:ff:ff
3: veth1@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 9a:45:4c:f9:77:eb brd ff:ff:ff:ff:ff:ff link-netns ns1
5: veth3@if6: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 96:d2:e4:ea:9a:1d brd ff:ff:ff:ff:ff:ff link-netns ns2

发现少了两个,然后进入容器对应Namespace查看一下容器中的网卡:

$ sudo ip netns exec ns1 ip link show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: veth0@if3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether fe:5a:a1:3b:94:9b brd ff:ff:ff:ff:ff:ff link-netnsid 0
$ sudo ip netns exec ns2 ip link show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
6: veth2@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 8e:6a:e4:a0:50:ce brd ff:ff:ff:ff:ff:ff link-netnsid 0

可以看到,veth0veth2确实已经放到“容器”里去了。

创建 bridge

一般使用brctl进行管理,不是自带的工具,需要先安装一下:

sudo apt-get install bridge-utils

创建 bridge br0

sudo brctl addbr br0
  • 将 Veth 的另一端接入 bridge
sudo brctl addif br0 veth1
sudo brctl addif br0 veth3

查看接入效果:

$ sudo brctl show
bridge name bridge id STP enabled interfaces
br0 8000.361580fa3c8b no veth1
veth3

可以看到,两个网卡veth1veth3已经“插”在bridge上。

至此,veth pair 已经一端在容器里,一端在宿主机网桥上了,大致拓扑结构完成。

分配 IP 并启动

  • 为 bridge 分配 IP 地址,激活上线
sudo ip addr add 172.18.0.1/24 dev br0
sudo ip link set br0 up
  • 为"容器“内的网卡分配 IP 地址,并激活上线

docker0 容器:

sudo ip netns exec ns1 ip addr add 172.18.0.2/24 dev veth0
sudo ip netns exec ns1 ip link set veth0 up

docker1 容器:

sudo ip netns exec ns2 ip addr add 172.18.0.3/24 dev veth2
sudo ip netns exec ns2 ip link set veth2 up
  • Veth 另一端的网卡激活上线
sudo ip link set veth1 up
sudo ip link set veth3 up

至此,整个拓扑结构搭建完成,且所有设备都分配好 ip 并上线。

测试

容器互通

测试从容器docker0 ping 容器docker1,测试之前先用 tcpdump 抓包,等会好分析:

sudo tcpdump -i br0 -n

在新窗口执行 ping 命令:

sudo ip netns exec ns1 ping -c 3 172.18.0.3

br0上的抓包数据如下:

tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
12:35:18.285705 ARP, Request who-has 172.18.0.3 tell 172.18.0.2, length 28
12:35:18.285903 ARP, Reply 172.18.0.3 is-at e2:31:15:64:bd:39, length 28
12:35:18.285908 IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 13829, seq 1, length 64
12:35:18.286034 IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 13829, seq 1, length 64
12:35:19.309392 IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 13829, seq 2, length 64
12:35:19.309589 IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 13829, seq 2, length 64
12:35:20.349350 IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 13829, seq 3, length 64
12:35:20.349393 IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 13829, seq 3, length 64
12:35:23.309404 ARP, Request who-has 172.18.0.2 tell 172.18.0.3, length 28
12:35:23.309517 ARP, Reply 172.18.0.2 is-at 2e:93:7e:33:b0:ed, length 28

可以看到,先是172.18.0.2发起的ARP请求,询问172.18.0.3MAC地址,然后是ICMP的请求和响应,最后是172.18.0.3的 ARP 请求。

因为接在同一个 bridge br0上,所以是二层互通的局域网。

同样,从容器docker1 ping 容器docker0也是通的:

sudo ip netns exec ns2 ping -c 3 172.18.0.2

宿主机访问容器

在“容器”docker0内启动服务,监听 80 端口:

sudo ip netns exec ns1 nc -lp 80

在宿主机上执行 telnet,可以连接到docker0的 80 端口:

$ telnet 172.18.0.2 80
Trying 172.18.0.2...
Connected to 172.18.0.2.
Escape character is '^]'.

可以联通。

容器访问外网

这部分稍微复杂一些,需要配置 NAT 规则。

1)配置容器内路由

需要配置容器内的路由,这样才能把网络包从容器内转发出来。

具体就是:将 bridge 设置为“容器”的缺省网关。让非172.18.0.0/24网段的数据包都路由给bridge,这样数据就从“容器”跑到宿主机上来了。

sudo ip netns exec ns1 ip route add default via 172.18.0.1 dev veth0
sudo ip netns exec ns2 ip route add default via 172.18.0.1 dev veth2

查看“容器”中的路由规则

$ sudo ip netns exec ns1 ip route
default via 172.18.0.1 dev veth0
172.18.0.0/24 dev veth0 proto kernel scope link src 172.18.0.2

可以看到,非 172.18.0.0 网段的数据都会走默认规则,也就是发送给网关 172.18.0.1。

2)宿主机开启转发功能并配置转发规则

在宿主机上配置内核参数,允许 IP forwarding,这样才能把网络包转发出去。

sudo sysctl net.ipv4.conf.all.forwarding=1

还有就是要配置 iptables FORWARD 规则

首先确认iptables FORWARD的缺省策略:

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

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

sudo iptables -t filter -P FORWARD ACCEPT

3)宿主机配置 SNAT 规则

sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/24 ! -o br0 -j MASQUERADE

上面的命令的含义是:在nat表的POSTROUTING链增加规则,当数据包的源地址为172.18.0.0/24网段,出口设备不是br0时,就执行MASQUERADE动作。

MASQUERADE也是一种源地址转换动作,它会动态选择宿主机的一个 IP 做源地址转换,而SNAT动作必须在命令中指定固定的 IP 地址。

测试能否访问外网:

$ sudo ip netns exec ns1 ping -c 3 114.114.114.114
PING 114.114.114.114 (114.114.114.114) 56(84) bytes of data.
64 bytes from 114.114.114.114: icmp_seq=1 ttl=80 time=21.1 ms
64 bytes from 114.114.114.114: icmp_seq=2 ttl=89 time=19.5 ms
64 bytes from 114.114.114.114: icmp_seq=3 ttl=86 time=19.2 ms

外部访问容器

外部访问容器需要进行 DNAT,把目的 IP 地址从宿主机地址转换成容器地址。

sudo iptables -t nat -A PREROUTING  ! -i br0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.18.0.2:80

上面命令的含义是:在nat表的PREROUTING链增加规则,当输入设备不是br0,目的端口为 80 时,做目的地址转换,将宿主机 IP 替换为容器 IP。

测试一下

在“容器”docker0 内启动服务:

sudo ip netns exec ns1 nc -lp 80

和宿主机同一个局域网的远程主机访问宿主机 IP:80

telnet 192.168.2.110 80

确认可以访问到容器内启动的服务。

不过由于只在PREROUTING 链上做了 DNAT,因此直接在宿主机上访问是不行,需要本机访问的话可以添加下面这个 iptables 规则,直接在 OUTPUT 链上增加 DNAT 规则:

这样其他节点来的流量和本机直接访问流量都可以正常 DNAT 了。

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

添加后再本机直接测试:

telnet 192.168.2.110 80

这下可以成功连上了。

环境恢复

删除虚拟网络设备

sudo ip link set br0 down
sudo brctl delbr br0
sudo ip link del veth1
sudo ip link del veth3

iptablersNamesapce的配置在机器重启后被清除。

4. 小结

本文主要通过 Linux 提供的各种虚拟设备以及 iptables 模拟出了 docker bridge 网络模型,并测试了几种场景的网络互通。实际上docker network 就是使用了vethLinux bridgeiptables等技术,帮我们创建和维护网络。

具体分析一下:

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

如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【探索云原生】即可订阅


5. 参考

iptables 笔记

veth-pair 笔记

Docker bridge networks

Docker 单机网络模型动手实验

揭秘 Docker 网络:手动实现 Docker 桥接网络的更多相关文章

  1. docker从零开始网络(二)桥接网络

    使用桥接网络 在网络方面,桥接网络是链路层设备,它在网络段之间转发流量.桥接网络可以是硬件设备或在主机内核中运行的软件设备. 就Docker而言,桥接网络使用软件桥接器,该软件桥接器允许连接到同一桥接 ...

  2. Centos7.x Docker桥接网络

    基于Centos7.x构建Docker桥接网络, 配置bridge桥接网络可以直接设置网卡配置文件: 自定义桥接网络设置如下: 关掉docker0 ifconfig docker0 down 删除do ...

  3. docker 设置固定ip、配置网络

    Docker安装后,默认会创建下面三种网络类型 $ docker network ls NETWORK ID NAME DRIVER SCOPE 9781b1f585ae bridge bridge ...

  4. Docker Centos7 下建立 Docker 桥接网络

    为什么要让docker桥接物理网络? docker默认提供了一个隔离的内网环境,启动时会建立一个docker0的虚拟网卡,每个容器都是连接到docker0网卡上的.而docker0的ip段为172.1 ...

  5. docker 配置桥接网络

    2.5 docker配置桥接网络(上): 为了使本地网络中的机器和Docker 容器更方便的通信,我们经常会有将Docker容器 配置到和主机同一网段的需求. 这个需求其实很容器实现, 我们只需要将D ...

  6. 使用Docker的macvlan为容器提供桥接网络及跨主机通讯

    对于了解Docker容器网络的朋友,我想对虚拟机的网络也不会陌生,毕竟我们是跟随这个时代一起学习和进步的人.相比VM,Docker的网络也在逐步走向成熟,本文主要针对其中的macvlan做下简单的介绍 ...

  7. 安装docker后,导致qemu的桥接网络出现问题

    按照Qemu-4.1 桥接网络设置中介绍的方法建立起桥接网络后,可以实现虚拟机和host的相互ping,但是在虚拟机里去ping其他跟host处于同一个网段的ip地址时却失败了,然后ifconfig后 ...

  8. Docker默认桥接网络是如何工作的

    1. 启动一个Docker容器 一般来说,我们起一个容器比如一个简单的nginx服务会向这样 docker run -d --rm nginx:XXX OK容器起来了,但是并不能通过宿主机被外面的机器 ...

  9. Docker之 默认桥接网络与自定义桥接网卡

    docker引擎会默认创建一个docker0网桥,它在内核层连通了其他的物理或虚拟网卡,这就将所有容器和宿主机都放到同一个二层网络. 1. docker如何使用网桥 1.1 Linux虚拟网桥的特点 ...

  10. docker(4)docker的网络,自定义网桥

    Docker 的网络 运行 ifconfig 找到 docker0 : 虚拟网卡默认网卡名称为docker0 查看docker 的网桥: 我这里默认们没有进行安装 网桥管理设备:进行安装一下: yum ...

随机推荐

  1. Hash-based Message Authentication Code(HMAC)

    一.引言 在现代信息安全领域,消息认证码(Message Authentication Code,简称MAC)起着至关重要的作用.Hash-based Message Authentication C ...

  2. Windows下使用图形化的Havoc C2

    Windows下使用图形化的Havoc C2 前言 这几天用goland去连虚拟机去coding真的心累,想着搞个wsl算了虽然测试又变麻烦了(wsl2和VMware一起开有问题,可能保存不了快照), ...

  3. Python——第二章:列表的增、删、改、查

    列表的添加 .append() lst = [] # 向列表末尾添加内容 # append() 追加 lst.append("张绍刚") lst.append("赵本山& ...

  4. 终于卷完了!MySQL 打怪升级进阶成神之路(2023 最新版)!

    从第一篇文章开始,我们逐步详细介绍了 MySQL 数据库的基础知识,如:数据类型.存储引擎.性能优化(软.硬及sql语句),MySQL 数据库的高可用架构的部分,如:主从同步.读写分离的原理与实践.跨 ...

  5. Zookeeper 的基本使用

    维基百科对 Zookeeper 的介绍如下所示: Apache ZooKeeper是 Apache 软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务.同步服务和命名注册 ZooKe ...

  6. [极客大挑战 2019]Havefun 1

    [极客大挑战 2019]Havefun 1 一,审题,观察题目信息和知识点 观察题目,没发现有效信息 ​ F12打开源代码,发现有一个GET传输. ​ 知识点 GET方法的数据传输是通过URL传输的, ...

  7. 斯坦福 UE4 C++ ActionRoguelike游戏实例教程 09.第二个游戏规则:玩家重生

    斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论 概述 本文对应课程15章,60 - Refining Player Respawns. 在本篇文章中,将会为游戏新增加 ...

  8. 2023-06-04:你的音乐播放器里有 N 首不同的歌, 在旅途中,你的旅伴想要听 L 首歌(不一定不同,即,允许歌曲重复, 请你为她按如下规则创建一个播放列表, 每首歌至少播放一次, 一首歌只有在

    2023-06-04:你的音乐播放器里有 N 首不同的歌, 在旅途中,你的旅伴想要听 L 首歌(不一定不同,即,允许歌曲重复, 请你为她按如下规则创建一个播放列表, 每首歌至少播放一次, 一首歌只有在 ...

  9. 面试官:String长度有限制吗?是多少?还好我看过

    前言 话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了,本人就遇到过面试的时候问这个的 ...

  10. 第三部分_Shell脚本简单四则运算

    简单四则运算 算术运算:默认情况下,shell就只能支持简单的整数运算 运算内容:加(+).减(-).乘(*).除(/).求余数(%) 1. 四则运算符号 表达式 举例 $(( )) | echo $ ...