已复制
全屏展示
复制代码

循序渐进演示Docker如何创建IP地址


· 15 min read

一. 运行HTTP SERVER实例

打开一个终端使用Python启动一个http server

python3 -m http.server 8080

打开第二个终端访问启动的8080端口,可以看到能正常访问。

curl localhost:8080
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
......

打开第三个终端,在启动一个http server,还是监听在8080端口,发现无法启动,原因是端口被占用了。

python3 -m http.server 8080
OSError: [Errno 98] Address already in use

二. 创建网络空间

Containers are just Linux cgroups and namespaces.

如果在网络上搜索什么是容器,那么通常会达到如上的结果,那么我们就从网络命名空间说起。

在第一个终端中我们启动了一个监听在8080端口的服务,其实我们是使用了主机的网络命名空间(host network namespace),有时又叫root or global network namespace,为了不让端口冲突,我们创建一个新的网络空间给第三个终端使用。

sudo ip netns add netns_dustin

使用新创建的网络空间启动http server服务,而不是host network namespace,现在问题来了,如何访问这个新的网络命名空间呢。

sudo ip netns exec netns_dustin python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...

同样使用ip netns exec netns_dustin前缀来访问,发现访问不通。为什么呢?这是因为这个新的网络命名空间的loopback device没有启动。

sudo ip netns exec netns_dustin curl localhost:8080
curl: (7) Couldn't connect to server

三. 启动LoopBack设备

每个网络空间都有自己的localhostloopback(lo)设备,所以netns_dustin网络命名空间的localhost和host网络命名空间的localhost是不相同的。

查看网络空间的设备列表,看到lo设备没有启动。

sudo ip netns exec netns_dustin ip address list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

启动lo设备以后再次查看。

sudo ip netns exec netns_dustin ip link set dev lo up

sudo ip netns exec netns_dustin ip address list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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

现在再次访问新的网络空间的8080端口,发现能正常访问了,由此可以发现,如果想通过非localhost访问网络命名空间的访问,必须要要一个虚拟设备,用来配置独立的ip地址,这就引出了接下来要说的虚拟设备。

sudo ip netns exec netns_dustin curl localhost:8080
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
......

四. 创建虚拟网络设备

Linux上有很多可以创建的虚拟设备类型,我们这里要说的是veth,创建veth时是成对出现的。显然成对出现的设备是用来连接不同的网络空间的,比如host network spacenetns_dustin network space

很容易想到,虚拟网络设备是成对出现的,这和网线连接两台计算机类似(这里是不同网络空间),网线也有两端,所以网络设备也有两端。即两个虚拟设备。

创建veth网络设备,下面命令会创建两个虚拟设备,对端的名字分别为veth_dustinveth_ns_dustin

sudo ip link add dev veth_dustin type veth peer name veth_ns_dustin

查看创建设备,看到两个设备都是down的,观察两个设备的名称。后面我们会把veth_dustin留在host网络空间,veth_ns_dustin放在新创建的网络空间。

ip link list
8: veth_ns_dustin@veth_dustin: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether ba:a4:24:b5:3a:1d brd ff:ff:ff:ff:ff:ff
9: veth_dustin@veth_ns_dustin: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 0e:53:4d:16:9b:93 brd ff:ff:ff:ff:ff:ff

启动host空间的网络.

sudo ip link set dev veth_dustin up

veth_ns_dustin移动的ns_dustin网络空间,移动以后再次查看,发现host网络空间只留下了veth_dustin设备了。

sudo ip link set veth_ns_dustin netns netns_dustin
ip link list
9: veth_dustin@if8: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN mode DEFAULT group default qlen 1000
    link/ether 0e:53:4d:16:9b:93 brd ff:ff:ff:ff:ff:ff link-netnsid 0

查看ns_dustin网络空间的设备,发现除了lo意外,还多出来了一个veth_ns_dustin

sudo ip netns exec netns_dustin ip link list
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
8: veth_ns_dustin@if9: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether ba:a4:24:b5:3a:1d brd ff:ff:ff:ff:ff:ff link-netnsid 0

