网络名称空间
相关操作命令

运行相关命令,添加网络名称空间
ip netns add r1
ip netns add r2

在默认情况下,我们没有给这个网络名称空间添加任务网卡,那么它就只有一个网卡,那就是lo,如下:

发现没有,是因为这个网络空间没有被激活,我们使用如下命令就可以看到
ip netns exec r1 ifconfig -a

虚拟网卡
我们可以通过命令创建一对虚拟网卡
首先我们可以通过ip link help 查看相关的帮忙命令。
ip link add name veth1.1 type veth peer name veth1.2

查看我们刚才创建的那一对虚拟网卡设备。通过命令:
ip link sh

在这里,我们知道,这两个网卡都是在宿主机上,而且默认都是没有被激活的,你通过ifconfig是看不见的。

现在我可以人工的将我刚才创建的两块网卡,其中一个留在宿主机上,另一个移动到我刚才创建的网络名称空间中。
现在我们需要做的是,将veth1.2移动到网络名称空间r1中去。需要使用如下命令:
ip link set dev veth1.2 netns r1

再使用ip link show命令,可以查看当前宿主机上面的虚拟网卡,发现只有veth1.1了。

一个网络设备,只能属于一个网络名称空间。
现在我们到网络名称空间r1中去看,发现 veth1.2 已经在里面了。
通过命令:ip netns exec r1 ifconfig -a

至此,我们已经手动的将一个网卡分配给一个网络名称空间了,而这个功能是通过内核级的系统调用实现的。而且我们还可以做更多的操作,例如我们还可以将网卡进行更名。例如我们可以将r1这个网络名称空间中的veth1.2 这个网卡设备,更名为veth0。
通过命令:ip netns exec r1 ip link set dev veth1.2 name veth0

而后,你可以通过命令: ip netns exec r1 ifconfig -a 查看

激活网卡
现在我们激活宿主机上面的veth1.1这个网卡。通过如下命令:
ifconfig veth1.1 10.1.0.1/24 up ##这里我给定的网段是10.1.0.1/24

而后,你可以通过ifconifg进行查看:

我现在也把r1网络名称空间中的veth0这个网卡设备也进行激活,将网段地址设置成宿主机上面的veth1.1这个网络设备的同一个网段。那么这两个网卡设备就可以互相通信了。接下来,我们尝试一下。
激活r1网络名称空间中的veth0这个网卡设备,使用如下命令:
ip netns exec r1 ifconfig veth0 10.1.0.2/24 up

而后,我们通过命令查看一下:
ip netns exec r1 ifconfig

测试一个网络的连通性,我们直接在宿主机上直接ping 10.1.0.2

从结果上看,网络是连通的。
事实上,你也可以将veth1.1这个网卡设备,移动到 r2 这个网络名称空间中,这样的话,r1 和 r2 这两个网络名称空间就可以直接通信了。我们试一下。
运行如下命令,将veth1.1这个网卡设备移动到 r1 这个网络名称空间中。
ip link set dev veth1.1 netns r2

通过ifconfig查看到,当前宿主机上面,已经没有veth1.1这个网卡设备了。

接下来,我们运行如下命令:
ip netns exec r2 ifconfig

我们发现没有看到相关的网卡设备,记住,当我们移动网卡设备的时候,默认是没有被激活的。你可以运行如下命令查看:
ip netns exec r2 ifconfig -a

现在激活r2网络名称空间中的veth1.1这个网卡设备。
ip netns exec r2 ifconfig veth1.1 10.1.0.3/24 up

现在我们再次运行:ip netns exec r2 ifconfig 可以看到网卡设备

现在我们可以在 r2 中去 ping r1 的地址:
ip netns exec r2 ping 10.1.0.2

至此,r1 和 r2 这两个网络名称空间中的网卡,就这么手动的被我们搞通信了。
Docker 四种网络模型

none
docker run --name t1 -it --network none --rm busybox:latest

bridge
docker run --name t1 -it --network bridge --rm busybox:latest

容器间通信,多数情况下,我们都会使用到主机名,那么我们观察一下当前这个容器的主机名是什么?

70cc8117a6ce, 一个很奇怪的字符串。如果你对容器的 id 敏感的话, 你大概就可以猜测出,默认情况下,容器的主机名就是容器的id,请看下面:

但是这种主机名对于使用者不友好。所以我们可以在创建这个容器的时候,手动的指定一个主机名。例如:
docker run --name t1 -it --network bridge -h t1.npf.com --rm busybox:latest

我们再看一下 etc/resolv.conf , 当前容器所指向的DNS服务器:

从上面可以看到,默认的DNS服务器指向的是当前宿主机的dns服务器。因为我们的宿主机可以连外部的网络,那么按照道理来说,当前的容器是可以解析外部的主机名的,例如:www.baidu.com 我们可以来尝试一下:

