虚拟化网络的原理简述
Linux内核支持六种名称空间,只要在用户空间有相应的客户端工具,都可以对对应的名称空间进行操作。
假如物理机有四块网卡,可以创建两个名称空间,此时可灵活将网卡分配到单独的名称空间中。
一般一个设备只能属于一个名称空间,如此每个名称空间都可以配置IP地址,并且和外部通信。
假如名称空间数量超过物理网卡数量,而名称空间中的进程必须通过物理网卡对外通信,如何处理?可以使用虚拟网卡设备,使用纯软件的方式模拟出网卡设备。
在Linux内核级别,支持两种模拟:二层设备和三层设备。
二层设备(链路层)
链路层,实现报文转发的设备。
利用内核对二层设备的模拟,创建虚拟网卡接口,这种网络接口是成对出现的,模拟为一根网线的两头。其中一头插在主机上,另一头插在交换机上。
内核原生支持二层虚拟网桥设备,用软件来构建交换机。比如bridge-utils工具的brctl来实现。
利用软件交换机和软件实现的名称空间,如此就可模拟一个主机连接到交换机中,以实现网络连接的功能。两个名称空间相当于连接到同一台交换机中的两台主机。
以上就是虚拟化网络,是网络虚拟化技术的一种简单实现。
三层设备(软件交换机)
OVS:Open VSwitch 开源的虚拟交换机,能模拟实现高级的三层网络设备如VLAN,VxLAN,GRE等等,它不属于Linux内核本身的模块,因此需要额外安装。它是由思科等众多网络设备生产公司开发的,其功能非常强大。
SDN:软件定义网络/软件驱动网络,需要在硬件层面上支持虚拟化网络,还需要在每个主机上构建复杂的虚拟化网络,来运行多个虚拟机或者容器。
容器间通信
如果在同一个host上的两个namespace需要通信,可以在host上建立虚拟交换机,使用纯软件的方式建立一对网卡,一半在容器上,一半在交换机上。此时只要容器配置同子网的IP地址,即可通信。
如果存在多个交换机,比如两个软件交换机,各自连接不同的容器。比如:
可以创建一对网卡,一半在SW1上,一半在容器中。(docker的默认网络模型就是这个)
如果希望C1仅仅和C3通信,则需要在交换机中间添加路由器,而路由器是三层设备,可以在Linux内核的模块来单独来支持,比如第三个容器(中的内核)来实现报文转发。
如果希望跨主机通信,比如C1和C5通信,如何实现?
方式有:
-
桥接:
把host的物理网卡当做交换机使用,各个容器都有自己的虚拟网卡/mac地址,如果需要和本host通信,则将目的mac指向host的虚拟网卡即可。如此可以让C1和C5通信。
然而这种方式的代价很大,如果都是桥接则所有容器/主机都在同一网络平面上,非常容易产生风暴,隔离性很差。
因此,在大规模的虚拟化或者容器的场景中,不能选用这种方式。除非使用大二层的网络技术将其隔离,否则,都不应该直接桥接。
-
NAT
如果希望对外通信,则应该使用nat技术而不是桥接。
如果C3和C5通信,C3将网关指向了S1,物理机上打开核心转发功能。报文:C3 -> S1 ->(路由表,转发) -> 外部网络。但是报文无法回来,因为C3是私有地址。因此,在C3的报文离开主机前,将IP转换为S1主机上的IP地址,这就是源地址转换。
如此C5可直接回复给S1。而S1内部的nat表可知道,该报文实际上是属于C3的,因此会自动转发给C3。
上述通信必须经过nat实现,而且两级的nat代理。因为C5可能也是nat内部。因此S1并看不到C5,除非将C5 DNAT发布出去。
因此,比如将C5发布到S2的地址和端口,由S2自动将其请求转换到C5中。
经过SNAT和DNAT转换,效率不高。而且通信的双方并看不到真正的对方。其好处是,网络易于管理。
-
叠加网络Overlay Network
这种网络方式不用完全暴露主机,也不用完全隐藏主机。其方式如下:
- 多个host,创建虚拟桥,让VM连接到虚拟桥上。
- 在虚拟桥上创建隧道,让C3直接看到C5
- 物理机可直接通信,C3和C5在同一个地址段内。C3先将报文送给桥,而桥知道C5并不在本地,于是将报文通过物理网卡发出去:在使用隧道转发出去前,报文C3|C5再封装一层IP首部:H1|H2
- H2拆开第一层封装,看到C3|C5,于是将报文转发给C5.
上述实现两级的三层封装,使用一个IP承载另一个IP,这就是隧道技术。如此C3和C5就可以直接通信。
模拟容器间通信
使用ip命令,可以在一台主机中构建出一个网络名称空间,并且可模拟容器间通信。
root@eto:~# ip netns help
Usage: ip netns list
ip netns add NAME
ip netns set NAME NETNSID
ip [-all] netns delete [NAME]
ip netns identify [PID]
ip netns pids NAME
ip [-all] netns exec [NAME] cmd ...
ip netns monitor
ip netns list-id
通过这种方式只有网络名称空间是隔离的,而其他名称空间都是共享的。和容器略有不同。
-
创建名称空间
root@eto:~# ip netns add r1 root@eto:~# ip netns add r2 root@eto:~# ip netns list r2 r1
-
创建对应的虚拟网卡对,然后分配到名称空间中
root@eto:~# ip link add name veth1.1 type veth peer name veth1.2 root@eto:~# ip link show ... 9: veth1.2@veth1.1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 6a:f0:ef:a0:76:80 brd ff:ff:ff:ff:ff:ff 10: veth1.1@veth1.2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
# 把veth1.2分配到r1中去 root@eto:~# ip link set dev veth1.2 netns r1 root@eto:~# ip a s ... 10: veth1.1@if9: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 0e:33:73:c9:73:fc brd ff:ff:ff:ff:ff:ff link-netnsid 0 root@eto:~# ip netns exec r1 ifconfig -a lo: flags=8<LOOPBACK> mtu 65536 loop txqueuelen 1000 (Local Loopback) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 veth1.2: flags=4098<BROADCAST,MULTICAST> mtu 1500 ether 6a:f0:ef:a0:76:80 txqueuelen 1000 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
-
修改网卡名称,设置IP地址并激活
root@eto:~# ip netns exec r1 ip link set dev veth1.2 name eth0 root@eto:~# ip netns exec r1 ifconfig eth0 10.1.0.2/24 up root@eto:~# ifconfig veth1.1 10.1.0.1/24 up root@eto:~# ping 10.1.0.2 PING 10.1.0.2 (10.1.0.2) 56(84) bytes of data. 64 bytes from 10.1.0.2: icmp_seq=1 ttl=64 time=0.054 ms 64 bytes from 10.1.0.2: icmp_seq=2 ttl=64 time=0.036 ms
此时两张网卡可通信,有宿主机 -> r1
-
将另一半网卡挪到r2,并激活
root@eto:~# ip link set dev veth1.1 netns r2 root@eto:~# ip netns exec r2 ifconfig veth1.1 10.1.0.3/24 up root@eto:~# ip netns exec r2 ping 10.1.0.2 PING 10.1.0.2 (10.1.0.2) 56(84) bytes of data. 64 bytes from 10.1.0.2: icmp_seq=1 ttl=64 time=0.037 ms 64 bytes from 10.1.0.2: icmp_seq=2 ttl=64 time=0.034 ms
使用这种方式来测试网络通信模型,和虚拟机几乎无区别。可以模拟物理桥,仅主机和nat桥。
Docker的网络
Docker安装完成后,自动提供三种网络:
- bridge
- host
- none
root@eto:~# docker network ls
NETWORK ID NAME DRIVER SCOPE
fd3713beb21e bridge bridge local
509f1fdd50fb host host local
83fea3e78ba1 none null local
Bridge:本机上创建的软交换机,也可以当做网卡使用。当启动容器时,可自动让容器自动分配一对网卡,一半在容器上,一半在交换机上。比如:
# 启动容器
root@eto:~# docker run --name t1 -it jaywin/httpd:v0.1-1
/ #
# 宿主机上,关联到docker0上的网卡设备,其中
root@eto:~# ip a s
...
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:72:ff:55:a2 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
...
6: veth2fadfaf@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
veth2fadfaf@if5 中的if5在容器中,另一端veth2fadfaf在宿主机中。它们连接到了docker0桥上。
root@eto:~# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024272ff55a2 no veth2fadfaf(宿主机的接口)
Docker0桥默认是一个nat桥,创建容器后会自动分配iptables规则:
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
只要从docker0桥的网段发出的报文,自动选择一个合适的源地址做SNAT(MASQUERADE)。如此同一个宿主机下的容器,都可以相互通信(属于docker0的二层网络),并且容器可以和外部通信(实现了SNAT)。
如果有一个nginx容器,提供服务。另外一个容器也是连接到桥上来的,则可以直接当客户端来访问nginx。通过如下方式:
- 物理机上也能直接访问(可以看成是仅主机的桥)
- 如果是外部的物理机访问nginx,它是无法看到nginx容器,因为nat将容器隐藏在背后。此时需要使用dnat将nginx发布
因此,当使用默认的桥接网络时,只能通过dnat规则以便让外部的客户端访问该容器。
如果同一台主机拥有两个web容器,如何对外做dnat?毕竟只有一个80端口,如何能正常提供访问?如果使用默认的网络模型,并没有很好的解决方法。
如果使用overlay网络,则无需有上述的困扰,直接访问对应的web服务即可。关于overlay网络,暂不表述。
除了上述描述的方式,还有什么方法可以使得容器可以对外服务?比如:
让每个容器仅拥有user,mount,pid是隔离的,两个容器共享UTS,Net,IPC。如此,容器的文件系统,pid和用户是隔离的,而网络是同一组,同一个网络协议栈,同一个域名主机名。容器间的通信可直接通过lo实现。
共享的一般是和网络通信相关的资源。
也可以让容器直接使用物理名称空间。
如果容器仅使用none网络,则容器仅有lo,没有网卡,无网络通信功能
Docker网络模型
Docker创建容器时,使用选项–network来指定使用哪种网络模型。默认default为bridge (docker0)
查看网络的详细信息:
root@eto:~# docker network inspect bridge
Docker的网络名称空间和UTS,IPC是可以让容器共享的,因此可构建出隔离,桥接,nat等网络模型。
docker网络模型
四种网络模型:
- 封闭式容器
- 桥接式容器:容器包含虚拟网络设备,一半在docker0上,一半在名称空间中。默认情况下,容器是启动为桥接式的网络模型。
- 联盟式容器:容器a有自己的名称空间,容器b共享容器a的名称空间。可以让a和b的容器应用在本地通信
- 容器共享宿主机的名称空间
可以指定为封闭式容器:
root@eto:~# docker run --name t4 -it --network none --rm busybox:latest
/ # ifconfig -a
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
Bridge可以实现不同容器间通信,而主机名默认为容器id,可使用hostname设置或者run的时候指定hostname,从容器外注入主机名。
# 查看默认主机名
root@eto:~# docker run --name t4 -it --network bridge --rm busybox:latest hostname
3ccdfa0745a3
# 注入主机名
root@eto:~# docker run --name t4 -it --network bridge -h t2.jaywin.com --rm busybox:latest
/ # hostname
t2.jaywin.com
也可以注入解析,默认情况下容器使用宿主机的DNS配置。
/ # cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 t2.jaywin.com t2
可自定义DNS服务器
root@eto:~# docker run --name t4 -it --network bridge -h t2.jaywin.com --dns 114.114.114.114 --rm busybox:latest cat /etc/resolv.conf
search midea.com.cn
nameserver 114.114.114.114
定义搜索域
root@eto:~# docker run --name t4 -it --network bridge -h t2.jaywin.com --dns 114.114.114.114 --dns-search ilinux.io --rm busybox:latest cat /etc/resolv.conf
search ilinux.io
nameserver 114.114.114.114
添加hosts记录
root@eto:~# docker run --name t4 -it --network bridge -h t2.jaywin.com --dns 114.114.114.114 --dns-search ilinux.io --add-host www.jaywin.com:1.1.1.1 --rm busybox:latest cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
1.1.1.1 www.jaywin.com
172.17.0.2 t2.jaywin.com t2
服务的暴露方式
使用-p选项进行服务暴露
动态端口暴露
root@eto:~# docker run --name w1 --rm -p 80 jaywin/httpd:v0.2
root@eto:~# docker port w1
80/tcp -> 0.0.0.0:32768
# 暴露随机端口
实际上,通过DNAT规则将服务发布出去。
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:32768 to:172.17.0.2:80
# 只要访问本机的32768端口,将DNAT到172.17.0.2:80
# 当删除容器时,nat规则也会一并移除
固定地址的动态端口暴露
root@eto:~# docker run --name w1 --rm -p 192.168.2.10::80 jaywin/httpd:v0.2
root@eto:~# docker port w1
80/tcp -> 192.168.2.10:32768
固定端口暴露
# 将端口暴露在宿主机的指定端口:
root@eto:~# docker run --name w1 --rm -p 80:80 jaywin/httpd:v0.2
root@eto:~# docker port w1
80/tcp -> 0.0.0.0:80
-p选项可以使用多次,以暴露多个端口。
暴露所有端口
-P选项或者–publish-all,将容器所有计划要暴露的端口全部映射至主机端口。
计划 要暴露的端口需要使用–expose选项指定
# docker run -d -P --expose 2222 --expose 3333 --name web busybox:latest /bin/httpd -p 2222 -f
联盟式容器Joined containers
联盟式容器是指使用某个已存在容器的网络接口的容器,接口被联盟内各容器共享使用。因此,联盟式容器彼此间完全无隔离。
联盟式容器彼此间虽然共享同一个网络名词空间,但其他名称空间如User、Mount等还是隔离的
联盟式容器彼此间存在端口冲突的可能。因此,通常只会在多个容器上的程序需要程序loopback接口相互通信,或者对某个已存在的容器的网络属性进行监控时,才使用这种模式的网络模型
创建联盟式容器
root@eto:~# docker run --name b2 --rm --network container:w1 -it busybox:latest
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:16 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1256 (1.2 KiB) TX bytes:0 (0.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
# 此时b2和W1共享网络名称空间,而其他资源是隔离的。此时,b2可通过lo口访问w1的httpd服务。
# 如此相当于同一主机上的两个进程。
/ # wget -O - -q 127.0.0.1
busybox http server
# 容器和宿主机的联盟式网络
root@eto:~# docker run --name b2 --rm --network host -it busybox:latest
/ # ifconfig
docker0 Link encap:Ethernet HWaddr 02:42:72:FF:55:A2
inet addr:172.17.0.1 Bcast:172.17.255.255 Mask:255.255.0.0
inet6 addr: fe80::42:72ff:feff:55a2/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:19 errors:0 dropped:0 overruns:0 frame:0
TX packets:38 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1289 (1.2 KiB) TX bytes:3578 (3.4 KiB)
如此一来,将十分简便地部署应用,并利用宿主机的网络。
修改docker0上的网络信息
自定义docker0桥的网络属性信息:
root@eto:~# cat /etc/docker/daemon.json
{
"exec-opts": ["native.cgrounpdriver=systemd"],
"registry-mirrors": ["https://e2615hzs.mirror.aliyuncs.com"],
"bip": "192.168.1.0/24"
}
# 核心选项bip,即bridge ip之意,用于指定docker0桥自身的IP地址
# 修改docker0的地址后,启动容器的地址也会随之改变。
dockerd默认监听在Unix Socket格式的地址:/var/run/docker.sock。如果使用TCP套接字,则修改配置文件选项为:
"hosts": ["tcp://0.0.0.0:2375","unix:///var/run/docker.sock"]
也可以向dockerd直接传递"-H | --host"选项。在ubuntu上,如果docker.service指定了-H fd,此处再设置hosts时会无法启动。此时可以将docker.service中的-H选项去掉即可。
此时客户端就可以通过-H,指定要连接的服务器地址:
root@eto:~# docker -H 192.168.2.10:2375 ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
创建自定义桥设备
root@eto:~# docker network create -d bridge --subnet "192.168.100.0/24" --gateway "192.168.100.1" mybr0
eb582031756ecb64541e6c95dd23caf1ece1c5b87378438cf6661b250b887052
root@eto:~# docker network ls
NETWORK ID NAME DRIVER SCOPE
f3f514b6596c bridge bridge local
509f1fdd50fb host host local
eb582031756e mybr0 bridge local
83fea3e78ba1 none null local
将容器和mybr0桥接
root@eto:~# docker run --name t4 -it --net mybr0 busybox
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:C0:A8:64:02
inet addr:192.168.100.2 Bcast:192.168.100.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:12 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1032 (1.0 KiB) TX bytes:0 (0.0 B)
再创建一个容器,桥接到docker0
root@eto:~# docker run --name b2 -it --rm busybox
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:8 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:696 (696.0 B) TX bytes:0 (0.0 B)
这两个容器可以相互通信,默认做nat地址转换,只需要宿主机打开forward转发,并清空此规则:
root@eto:~# iptables -F DOCKER-ISOLATION-STAGE-1
此规则包含限制不同容器网络之间的访问的规则:
target prot opt source destination
DOCKER-ISOLATION-STAGE-2 all -- 0.0.0.0/0 0.0.0.0/0
DOCKER-ISOLATION-STAGE-2 all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0
如此就可以通信了:
/ # ping 192.168.100.2
PING 192.168.100.2 (192.168.100.2): 56 data bytes
ip64 bytes from 192.168.100.2: seq=125 ttl=63 time=0.133 ms
64 bytes from 192.168.100.2: seq=126 ttl=63 time=0.074 ms
64 bytes from 192.168.100.2: seq=127 ttl=63 time=0.168 ms