Linux网络虚拟化
网络虚拟化基石:network namespace
network namespace,它在Linux内核2.6版本引入,作用是隔离Linux系统的设备,以及IP地址、端口、路由表、防火墙规则等网络资源。因此,每个网络namespace里都有自己的网络设备(如IP地址、路由表、端口范围、/proc/net目录等)。
veth pair
veth是虚拟以太网卡(Virtual Ethernet)的缩写。veth设备总是成对的,因此称之为veth pair。veth pair一端发送的数据会在另外一端接收,常被用于跨network namespace之间的通信,即分别将veth pair的两端放在不同的namespace里。 经典容器组网模型就是veth pair+bridge的模式。容器中的eth0实际上和宿主机上的某个veth是成对的(pair)关系。
在目标容器里执行以下命令:
1
2
3
# ip link show eth0
1219: eth0@if1220: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
从上面的命令输出可以看到1219: eth0@if1220,其中1219是eth0接口的index,1220是和它成对的veth的index。
当host执行下面的命令时,可以看到对应1220的veth网卡是哪一个,这样就得到了容器和veth pair的关系。
1
2
# ip link show |grep 1220
1220: veth2a7b926@if1219: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
Linux bridge
Linux bridge就是Linux系统中的网桥,但是Linux bridge的行为更像是一台虚拟的网络交换机,任意的真实物理设备(例如eth0)和虚拟设备(例如,前面讲到的veth pair)都可以连接到Linux bridge上。需要注意的是,Linux bridge不能跨机连接网络设备。
iptables
iptables在Docker和Kubernetes网络中应用甚广。例如,Docker容器和宿主机的端口映射、Kubernetes Service的默认模式、CNI的portmap插件、Kubernetes网络策略等都是通过iptables实现的。
netfilter
iptables的底层实现是netfilter。netfilter是Linux内核2.4版引入的一个子系统,它作为一个通用的、抽象的框架,提供一整套hook函数的管理机制,使得数据包过滤、包处理(设置标志位、修改TTL等)、地址伪装、网络地址转换、透明代理、访问控制、基于协议类型的连接跟踪,甚至带宽限速等功能成为可能。
- 当网卡上收到一个包送达协议栈时,最先经过的netfilter钩子是PREROUTING,如果确实有用户埋了这个钩子函数,那么内核将在这里对数据包进行目的地址转换(DNAT)。不管在PREROUTING有没有做过DNAT,内核都会通过查本地路由表决定这个数据包是发送给本地进程还是发送给其他机器。
- 如果是发送给其他机器(或其他networknamespace),就相当于把本地当作路由器,就会经过netfilter的FORWARD钩子,用户可以在此处设置包过滤钩子函数。
- 所有马上要发到协议栈外的包都会经过POSTROUTING钩子,用户可以在这里埋下源地址转换(SNAT)或源地址伪装(Masquerade,简称Masq)的钩子函数。
- 如果经过上面的路由决策,内核决定把包发给本地进程,就会经过INPUT钩子。
- 本地进程收到数据包后,回程报文会先经过OUTPUT钩子,然后经过一次路由决策
table、chain和rule
- INPUT链:一般用于处理输入本地进程的数据包;
- OUTPUT链:一般用于处理本地进程的输出数据包;
- FORWARD链:一般用于处理转发到其他机器/network namespace的数据包;
- PREROUTING链:可以在此处进行DNAT;
-
POSTROUTING链:可以在此处进行SNAT
- filter表:用于控制到达某条链上的数据包是继续放行、直接丢弃(drop)或拒绝(reject);
- nat表:用于修改数据包的源和目的地址;
- mangle表:用于修改数据包的IP头信息;
- raw表:iptables是有状态的,即iptables对数据包有连接追踪(connection tracking)机制,而raw是用来去除这种追踪机制的;
- security表:最不常用的表(通常,iptables只有4张表,security表是新加入的特性),用于在数据包上应用SELinux。
这5张表的优先级从高到低是:raw、mangle、nat、filter、security。iptables的表是来分类管理iptables规则(rule)的,系统所有的iptables规则都被划分到不同的表集合中。
PREROUTING | POSTROUTING | FORWARD | INPUT | OUTPUT | |
---|---|---|---|---|---|
raw | Y | N | N | N | Y |
mangle | Y | Y | Y | Y | Y |
nat(SNAT) | N | Y | N | Y | N |
nat(DNAT) | Y | N | N | N | Y |
filter | N | N | Y | Y | Y |
security | N | N | Y | Y | Y |
Docker网络模型
Docker的四大网络模式
在使用docker run命令创建Docker容器时,可以使用–network选项指定容器的网络模式。Docker有以下4种网络模式:
- bridge模式,通过–network=bridge指定;
- host模式,通过–network=host指定;
- container模式,通过–network=container:NAME_or_ID指定,即joiner容器;
- none模式,通过–network=none指定。
在安装完Docker之后,Docker Daemon会在宿主机上自动创建三个网络,分别是bridge网络、host网络和none网络,可使用docker network ls命令查看。
1 2 3 4 5
# docker network ls NETWORK ID NAME DRIVER SCOPE bfc220c46e2a bridge bridge local 64b69f333f58 host host local 27fa93a9cf78 none null local
bridge模式
Docker在安装时会创建一个名为docker0的Linux网桥。bridge模式是Docker默认的网络模式,在不指定–network的情况下,Docker会为每一个容器分配network namespace、设置IP等,并将Docker容器连接到docker0网桥上。严谨的表述是,创建的容器的veth pair中的一端桥接到docker0上。
当成功创建了一个容器后,docker0上就挂接了一块新的网卡,这块网卡就是新建容器时创建的,并放在主机network namespace的veth pair一端。
在默认情况下,docker0的IP地址均为172.17.0.1(除非在DockerDaemon启动时自行配置),而接到docker0上的Docker容器的IP地址范围是172.17.0.0/24。连接在docker0上的所有容器的默认网关均为docker0,即访问非本机容器网段要经过docker0网关转发,而同主机上的容器(同网段)之间通过广播通信。
1
2
3
4
5
# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.255.0 U 100 0 0 eth0
发到主机上要访问Docker容器的报文默认也要经过docker0进行广播
1
2
3
4
# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
bridge模式为Docker容器创建独立的网络栈,保证容器内的进程使用独立的网络环境,使容器和容器、容器和宿主机之间能实现网络隔离。
host模式
连接到host网络的容器共享Docker host的网络栈,容器的网络配置与host完全一样。host模式下容器将不会获得独立的network namespace,而是和宿主机共用一个network namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。
host模式下的容器可以看到宿主机的所有网卡信息,甚至可以直接使用宿主机IP地址和主机名与外界通信,无须额外进行NAT,也无须通过Linux bridge进行转发或者进行数据包的封装。
container模式
创建容器时使用–network=container:NAME_or_ID模式,在创建新的容器时指定容器的网络和一个已经存在的容器共享一个network namespace,但不能和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过lo网卡设备通信。
Kubernetes的Pod网络采用的就是Docker的container模式网络。
none模式
none模式网络可以在容器创建时通过–network=none指定。none模式下的容器拥有自己的network namespace,但只有lo回环网络,没有网卡、IP、路由等信息,需要我们自己为Docker容器添加网卡、配置IP等。 这种类型的网络没有办法联网,属于完全封闭的网络。唯一的用途是有充分的自由度做后续的配置。
Docker网络
端口映射
在使用docker run的时候可以使用-P或者-p命令进行容器和主机之间的端口映射。使用-P(大写)不需要指定任何映射关系,默认情况下,Docker会随机将一个49000~49900的端口映射到内部容器开放的网络端口。使用-p(小写)则需要指定主机的端口应该映射到容器的哪个端口。
Docker容器端口映射原理都是在本地的iptable的nat表中添加相应的规则,将访问本机IP地址:hostport的网包进行一次DNAT,转换成容器IP:containerport。
1
2
3
4
5
6
7
8
9
10
11
12
iptables -t nat -nL
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
Chain DOCKER (2 references)
target prot opt source destination
DNAT tcp -- 0.0.0.0/0 127.0.0.1 tcp dpt:1234 to:172.17.0.2:80
如上所示,DNAT发生在DOCKER这条iptables链,它有两处引用,分别是PREROUTING链和OUTPUT链,意味着从外面发到本机和本地进程访问本机(由iptables匹配规则ADDRTYPE match dst-type LOCAL指定)的1234端口的包目的地址都会被修改成172.17.0.2:80。
访问外网
在默认情况下,容器可以访问外部网络的连接,因为容器的默认网络接口为docker0网桥上的接口,即主机上的本地接口。其原理是通过Linux系统的转发功能实现的(把主机当交换机)。
至于SNAT/MASQUERADE,Docker会自动在iptables的POSTROUTING链上创建形如下面的规则:
1
2
3
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
即从容器网段出来访问外网的包,都要做一次MASQUERADE,即出去的包都用主机的IP地址替换源地址。
DNS和主机名
容器中的DNS和主机名一般通过三个系统配置文件维护,分别是/etc/resolv.conf、/etc/hosts和/etc/hostname,其中:
- /etc/resolv/conf在创建容器的时候,默认与本地主机/etc/resolv.conf保持一致;
- /etc/hosts中则记载了容器自身的一些地址和名称;
- /etc/hostname中记录容器的主机名。
用户如果直接在容器内修改这三个文件会立即生效,但容器重启后修改又会失效。如果想统一、持久化配置所有容器的DNS,通过修改主机Docker Daemon的配置文件(一般是/etc/docker/daemon.json)的方式指定除主机/etc/resolv.conf的其他DNS信息:
1
2
3
4
5
6
{
"dns": [
"114.114.114.114",
"8.8.8.8"
]
}
同时,也可以在docker run时使用–dns=address参数来指定。
至于配置Docker容器的主机名,则可以在运行docker run创建容器时使用参数-h hostname或者–hostname hostname配置。
docker link:两两互联
首先声明,docker link是一个遗留的特性,在新版本的Docker中,一般不推荐使用。 docker link就是把两个容器连起来
1
docker run -d nginx --link=<container name or ID>:<alias>
link方式最大的便利在于容器可以使用容器名进行通信,而不需要依赖IP地址。不过,link方式仅解决了单机容器间点对点的互联问题。
接收容器的host文件里把源容器的ID(96f07804df65)、容器名(abc)和别名(source)都映射到172.17.0.2 IP地址。如果重启了源容器,则接收容器的/etc/hosts文件会自动更新源容器的新IP。
Docker的网络实现
在bridge模式下,Docker Daemon首次启动时会创建一个虚拟网桥,默认的名称是docker0,然后按照RPC1918的模型在私有网络空间中给这个网桥分配一个子网。针对由Docker创建的每一个容器,都会创建一个虚拟以太网设备(Veth设备对),其中一端关联到网桥上,另一端使用Linux的网络命名空间技术映射到容器内的eth0设备,然后在网桥的地址段内给eth0接口分配一个IP地址。
其中ip1是网桥的IP地址,Docker Daemon会在几个备选地址段里给它选一个地址,通常是以172开头的一个地址,这个地址和主机的IP地址是不重叠的。ip2是Docker在启动容器时在这个地址段选择的一个没有使用的IP地址,它被分配给容器,相应的MAC地址也根据这个IP地址,在02:42:ac:11:00:00和02:42:ac:11:ff:ff的范围内生成,这样做可以确保不会有ARP冲突。
启动后,Docker还将Veth设备对的名称映射到eth0网络接口。ip3就是主机的网卡地址。