确实如我们所料,能够解析百度的域名。所以,192.168.47.2 确实是一个 DNS 服务器。从这里,我们可以大胆的猜测,只要你能设置一个正确的 DNS 服务器,那么当前容器就能解析你想要的主机名。
接下来,我们手动的去给容器设置 DNS 服务器,我们可以使用 docker run --help 命令,去寻求帮助。

运行如下命令:
docker run --name t1 -it --network bridge -h t1.npf.com --dns 114.114.114.114 --rm busybox:latest

当然,如果你想直接通过hosts,直接去解析主机名,那么也是可以的。运行如下的命令,你就可以达到预期:
docker run --name t1 -it --network bridge -h t1.npf.com --dns 114.114.114.114 --add-host niepengfei.com 100.100.100.100 --rm busybox:latest

如何暴露端口
docker run --name npfweb --network bridge -p 80 --rm mageedu/httpd:v0.2

看样子是成功了,我们可以另开一个client查看一下,使用
docker inspect npfweb

查看到IP地址是:172.17.0.2 我们可以很容易的在宿主机上 curl 到这个容器。
curl 172.17.0.2

这个是在内部通信的,所以很容器实现,但是如果我们想在外部访问呢?该要怎么去操作?应该要访问的是 node02 这台宿主机的地址才对,也就是 192.168.47.102 。

但是我们应该访问宿主机的哪个端口呢?我们虽然在容器中暴露了 80 端口,但是这个80 被映射成宿主机的哪个端口呢?我们可以使用如下命令查看:
iptables -t nat -vnL

那么,接下来我们可以愉快的在外部访问宿主机的ip地址+端口,就可以访问到我们的容器了。如下所示:

我们之所以可以访问到,就是靠那条 nat 规则。那么那条 nat 规则是怎么生成的呢?就是你刚才创建容器的时候,加上了 -p 选项,就会自动给你生成。如果你把你的容器删除掉的话,那么这条 nat 规则也会自动被删除的。我们来测试一下。
使用 docker kill npfweb 删除这个容器:

再次查看 nat 规则:
iptables -t nat -vnL

发现刚才那条 nat 规则已经被删除掉了。
指定端口映射
如果我们想指定端口是如何映射的?该要怎么去做呢?大家应该知道,我们的宿主机上有可能存在多个IP地址的。除了 192.168.47.102 地址可用外,可能还会存在其他可用地址。默认的情况下,端口映射是在宿主机的每一个可用的IP地址的端口空间中,占用一个端口号,将其映射到容器的端口号上面去。如果我们不想使用所有的主机IP地址,而是想使用固定的IP地址,该要怎么去做呢?
我们再把刚才的那个容器启动起来,观察一下,刚才我所说的是否正确。
docker run --name npfweb --network bridge -p 80 --rm mageedu/httpd:v0.2

使用 docker port npfweb 命令,可以显示当前容器的端口映射。

从这里可以看到,默认是映射到当前物理机所有可用IP地址的 32769 端口上来。如果我想使用固定的IP地址呢?我们先把容器杀掉。
docker kill npfweb

使用如下命令:
docker run --name npfweb --network bridge -p 192.168.47.102::80 --rm mageedu/httpd:v0.2

使用 docker port npfweb 命令,可以显示当前容器的端口映射。

可以看到,现在是映射到固定的IP地址上面去了。
joined
联盟式容器,让两个容器可以共享同一个网络名称空间,但其他空间,例如User, PID, Mount等还是隔离的。
先启动第一个容器
docker run --name b1 -it --rm busybox

这个容器的 ip 地址是:172.17.0.3
而后,我们再启动一个容器:
docker run --name b2 --network container:b1 -it --rm busybox

我们发现这个容器的 ip 地址也是:172.17.0.3
证明这两个容器的网络名称空间是共享的。
但是需要注意一点,他们的文件系统还是隔离的。我们在 b1 这个容器里面创建一个目录,如下:

我们到 b2 这个容器上去查看,是否存在这个目录。

完全符合我们的预期,b2 这个容器里面是不存在这个目录的。
我们在 b2 上面启动 httpd,测试一下网络。如下:

而后,我们去 b1 上去访问:

在 b1 这个容器上面,可以通过 127.0.0.1 进行访问,可想而知,b1 和 b2 确实是共享同一个网络名称空间,就相当于他们两个是运行在同一个主机上面的两个进程一样。但是实际上,他们还是有隔离的。
host
我们看下最后一种网络模式。运行如下:
docker run --name b3 --network host -it --rm busybox

很明显,在 b3 这个容器里面看到,都是宿主机的网络。
如果我们在 b3 这个容器里面启动一个 httpd 服务,会怎么样呢?如下:

接下来,我们可以在物理机的外部访问:

docker0 网桥
在centos上面安装了docker之后,宿主机上默认被创建了一个名为 docker0 的网卡,其 IP 为 172.17.0.1:

