5. Docker 网络栈协议
- NIC: Network Interface Card, 物理网卡, 真正的硬件;
- net_device: Linux 内核里描述"一张网卡"的统一抽象, 虚拟和物理共用同一个数据结构;
- 虚拟网卡: 没有对应物理硬件、纯软件模拟的 net_device, 例如 lo / veth / tap / tun / bridge;
- tap / tun: 给用户态进程收发包用的虚拟网卡, VM 和 VPN 的标配;
- netns: Linux network namespace, 内核提供的网络栈隔离机制;
- veth pair: virtual ethernet pair, 一对虚拟网卡, 一端发, 另一端收, 是连接两个 netns 的通道;
- bridge: 内核虚拟二层交换机, 把若干网卡拉到同一广播域;
- DNAT / SNAT: destination / source NAT, iptables 用来改包的 dst/src IP, 是端口映射和"出口走主机 IP"的核心;
前置 A: 物理网卡 (NIC) 与虚拟网卡到底是什么
聊 Docker 网络之前必须先把"网卡"这个词拆开, 否则后面 veth, tap, bridge 这些会一片混乱;
物理网卡 (NIC)
物理网卡就是字面意义上的硬件, 一块 PCIe / USB / on-board 设备, 它干的事情非常底层:
- 把内存里的字节序列编码成线路上的电信号 / 光信号 (发);
- 把线路上的信号解码成字节, 通过 DMA 写进内存, 再触发中断告诉 CPU “有包来了” (收);
- 自带一个出厂烧死的 MAC 地址, 用来在二层网络里被定位;
- 现代网卡还会做一堆 offload: checksum 算硬件里、TCP segmentation offload (TSO)、receive side scaling (RSS) 多队列分流等等;
操作系统层面, Linux 用一个叫 struct net_device 的内核数据结构来代表"一张网卡"; 物理网卡有一个驱动, 驱动负责把 net_device 上的发包请求翻译成具体硬件操作;
虚拟网卡
关键洞察: struct net_device 不一定要后面挂硬件;
只要写一段内核代码, 实现 net_device 要求的几个回调 (发包时怎么处理、收包时怎么调用协议栈), 就凭空造出一张"网卡"; 这种没有物理硬件背书、纯软件模拟的 net_device 就是虚拟网卡;
对内核的 TCP/IP 协议栈来说, 虚拟网卡和物理网卡没有任何区别:
- 都能分配 IP 地址;
- 都能加路由表条目;
- 都能跑 ARP, ICMP, TCP, UDP;
- 都能被 iptables / tcpdump 看到;
唯一的区别在于: 发包的时候, 包没去到线上, 而是被另一段内核代码拦下来转手处理掉了;
类比一下: 协议栈是水管, net_device 是水龙头, 龙头后面是真水管 (物理网卡) 还是另一根管子 (虚拟网卡), 协议栈不关心也看不见;
Linux 里常见的虚拟网卡种类
| 类型 | 干嘛用 | 举例 |
|---|---|---|
lo (loopback) |
自发自收, 每个 netns 必有一张 | 127.0.0.1 走的就是 lo |
veth |
一对网卡像虚拟双绞线, 一端发另一端收 | 连接 container netns 和宿主 |
tap |
给用户态进程一个收发包接口, 可以读到完整以太网帧 | VM 的"虚拟网卡"通常是一张 tap |
tun |
类似 tap 但只到 IP 层, 用户态读到的是 IP 包不带二层头 | OpenVPN, WireGuard 客户端 |
bridge |
内核虚拟二层交换机, 自己也是一种 net_device | docker0, br0 |
bond |
把多张物理网卡聚合成一张, 做负载/冗余 | 服务器双网口绑定 |
vlan |
给物理网卡打 VLAN tag 后派生出的子接口 | eth0.100 是 eth0 的 VLAN 100 |
dummy |
永远收不到包, 拿来挂 IP 做配置占位 | 一些虚拟路由方案会用 |
Linux 网络栈的强大之处就在于: 一切网络设备都被抽象成 net_device, 物理和虚拟混在一起用, 协议栈一视同仁; 所以容器、VM、VPN、SDN, 底子上都是在玩 net_device 的不同拼装方式;
前置 B: 看懂 ifconfig / ip a 的输出
ifconfig 是老一辈工具, 现在 Linux 标准做法是 ip 命令族 (ip addr, ip link, ip route), 输出更全, 但内容含义一致, 这里都讲;
示例输出
1 | $ ip a |
字段含义逐项拆
1:,2:… — 接口索引, 内核给的连续 ID, 用来在内部相互引用;<UP,BROADCAST,LOWER_UP,...>— flags, 几个常见值:UP: 管理员把这张网卡置为启用 (ip link set up);LOWER_UP: 物理层有载波, 线插着且对端在线 (拔了网线就没了);BROADCAST/MULTICAST: 支持广播 / 组播;PROMISC: 混杂模式, 收所有包不过滤 (tcpdump 抓包会临时开);NO-CARRIER: 接口 UP 但物理层没信号 (空 bridge 经常这样);
mtu 1500— Maximum Transmission Unit, 这张网卡一次能发的最大包字节数; 以太网默认 1500, loopback 65536, VXLAN 因为多了封装头通常调到 1450;qdisc fq_codel— queueing discipline, 这张网卡的发包排队算法, 一般默认fq_codel或pfifo_fast;state UP / DOWN / UNKNOWN— 当前可用状态;link/ether 08:00:27:1a:2b:3c— MAC 地址,link/loopback的 MAC 全 0 因为没意义;brd ff:ff:ff:ff:ff:ff— 广播 MAC;inet 192.168.1.42/24— IPv4 地址 + 子网掩码长度;inet6 fe80::.../64— IPv6 地址,fe80::开头是 link-local;scope global / host / link— 地址作用域, host 只在本机有效, link 在本链路有效, global 可以路由到外面;master docker0— 这张网卡挂在 docker0 这个 bridge 上 (类比"插在某个交换机上");vethXXX@if4— 这是 veth pair 一端,@if4表示对端的接口索引是 4;link-netnsid 0— 对端在 netns id 0 里 (说明这张 veth 跨 netns);
Linux 接口命名套路
接口名前缀就藏着它的来源, 看名字就知道是什么:
| 前缀 | 含义 |
|---|---|
lo |
loopback, 回环, 每个 netns 一张 |
eth0 (老风格) |
以太网, BIOS 时代命名, 现代 systemd 默认不用了 |
enp0s3, eno1, ens3 |
Predictable Network Interface Names, 基于 PCI 总线/插槽位置, 重启后名字稳定 |
wlp3s0, wlan0 |
无线网卡 |
docker0, br-XXXX |
Docker 创建的 bridge |
vethXXXX |
veth pair 一端 |
tun0, tap0 |
VPN / VM 用的 tun/tap 设备 |
vxlan.calico, flannel.1 |
overlay 网络的 VXLAN 接口 |
wg0 |
WireGuard VPN |
看到一个不认识的接口名, 先看前缀; 看到 <...> 里的 flags 缺 LOWER_UP 八成是物理层断了; 看到一张 veth 没 master, 八成是没挂上 bridge, 这是 Docker 出毛病常见现象;
前置 C: VM 和 Container 的网卡设计差在哪
VM 和 Container 都给应用一个"独立网络"的错觉, 但底层手法完全不同, 理解这点有助于看懂为什么 Docker 选用 netns + veth, 而不是抄 VM 那套;
VM 的网卡 (经过 hypervisor)
VM 里跑的是完整一套独立的 guest OS, guest 内核有自己的协议栈, 它认得的"网卡"必须看起来像真硬件; hypervisor 提供这层假象, 主流有三种做法, 性能/复杂度递增:
| 方式 | 实现 | 性能 | 适用 |
|---|---|---|---|
| Emulated NIC | hypervisor 用软件假装一块 e1000/rtl8139, guest 加载真驱动跟它讲话 | 慢, 每个 register 访问都要 trap 到 host | 兼容性最好, 默认选项 |
| Paravirtualized (virtio-net) | guest 装 virtio 驱动, host/guest 之间共享 ring buffer 直接交换 packet, 跳过完整硬件模拟 | 快得多 | 现代 Linux VM 默认 |
| SR-IOV / PCIe passthrough | 物理网卡的 Virtual Function 直接挂给 guest, 中间没 hypervisor 经手 | 接近裸机 | 高性能场景, 需要硬件支持 |
不管哪种, host 侧通常还要有一个与之配对的设备把流量送出去, 标配组合是:
1 | Guest VM Host (Linux + KVM/QEMU) |
guest 视角看自己有一张 eth0, host 视角看多了一张 tap0, 两边由 hypervisor (qemu-kvm) 在内核 ring buffer 上 backed; 然后 host 把 tap0 挂到 bridge 上, bridge 再连物理网卡 — 流量就通了;
关键点: VM 的 guest 内核完全独立, 它不知道自己跑在虚拟里, 因此必须通过"模拟一张物理网卡"来骗它;
Container 的网卡 (共享 host kernel)
Container 没有 guest OS, 直接跑在 host kernel 之上; 这意味着根本不需要骗它"这是真硬件", 内核就一个, container 看到的"网卡" 就是 host 内核里的一个 net_device 而已, 区别只是在不同 netns 视图里;
1 | Container netns Host root netns |
container 内的 eth0 就是 veth pair 的一端, 没有任何 hypervisor 模拟层, 没有 ring buffer 中转, 包从 container 协议栈下来直接被内核挪给 host 端 veth, 整个过程没出过内核;
二者直接对比
| 维度 | VM | Container |
|---|---|---|
| 隔离层级 | 整套独立 guest OS + 独立内核 | 同一 host kernel + netns 视图隔离 |
| "网卡"本质 | hypervisor 模拟的硬件 (或直通物理 VF) | host 内核里的一个虚拟 net_device |
| 协议栈 | guest 跑自己的, host 跑自己的, 各自完整 | 只有一份协议栈 (host kernel), 多套配置 (多 netns) |
| 数据路径 | guest 协议栈 → 模拟设备 → ring buffer → host tap → bridge → 物理网卡 | container 协议栈 → veth → bridge → 物理网卡 |
| 性能开销 | emulated 高, virtio 中, SR-IOV 几乎无 | 极低 (无模拟无 trap) |
| 启动时间 | 秒到几十秒 (整套 OS 启动) | 毫秒到秒 |
| 隔离强度 | 强, guest 内核漏洞不影响 host | 弱, 共享 kernel, 一个 kernel CVE 全完 |
| 典型场景 | 跨 OS, 不信任的 workload, 强隔离 | 微服务, CI, 高密度部署 |
VM 给的是"假硬件", container 给的是"独立视图的同一个内核"; 这就是为什么 Docker 网络栈的全部技巧都是在 net_device + netns + veth + bridge 之间排列组合, 你下面看到的所有模式都不出这个圈;
前置 D: Linux network namespace
Docker 网络的本质是对内核 network namespace 的封装, 没有 namespace 就没有容器网络可言;
network namespace 是 Linux 提供的一种隔离手段, 每个 netns 拥有它自己的一整套网络栈:
- 自己的 network interface (网卡列表);
- 自己的 IP 地址和路由表;
- 自己的 ARP 邻居表;
- 自己的 iptables / nftables 规则视图;
- 自己的端口空间 (一个 netns 占了 80 端口不影响另一个 netns 也开 80);
- 自己的 socket 列表;
但底下复用的是宿主机同一个 Linux kernel, 所以 namespace 之间通信不走真实网络, 全靠内核内部转发;
每启动一个 container, Docker 就为它新建一个 netns; docker exec 进容器时, 你看到的 ip a, route -n, netstat 都是这个 netns 视角下的结果, 跟宿主机是隔离的;
前置 E: veth pair + bridge 是怎么把两个 netns 连起来的
两个 netns 想互通, 内核给的最常见工具组合是 veth pair + bridge:
- veth pair: 创建出来就是两张虚拟网卡, 一张丢进 container netns, 另一张留在宿主 netns; 这两张网卡像一根虚拟双绞线 — 一端发什么, 另一端就收到什么;
- bridge: 宿主侧那一端不能孤零零悬着, 要插到一个虚拟交换机 (bridge) 上, 这样多个容器的"宿主端 veth" 都挂在同一台 bridge 上, 就形成了一个虚拟二层网络, 容器之间可以互相 ping;
1 | 宿主机 root netns │ Container A netns |
所有 Docker 网络模式都是这两个原语的不同组合, 看完这张图剩下的章节就只是排列组合;
Docker 网络模式总览
Docker 内置六种网络驱动, 各自解决不同的隔离/连通性需求:
| 驱动 | 是否独立 netns | 是否独立 IP | 跨主机 | 典型用途 |
|---|---|---|---|---|
bridge (默认) |
✅ | ✅ (172.17.0.0/16) | ❌ | 单机多容器, 默认场景 |
host |
❌ (复用宿主) | ❌ (用宿主 IP) | — | 极致性能, 牺牲隔离 |
none |
✅ (空白) | ❌ | — | 完全离线容器 / 自定义网络 |
container |
❌ (复用别的容器) | ❌ | — | sidecar / Pod 模型 |
macvlan |
✅ | ✅ (LAN 同段) | ❌ | 容器要像物理机一样直挂 LAN |
ipvlan |
✅ | ✅ | ❌ | 同 macvlan, 但共享 MAC |
overlay |
✅ | ✅ | ✅ | 多机集群 (Swarm/K8s) |
下面逐个展开;
模式 1: bridge (默认模式)
docker run 不加 --network 时走的就是这条; 启动流程:
- 宿主机 boot 时 Docker daemon 创建一个名为
docker0的 bridge, 默认网段172.17.0.0/16; - 创建容器时, Docker 新建一个 netns, 拉一对 veth, 一端塞进容器叫
eth0, 另一端在宿主, 名字像vethXXXX@if5, 然后挂到docker0; - 给容器
eth0分一个172.17.0.X, 把默认网关指向docker0的地址172.17.0.1; - 在宿主机的 iptables 里加一条 SNAT 规则, 容器出网时把源 IP 改成宿主 IP, 这样 LAN/公网就能正常回包;
1 | Container (172.17.0.2) Host |
端口映射 -p 的本质
docker run -p 8080:80 看起来是"把容器 80 暴露到宿主 8080", 实现上是 iptables 的 DNAT:
1 | LAN 来包: dst = 192.168.x.y:8080 |
回包反向走 SNAT, 让 LAN 客户端以为自己一直在跟宿主 IP 聊天; 这就是为什么 docker run -p 必须由 daemon 来注入 iptables, 而不是容器自己能搞定;
默认 bridge 的坑: 容器名 DNS 不通
默认 docker0 上的容器只能通过 IP 互相访问, 不能用容器名; 想用 ping container_b 这种就得用 user-defined bridge (下一个章节);
模式 2: host 模式
docker run --network host:
- 不创建新的 netns, 容器进程直接复用宿主 root netns;
- 容器看到的
eth0, IP, 路由表, 全是宿主的; - 容器开
LISTEN 0.0.0.0:80就直接占用宿主的 80 端口;
| 优点 | 缺点 |
|---|---|
| 没有 NAT, 没有 veth, 性能最高 | 没有网络隔离, 端口冲突, 安全风险大 |
| 容器内程序看到的网络拓扑 = 宿主拓扑, 调试方便 | 跨容器协调端口很麻烦 |
适合场景: 高吞吐网络应用 (DPDK, 数据库 replica, 监控 agent), 或者明确不需要隔离的系统级容器;
模式 3: none 模式
docker run --network none:
- 容器有独立 netns, 但里面只有 lo 一张 loopback 网卡, 没有任何对外连通性;
- 容器内进程能跑, 但
curl谁都不通,ping 8.8.8.8也不通;
适合场景:
- 完全离线计算 (纯 CPU 跑加密/分析, 不允许偷数据出去);
- 自己手动用
ip netns搭复杂网络拓扑, 不想要 Docker 默认那套;
模式 4: container 模式 (共享 netns)
docker run --network container:<name>:
- 新容器不创建自己的 netns, 直接进入指定容器的 netns;
- 两个容器看到一模一样的网卡 / IP / 端口, 它们之间通过
localhost互访;
这其实就是 Kubernetes Pod 模型的底层原理: 一个 Pod 里多个 container 共享 netns, 主容器开 80, sidecar 用 localhost:80 就能直接连;
1 | Pod / Container Group |
模式 5: user-defined bridge (推荐生产用)
docker network create my-net 创建一个 user-defined bridge, 然后 docker run --network my-net ...;
虽然底层和默认 docker0 都是 bridge driver, 但有一系列重要差异, Docker 官方明确推荐生产环境用 user-defined bridge:
| 特性 | 默认 docker0 | user-defined bridge |
|---|---|---|
| 容器名 DNS 互访 | ❌ 只能用 IP | ✅ 内置 DNS, 容器名直接当 hostname |
| 网络可隔离 | ❌ 所有容器共用 docker0 | ✅ 不同 user-defined network 之间默认隔离 |
| 动态加入/退出 | 容器启停才行 | ✅ docker network connect/disconnect 在线切换 |
| 自定义子网 | ❌ 固定 | ✅ 可指定 subnet/gateway |
- 一个 docker-compose 项目自动创建一个 user-defined bridge, 项目内的服务用 service 名字互相调用, 不要硬编码 IP;
- 不同业务/环境之间用不同的 user-defined network, 保证网络层面就是隔离的;
模式 6: macvlan / ipvlan (容器 = LAN 设备)
前面所有 bridge 模式, 容器对外都是宿主在做 NAT, LAN 上别的机器看到的是宿主 IP; 有时候这不够, 比如:
- 老系统认 MAC 地址做 license;
- 监控/分析需要直接接收 LAN 二层广播 (DHCP, ARP, multicast);
- 想让容器拥有 LAN 上一个独立 IP, 跟物理机平起平坐;
这时候用 macvlan:
1 | Host eth0 (192.168.1.10) |
每个容器获得一个真实 MAC 地址, 看起来就像直接插在 LAN 交换机上的物理机;
ipvlan 思路类似但不分配新 MAC, 多个容器共用宿主 MAC, 在 L3 上做隔离; 适合云上虚机环境 — 很多云的虚机不允许接口学习多个 MAC, macvlan 用不了, 这时候 ipvlan 是替代方案;
| 模式 | 独立 MAC | 与宿主同网段 | 限制 |
|---|---|---|---|
macvlan |
✅ | ✅ | 物理交换机要允许 promiscuous, WiFi 通常不行 |
ipvlan |
❌ | ✅ | 不能在二层和宿主直接通信 (L2 mode 例外) |
模式 7: overlay (跨主机)
前面六种全都是单机内的方案, 如果两台不同物理机上的容器要互通, 单机 bridge 没办法, 你需要 overlay network;
overlay 的核心思路是: 在宿主之间建一条隧道, 把容器之间的 L2 帧封进 UDP 包扔过去:
1 | Container A (10.0.0.5) Container B (10.0.0.6) |
实现协议是 VXLAN (RFC 7348), 默认 UDP 4789 端口; 跨主机的服务发现通常需要一个 KV 存储 (Swarm 自带, K8s 用 etcd) 来维护"哪个 IP 在哪台宿主"的映射;
overlay 几乎不会让你单独 docker run 来玩, 它跟编排器 (Docker Swarm / Kubernetes) 是绑定使用的;
容器间通信的几条路径
把上面的模式串起来看, 两个容器要互通有几条典型路径:
| 场景 | 路径 |
|---|---|
| 同一 user-defined bridge | 容器 A eth0 → veth → bridge → veth → 容器 B eth0 (纯内核转发, 不走 NAT) |
| 默认 docker0 不同容器 | 同上, 但只能用 IP, 没有 DNS |
| 不同 user-defined network | 默认隔离, 需要 docker network connect 把容器加入对方网络 |
| host 模式 ↔ bridge 模式 | 通过 docker0 网关 / 宿主 IP 走 |
| 跨主机 (overlay) | 容器 → overlay → VXLAN 封包 → 物理网 → 对端宿主 → 解包 → 对端容器 |
| 同 Pod (container 模式) | localhost, 共享 netns, 不出网卡 |
排查 Docker 网络的常用命令
1 | # 看所有网络 |
- 单机, 一般业务 → user-defined bridge (不要用默认 docker0);
- 要极致网络性能 → host, 但你得接受没有隔离;
- 要完全离线 / 自己搭网络 → none;
- K8s Pod 这种共享网络的 sidecar → container;
- 容器要像物理机一样上 LAN → macvlan (云上用 ipvlan);
- 多台宿主集群联通 → overlay, 通常配合 Swarm 或 K8s;
- 凡是涉及端口映射
-p, 背后一定是 iptables 在做 DNAT, 别忘了去iptables -t nat -L验证;