启动veth_ns_dustin设备,查看状态。

sudo ip netns exec netns_dustin ip link set dev veth_ns_dustin up

sudo ip netns exec netns_dustin ip link list
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
8: veth_ns_dustin@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether ba:a4:24:b5:3a:1d brd ff:ff:ff:ff:ff:ff link-netnsid 0

五. 虚拟设备创建ip地址

现在我们可以给刚刚创建的两个设备创建IP地址了。分别为10.0.0.10/2410.0.0.11/24,其实docker分配IP地址也是类似的。

sudo ip address add 10.0.0.10/24 dev veth_dustin
sudo ip netns exec netns_dustin ip address add 10.0.0.11/24 dev veth_ns_dustin

下图是目前网络情况。

接下来验证我们的IP地址是否可用,测试发现和预想的完全符合

ping 10.0.0.10 -c 1
ping 10.0.0.11 -c 1
sudo ip netns exec netns_dustin ping 10.0.0.10 -c 1
sudo ip netns exec netns_dustin ping 10.0.0.11 -c 1

接着通过设置的IP地址来访问服务,同样也能正常访问。

curl 10.0.0.11:8080
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
......

同样尝试在netns_dustin空间访问host network namespace,也能正常使用。

sudo ip netns exec netns_dustin curl 10.0.0.10:8080

现在测试从netns_dustin 到 host network namespace的访问,假设host的IP地址为192.168.0.100。

sudo ip netns exec netns_dustin curl 192.168.0.100:8080
curl: (7) Couldn't connect to server

不能访问是为什么呢,原因是netns_dustin不知道怎么去路由192.168.0.100,所以我们要手动添加路由(默认路由为10.0.0.10,和自己是直连的),添加后就能正常访问了。

sudo ip netns exec netns_dustin ip route add default via 10.0.0.10
sudo ip netns exec netns_dustin curl 192.168.0.100:8080
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
......

六. 访问互联网

下一步就要设置netns_dustin空间能访问外网了,先简单测试一下,ping百度、ping IP地址,发现都不通。

sudo ip netns exec netns_dustin ping www.baidu.com
ping: www.baidu.com: Name or service not known

sudo ip netns exec netns_dustin ping 114.114.114.114
PING 114.114.114.114 (114.114.114.114) 56(84) bytes of data.

为了能使虚拟设备访问网络,需要设置iptables,让iptables转发来自虚拟设备veth_dustin的包,转发到物理网卡上。首先系统需要开启转发包。

# 临时生效
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward

# 永久生效
cat /etc/sysctl.conf | grep "^net.ipv4.ip_forward=1" || echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

确定物理机器的网卡名称,即192.168.0.100对应的设备名称,我这里是 enp4s0

sudo iptables --append FORWARD --in-interface veth_dustin --out-interface enp4s0 --jump ACCEPT
sudo iptables --append FORWARD --in-interface enp4s0 --out-interface veth_dustin --jump ACCEPT

如果按照上面的转发规则,我们永远也不会获取到请求的响应,因为当物理设备为我们转发时,数据包的源ip没有改变,所以现在数据包的原地址为10.0.0.10,网络上的主机并不知道这个虚拟地址。好在iptables在数据包离开网卡设备时可以修改数据的源地址,即如下,这样一来,任何从enp4s0设备流出的数据包,数据包的源地址都会被修改成enp4s0的地址,即192.168.0.100

sudo iptables --append POSTROUTING --table nat --out-interface enp4s0 --jump MASQUERADE

下面是目前的网络流向图:

再次测试,发现一切正常。但是现在ping域名还是不通,还需要设置域名解析。

sudo ip netns exec netns_dustin ping 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=85 time=45.1 ms
64 bytes from 114.114.114.114: icmp_seq=2 ttl=68 time=50.0 ms
64 bytes from 114.114.114.114: icmp_seq=3 ttl=76 time=45.2 ms

sudo ip netns exec netns_dustin ping www.baidu.com
ping: www.baidu.com: Name or service not known

七. 域名解析设置