有了这个 docker0 的网卡,宿主机还会在内核的路由表中添加一条到达相应网络的静态路由记录:

这条路由信息表示所有目的IP地址为 172.17.0.0/16的数据包都会从 docker0 网卡发出。
接下来我们创建一个名为centosnpf1的容器,并观察其网络配置:

在 centosnpf1 容器内可以看到两块网卡:lo 和 eth0。其中 lo 是容器的回环网卡,eth0 是容器与外界通信的网卡,eth0 的 IP 信息为 172.17.0.2/16,和宿主机上的网卡 docker0 在同一网段中。查看 centosnpf1 的路由信息:

centosnpf1 容器的默认网关正是宿主机的 docker0 网卡。通过 ping 命令测试与外网的连通性,此时容器 centosnpf1 是可以连通外网的,这就说明 centosnpf1 的 eth0 网卡与宿主机的 docker0 网卡是连通的。

下面我们来查看宿主机的网络设备:

发现多了一个以 "veth" 开头的网卡,这是一个 veth 设备。而 veth 设备总是成对出现的(veth pair是用于不同network namespace间进行通信的方式,veth pair 将一个 network namespace 数据发往另一个 network namespace 的 veth),那么与 vethb7c23b7 配对的就应该是 centosnpf1 容器中的 eth0 了。既然 centosnpf1 容器中的 eth0 是与 docker0 连通的,那么 vethb7c23b7 也应该是与 docker0 连通的。因此 docker0 并不是一个简单的网卡设备,而是一个网桥。
重复上述的步骤,我们重新再创建一个centosnpf2的contaier,如下:

同时宿主机又多了一个以 "veth" 开头的网卡。

通过brctl show命令我们可以看到,docker0网桥上面的网卡接口。

下图展示了 docker bridge 网络模式的拓扑图:

总结
Docker 利用 veth pair [用于不同network namespace间进行通信的方式,veth pair 将一个 network namespace 数据发往另一个 network namespace 的 veth]技术,在宿主机上创建了两个虚拟网络接口 veth0 和 veth1(veth pair 技术的特性可以保证无论哪一个 veth 接收到网络报文,都会无条件地传输给另一方)。

容器与宿主机通信 : 在桥接模式下,Docker Daemon 将 veth1,veth2,veth3 附加到 docker0 网桥上,保证宿主机的报文有能力发往 docker0网桥上面的veth1,veth2,veth3。再将 veth0 添加到 Docker 容器所属的网络命名空间[网络命名空间是用于隔离网络资源(/proc/net、IP 地址、网卡、路由等)。由于一个物理的网络设备最多存放在一个网络命名空间中,所以通过 veth pair 在不同的网络命名空间中创建通道,才能达到通信的目的],保证宿主机的网络报文若发往 veth1,veth2,veth3 可以立即被Docker 容器上的 veth0 收到。
容器与外界通信 : 容器如果需要联网,则需要采用 NAT [网络地址转换(Network Address Translation)的缩写,是一种在 ip 数据包通过路由器或防火墙时重写来源 ip 地址或目的 ip 地址的技术] 方式。NAT 包含两种转换方式:SNAT 和 DNAT 。
目的 NAT (Destination NAT,DNAT): 修改数据包的目的地址。
当宿主机以外的世界需要访问容器时,数据包的流向如下图所示:

由于容器的 IP 与端口对外都是不可见的,所以数据包的目的地址为宿主机的 ip 和端口,为 192.168.1.10:24 。
数据包经过路由器发给宿主机 eth0,再经 eth0 转发给 docker0 网桥。由于存在 DNAT 规则,会将数据包的目的地址转换为容器的 ip 和端口,为 172.17.0.n:24 。
宿主机上的 docker0 网桥识别到容器 ip 和端口,于是将数据包发送附加到 docker0 网桥上的 veth1 接口,veth1 接口再将数据包发送给容器内部的 veth0 接口,容器接收数据包并作出响应。

源 NAT (Source NAT,SNAT): 修改数据包的源地址。
当容器需要访问宿主机以外的世界时,数据包的流向为下图所示:

此时数据包的源地址为容器的 ip 和端口,为 172.17.0.n:24,容器内部的 veth0 接口将数据包发往 veth1 接口,到达 docker0 网桥。宿主机上的 docker0 网桥发现数据包的目的地址为外界的 IP 和端口,便会将数据包转发给 eth0 ,并从 eth0 发出去。由于存在 SNAT 规则,会将数据包的源地址转换为宿主机的 ip 和端口,为 192.168.1.10:24 。由于路由器可以识别到宿主机的 ip 地址,所以再将数据包转发给外界,外界接受数据包并作出响应。这时候,在外界看来,这个数据包就是从 192.168.1.10:24 上发出来的,Docker 容器对外是不可见的。





