循序渐进演示Docker如何创建IP地址
一. 运行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设备
每个网络空间都有自己的localhost
和loopback(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 space
和netns_dustin network space
。
很容易想到,虚拟网络设备是成对出现的,这和网线连接两台计算机类似(这里是不同网络空间),网线也有两端,所以网络设备也有两端。即两个虚拟设备。
创建veth
网络设备,下面命令会创建两个虚拟设备,对端的名字分别为veth_dustin
和veth_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/24
、10.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
参考资料