为了能使用域名解析,需要设置network namespace’s resolv.conf,默认情况下网络命名空间会使用/etc/resolv.conf文件内是dns做解析,每个网络命名空间都有其自己的dns配置,我们给netns_dustin设置域名解析

sudo mkdir -p /etc/netns/netns_dustin
echo "nameserver 114.114.114.114" | sudo tee /etc/netns/netns_dustin/resolv.conf

再次测试成功,接下来要考虑虚拟网络空间和其他虚拟网络命名空间的通讯问题。

sudo ip netns exec netns_dustin ping www.baidu.com
PING www.a.shifen.com (220.181.38.150) 56(84) bytes of data.
64 bytes from 220.181.38.150 (220.181.38.150): icmp_seq=1 ttl=47 time=8.67 ms
64 bytes from 220.181.38.150 (220.181.38.150): icmp_seq=2 ttl=47 time=13.0 ms
64 bytes from 220.181.38.150 (220.181.38.150): icmp_seq=3 ttl=47 time=5.44 ms

八. 多个网络命名空间之间通讯

为了测试,创建另一个网络命名空间netns_leah,并配置相应的ip地址。

sudo ip link add dev veth_leah type veth peer name veth_ns_leah
sudo ip link set dev veth_leah up
sudo ip address add 10.0.0.20/24 dev veth_leah
sudo ip netns add netns_leah
sudo ip link set dev veth_ns_leah netns netns_leah
sudo ip netns exec netns_leah ip link set dev lo up
sudo ip netns exec netns_leah ip link set dev veth_ns_leah up
sudo ip netns exec netns_leah ip address add 10.0.0.21/24 dev veth_ns_leah
sudo ip netns exec netns_leah ip route add default via 10.0.0.20
sudo ip netns exec netns_leah python3 -m http.server 8080

现在测试,从物理机到新创建的网络不通,netns_dustin 和 netns_leah 之间也不通。这是因为host网络空间的路由的问题。

ping 10.0.0.21 -c 1
sudo ip netns exec netns_dustin ping 10.0.0.21 -c 1
sudo ip netns exec netns_leah ping 10.0.0.11 -c 1

执行如下命令查看路由。目的地址为10.0.0.0/24的网段,使用了第一个默认路由,也就是说使用所有数据包都被导向到了veth_dustin虚拟设备。一种解决办法是veth_leah和netns_leah不使用10.0.0.0/24网段,而是使用10.0.1.0/24网段,这样一来就有了不通的路由了。这样做是可以的,但是做既浪费了ip地址,虚拟设备和实际设备也需要设置数据包转发、虚拟设备之间也需要设置包转发,显得繁琐。这是linux的虚拟网桥设备解决了这个问题。

ip route list
10.0.0.0/24 dev veth_dustin proto kernel scope link src 10.0.0.10 
10.0.0.0/24 dev veth_leah proto kernel scope link src 10.0.0.20

目前的网络流向是这样的:

九. 虚拟网桥与veth设备对

Linux 还有另外一个bridge类型的虚拟设备,它允许多个网络与多个虚拟设备之间的通信。创建网桥bridge_home

sudo ip link add dev bridge_home type bridge
sudo ip address add 10.0.0.1/24 dev bridge_home
sudo ip link set bridge_home up

查看网桥

ip link list
13: bridge_home: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ether da:8b:3d:c7:85:a4 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.1/24 scope global bridge_home
       valid_lft forever preferred_lft forever
    inet6 fe80::d88b:3dff:fec7:85a4/64 scope link 
       valid_lft forever preferred_lft forever

为了把虚拟的网络连接到bridge_home,我们将veth的master分配的bridge上。即veth_dustin 和 veth_leah 的master分配到 bridge_home 上

sudo ip link set dev veth_dustin master bridge_home
sudo ip link set dev veth_leah master bridge_home

现在的网络拓扑图如下:

设置master到bridge上以后可以到的虚拟设备的信息,重点注意关键字... master bridge_home ...

