Docker - 虚拟化网络设备
解释 Docker 网络背后的 netfilter、tun/tap、veth 和 Linux bridge 等虚拟网络设备。
在docker中,容器的网络隔离不仅用到了iptables对数据包的转发规则进行干预,还离不开网络设备的虚拟化技术,毕竟容器需要配上虚拟化的网络设备才能组成完整的虚拟网络。
Docker 网络设备拼图
Docker 单机网络不是靠一个神秘黑盒完成的,而是几种 Linux 网络能力拼起来的:
| 组件 | 像什么 | Docker 里主要做什么 |
|---|---|---|
| netfilter/iptables | 内核网络协议栈的插件点和配置工具 | 决定哪些包能转发、哪些包要 NAT/DNAT |
| veth pair | 一根虚拟网线,两端各一张网卡 | 一端在容器 netns,一端接到宿主机 bridge |
| Linux bridge | 虚拟交换机 | 把同一 Docker network 的容器接到同一个二层网络 |
| tun/tap | 用户态程序接入协议栈的虚拟网卡 | VPN、隧道等场景常用,帮助理解虚拟网卡模型 |
flowchart LR
subgraph Host["宿主机"]
B["docker0 / br-xxx<br/>Linux bridge"]
V1["veth-host-a"]
V2["veth-host-b"]
IPT["iptables<br/>FORWARD/NAT/DNAT"]
ETH["eth0<br/>物理网卡"]
end
subgraph C1["container A netns"]
A["eth0<br/>veth 容器端"]
end
subgraph C2["container B netns"]
C["eth0<br/>veth 容器端"]
end
A --- V1
C --- V2
V1 --- B
V2 --- B
B --> IPT --> ETH
网络协议栈
linux网络协议栈,无论被描述为是OSI五层模型,还是TCP/IP七层模型,都有一个共同点:应用层以下(从传输层开始,调用socket系统调用)的协议栈,都实现在内核态。
之所以这么设计,主要考虑的是数据安全隔离。虽然这会增加数据在内核态和用户态之间的复制成本,但避免了一个应用程序窃听/伪造另一个应用程序的通信内容。
因为送信人是内核,内核相比于用户态程序是安全可靠的,别的用户态程序无法插手。
但网络协议栈的设计也并不是完全没有给用户态程序任何操作空间,还是允许root用户配置一些回调函数(hook)对协议栈里的数据进行干预的。
PREROUTING/POSTROUTING、FORWARD、INPUT/OUTPUT就是这样五种callback,每种callback都可以设置多个callback函数,形成了链,因此也被称为chain。这套过滤器框架名为Netfilter,有一些上层应用能够更方便地设置Netfilter hook,比如Xtables家族,最出名的是iptables。
FORWARD:报文经过IP路由判定,如果确定不是发往本机的,就会直接转发出去。此时linux系统充当了数据包转发工具。
其实根据这么多年的开发经验,明显能感觉到netfilter相当于给网络协议栈引入了“插件机制”,使得网络协议栈的玩法变得多样化了起来,比如下面要介绍的VPN。而且由于几乎整个协议栈都处于内核态,想安装这些“插件”需要root权限。
虚拟网卡
普通的网卡驱动,可以理解为一端连着网络协议栈,另一端连着物理网卡。从一端收数据,发送到另一端。
tun、tap
虚拟网卡,一端连着网络协议栈,另一端连着用户态程序。
比如VPN,普通应用程序把数据包发送给网络协议栈,tun/tap模拟了虚拟的网卡,从网络协议栈里收到数据包之后,没有像实体网卡一样把数据包通过网线发送出去,而是发送给了VPN程序。VPN程序就可以用任何想要的方式操作数据包了。比如隧道技术(Tunneling),把发往ip A的报文(以IPsec协议)加密,封装到新的报文里,新报文的目标ip地址为B。
对于发送报文的应用程序来说,这些都是无感知的。
被tun/tap传输的数据一共经过了两次协议栈。
sequenceDiagram
participant App as 普通应用
participant Kernel as Linux 协议栈
participant Tun as tun/tap 虚拟网卡
participant VPN as 用户态 VPN 程序
App->>Kernel: 写 socket,目标 A
Kernel->>Tun: 路由到虚拟网卡
Tun->>VPN: 把 IP 包交给用户态
VPN->>Kernel: 加密/封装后再发往目标 B
veth
如果进行容器对容器的通信,一般使用veth(因为实体网卡叫eth,比如eth0,所以它叫veth),它代表一对儿使用网线连接的网卡,两个容器一边一个,数据可以直接发送过去。
交换机
现实中,设备一旦多了起来,再使用网线进行两两直连就爆炸了,所以出现了交换机。因此虚拟网络设备既然有虚拟网卡,也会有虚拟交换机。
bridge
注:本章节接下来的千言万语汇成一句话——linux主机相当于路由器(也是交换机),bridge网络相当于其下的一个子网。bridge内部通信的时候直接交换数据,同时bridge上的容器也可以和外部通信,此时linux主机相当于路由器,进行了NAT。进一步地,docker所谓publish port就是DNAT,提前在路由器上写好端口映射!
知道上面这些,下面的内容也可以不看。
linux上虚拟交换机的名字不是switch(交换机),而是bridge(网桥),但实际上就是虚拟交换机。linux bridge从kernel 2.2开始引入,无论是真实的物理网卡(eth0)还是虚拟网卡(tap、veth)都能和linux bridge配合工作。
bridge既然是个虚拟交换机,那么它的工作原理和数据链路层里介绍的交换机一模一样。只有一点点区别:普通交换机只会单纯地做二层转发,忽略ip地址,Linux Bridge 却还支持把发给它自身的数据包接入到主机的三层的协议栈中。
然而 Linux Bridge 与普通交换机的区别是除了显式接入的设备外,它自己也无可分割地连接着一台有着完整网络协议栈的 Linux 主机,因为 Linux Bridge 本身肯定是在某台 Linux 主机上创建的,可以看作 Linux Bridge 有一个与自己名字相同的隐藏端口,隐式地连接了创建它的那台 Linux 主机。因此,Linux Bridge 允许给自己设置 IP 地址,比普通交换机多出一种特殊的转发情况:如果数据包的目的 MAC 地址为网桥本身,并且网桥设置了 IP 地址的话,那该数据包即被认为是收到发往创建网桥那台主机的数据包,此数据包将不会转发到任何设备,而是直接交给上层(三层)协议栈去处理。
这样,linux bridge本身还充当了一点点路由器的功能。如果没有这个功能,只能做到接入到同一个bridge的虚拟设备之间相互通信。有了这个功能,当数据包被通过bridge扔到了主机的网络协议栈,只需要提前预支一些iptables NAT规则,就可以把原ip和mac替换为实际网卡eth0的ip和mac,从而实现外部通信。同理,数据包返回的时候还要把ip和mac换回来。
因此可以认为linux bridge是一个虚拟交换机,linux内核本身就是一个虚拟路由器,因为只有路由器才能做NAT转换。
- docker安装之后就会创建这样一个默认的linux bridge:docker0;
- 所有的容器如果没有显式指定network,都会接入docker0,相互之间可以通信;
- 如果创建了新的network,比如使用docker compose启动多个服务,相当于新建了一个linux bridge,组了一个独立的局域网;
- 如果某个容器把端口publish到主机上,此时iptables里的
FORWARDchain里的DOCKERchain就会多一条允许相关流量通过的ACCEPT规则;
而--network=host则是完全不创建虚拟网络设备,直接使用宿主机的网卡、网络栈,也不会拥有自己的ip,和在宿主机上直接起一个这样的进程没什么太大区别。优点是容器和外界通信不需要再做NAT,性能好,缺点就是没有隔离就不能避免网络资源冲突,比如端口冲突。