10: veth_dustin@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master bridge_home state UP group default qlen 1000
    link/ether c2:0c:ae:27:4a:05 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.10/24 scope global veth_dustin
       valid_lft forever preferred_lft forever
    inet6 fe80::c00c:aeff:fe27:4a05/64 scope link 
       valid_lft forever preferred_lft forever
12: veth_leah@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master bridge_home state UP group default qlen 1000
    link/ether 8a:84:ac:40:77:b2 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet 10.0.0.20/24 scope global veth_leah
       valid_lft forever preferred_lft forever
    inet6 fe80::8884:acff:fe40:77b2/64 scope link 
       valid_lft forever preferred_lft forever

此时,我们可以把netns_dustin和netns_leah的默认路由改成相同(10.0.0.1)的了。之前都是指向自己的对端的虚拟设备。

sudo ip netns exec netns_dustin ip route delete default via 10.0.0.10
sudo ip netns exec netns_dustin ip route add default via 10.0.0.1
sudo ip netns exec netns_leah ip route delete default via 10.0.0.20
sudo ip netns exec netns_leah ip route add default via 10.0.0.1

在物理机上,现在还有三条路由,需要将前两条删除

ip route
10.0.0.0/24 dev veth_dustin proto kernel scope link src 10.0.0.10 
10.0.0.0/24 dev veth_leah proto kernel scope link src 10.0.0.20 
10.0.0.0/24 dev bridge_home proto kernel scope link src 10.0.0.1 

sudo ip address delete 10.0.0.10/24 dev veth_dustin
sudo ip address delete 10.0.0.20/24 dev veth_leah

现在从host网络空间,我们可以对两个新的命名空间进行访问了

ping 10.0.0.11 -c 1
ping 10.0.0.21 -c 1

但是网络空间之间还是不能通讯,下面的测试均会失败,因为bridge_home不会转发数据包,bridge_home会接收来自netns_dustin和netns_leah的数据包,但是不转发。

sudo ip netns exec netns_dustin ping 10.0.0.21 -c 1
sudo ip netns exec netns_leah ping 10.0.0.11 -c 1

开启bridge_home的转发功能,开启之后namespace之间的通信就通了

sudo iptables --append FORWARD --in-interface bridge_home --out-interface bridge_home --jump ACCEPT

sudo ip netns exec netns_dustin ping 10.0.0.21 -c 1
sudo ip netns exec netns_leah ping 10.0.0.11 -c 1

现在删除veth_dustin和enp4s0的iptables规则。

sudo iptables --delete FORWARD --in-interface veth_dustin --out-interface enp4s0 --jump ACCEPT
sudo iptables --delete FORWARD --in-interface enp4s0 --out-interface veth_dustin --jump ACCEPT

现在来设置bridge_home 和 enp4s0的数据包转发

sudo iptables --append FORWARD --in-interface bridge_home --out-interface enp4s0 --jump ACCEPT
sudo iptables --append FORWARD --in-interface enp4s0 --out-interface bridge_home --jump ACCEPT

现在的拓扑图如下:

bridge的功能非常强大,在新创建的veth设备对中,按照如下步骤就能正常使用网络了。

  • 将host网络命名空间的这一端设置master为bridge_home
  • 将新的网络命名空间的路由指向bridge_home
  • 设置新的网络命名空间的resolv.conf

当运行 ocker network create 时,Docker会创建一个bridge,当运行容器时,Docker也是将一个veth设备关联到bridge上,让bridge给自己转发数据包。

十. 清除测试环境

或者从起电脑,这些都会被自动清除

sudo ip link delete dev bridge_home
sudo ip link delete dev veth_dustin
sudo ip link delete dev veth_leah
sudo ip netns delete netns_dustin
sudo ip netns delete netns_leah
sudo iptables --delete FORWARD --in-interface bridge_home --out-interface enp4s0 --jump ACCEPT
sudo iptables --delete FORWARD --in-interface enp4s0 --out-interface bridge_home --jump ACCEPT
sudo iptables --delete POSTROUTING --table nat --out-interface enp4s0 --jump MASQUERADE
sudo rm -rf /etc/netns/netns_dustin

参考资料

🔗

文章推荐