当你执行 docker run -p 8080:80 nginx 后,从浏览器访问 http://localhost:8080 就能看到 Nginx 欢迎页。但数据包是怎么从浏览器到达容器内的 Nginx 的?
它穿越了 iptables DNAT 规则(8080→172.17.0.2<80>80>),经过 docker0 bridge,通过 veth pair 进入容器的 Network Namespace,最终到达 Nginx 进程的 socket。这条链路涉及 Network Namespace、veth pair、bridge、iptables 四个核心组件。
下面逐一拆解容器网络的每个环节,追踪数据包从宿主机到容器的完整路径。
容器网络的演进从“简单连通”到“安全策略”。Docker 早期用 bridge + NAT,所有容器挂 docker0、通过 MASQUERADE 出网、通过 DNAT 暴露端口,只解决单机通信。多主机通信需要覆盖网络,CoreOS 在 2015 年提出 CNI 标准(spec 初版随 Kubernetes 1.0 发布,2016 年作为独立项目入 CNCF 托管),将网络配置从运行时解耦,flannel、Calico、Cilium 各自实现不同方案。2019 年基于 eBPF 的 Cilium 用内核态程序替代 iptables 做策略和转发,性能远超规则线性匹配。
前置知识
Ch02 Linux Namespace 深入:Network Namespace 是容器网络的基础,每个容器拥有独立的网络栈
Linux 网络基础:veth pair、bridge、iptables/NAT、路由表
网络协议基础:TCP/IP、DNS、ARP
本章需要 Linux 网络基础(veth pair、bridge、iptables/NAT、路由表)和 TCP/IP 协议基础。
一、容器网络基础
1.1 Network Namespace 回顾
每个容器有独立的 Network Namespace,拥有自己的网络栈(Network Namespace 的细节见 Ch02)。刚创建的 Namespace 只有一个 DOWN 状态的 loopback,Docker 会为其接入 eth0:
# 容器的 Network NamespacePID=$(docker inspect -f '{{.State.Pid}}' mynginx)sudo nsenter -t $PID -n ip link# 1: lo: <LOOPBACK,UP,LOWER_UP># 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>1.2 容器网络的核心问题
Network Namespace 隔离了网络栈,但也带来了核心问题:如何让隔离的 Namespace 与外部通信?
二、veth pair
2.1 veth pair 原理
veth pair 是一对虚拟网卡,像一根”虚拟网线”,从一端发送的数据包会从另一端接收:
# 创建 veth pairsudo ip link add veth-host type veth peer name veth-container
# 将一端放入容器的 Network Namespacesudo ip link set veth-container netns ns1
# 在容器内配置 IPsudo ip netns exec ns1 ip addr add 10.0.0.2/24 dev veth-containersudo ip netns exec ns1 ip link set veth-container upsudo ip netns exec ns1 ip link set lo up
# 在宿主机配置 IPsudo ip addr add 10.0.0.1/24 dev veth-hostsudo ip link set veth-host up
# 测试连通性sudo ip netns exec ns1 ping -c 3 10.0.0.12.2 veth pair 的特性
| 特性 | 说明 |
|---|---|
| 成对出现 | 创建时必须指定两端 |
| 跨 Namespace | 一端在宿主,一端在容器 |
| MAC 地址 | 每端有独立的 MAC 地址 |
| MTU | 默认 1500 字节 |
| 性能 | 数据包在内核空间传递,延迟极低 |
2.3 veth pair 在容器中的使用
veth pair 的两端一端在容器命名空间里(通常叫 eth0),另一端在宿主机上(通常叫 vethXXX)。排查容器网络问题时,常要从容器内的 eth0 找到宿主机上的对端,或者反过来。靠的就是 interface index 的对应关系:
# 查看 Docker 容器的 veth pairdocker exec mynginx ip link show eth0# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500# link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
# 在宿主机上找到对应的另一端ip link | grep -A1 "veth"# 5: veth123@if2: <BROADCAST,MULTICAST,UP,LOWER_UP># link/ether 3a:2b:1c:4d:5e:6f brd ff:ff:ff:ff:ff:ff
# 注意:veth123 的 @if2 表示它的对端是 interface index 2# 容器内 eth0 的 index 正是 2三、Bridge
3.1 Bridge 原理
Bridge(网桥)是二层数据链路层设备,类似于物理交换机,它连接多个网络接口,根据 MAC 地址转发数据帧:
# 创建 bridgesudo ip link add br0 type bridgesudo ip link set br0 up
# 将 veth pair 的宿主端连接到 bridgesudo ip link set veth-host master br0
# 配置 bridge 的 IP 地址(作为容器的网关)sudo ip addr add 172.17.0.1/16 dev br03.2 Docker 的 bridge 网络模型
3.3 Bridge 的 MAC 学习
Bridge 维护一张 MAC 地址转发表,记录每个端口对应的 MAC 地址:
# 查看 bridge 的 MAC 转发表bridge fdb show br docker0
# 输出示例:# 02:42:ac:11:00:02 dev veth123 master docker0# 02:42:ac:11:00:03 dev veth456 master docker0# 33:33:00:00:00:01 dev docker0 self permanent四、iptables/NAT
4.1 iptables 链遍历顺序
容器网络中,数据包经过的 iptables 链有严格的先后顺序。理解这个顺序是排查 NAT 问题的前提。
需要先说明的是,本文用 iptables 命令演示,但现代发行版(Debian 12、RHEL 9 等)默认的 iptables 已经是 iptables-nft,背后走的是 nftables 后端;Docker 25+ 和 Kubernetes 1.29+ 也已原生支持 nftables 规则。链遍历的逻辑两者一致,只是查看命令不同:iptables -t nat -L -n -v 看到的是 iptables 视图,对应的 nftables 视图用 nft list ruleset 查看。排查时如果发现 iptables-save 输出和预期不符,先确认当前走的是哪个后端。
以入站流量(外部访问容器)为例,数据包进入宿主机后的链遍历路径:
具体流程:
- PREROUTING 链:数据包刚进入网络栈,路由判断之前。Docker 在这里挂载 DNAT 规则,将目标地址从
宿主机IP:8080改写为容器IP:80 - DOCKER 链:Docker 自定义的子链,从 PREROUTING 跳入。所有端口映射的 DNAT 规则都在这条链上,方便集中管理
- FORWARD 链:DNAT 完成后,数据包的目标地址已经是容器 IP,不属于本机,所以走 FORWARD 而非 INPUT
- POSTROUTING 链:数据包离开宿主机前最后的机会做源地址转换。出站 NAT(MASQUERADE)在这里执行
出站流量(容器访问外网)的路径类似,但不经过 DOCKER 链,只在 POSTROUTING 做 MASQUERADE:
# 查看完整的链跳转关系sudo iptables -t nat -L PREROUTING -n -v# Chain PREROUTING (policy ACCEPT)# DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ← 所有流量跳到 DOCKER 链
sudo iptables -t nat -L DOCKER -n -v# Chain DOCKER (2 references)# DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
sudo iptables -t nat -L POSTROUTING -n -v# Chain POSTROUTING (policy ACCEPT)# MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0排查容器网络问题时,用 iptables -t nat -L <链名> -n -v 逐条链检查,看数据包到底在哪条链上被处理(或被漏掉)。-v 参数会显示每条规则的匹配计数,如果某条规则计数为 0,说明流量根本没走到这里。
4.2 容器的出站 NAT
容器访问外网时,数据包的源地址需要从容器 IP(172.17.0.2)转换为宿主机 IP(192.168.1.100):
# Docker 创建的 NAT 规则sudo iptables -t nat -L POSTROUTING -n -v
# Chain POSTROUTING (policy ACCEPT)# MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0# ↑ 容器网段的所有出站流量做 MASQUERADE(动态 SNAT)为什么用 MASQUERADE 而不是静态 SNAT?两者都能做源地址转换,但有关键区别:
| 对比项 | MASQUERADE | SNAT |
|---|---|---|
| 源地址 | 自动选取出口网卡的 IP | 手动指定固定 IP |
| 接口变化 | 立即适应,无需修改规则 | 需要手动更新规则 |
| 性能 | 略慢(每次查出口 IP) | 略快(IP 已固定) |
| 适用场景 | DHCP 环境、多网卡 | 固定 IP 环境 |
Docker 选择 MASQUERADE 的原因很实际:宿主机的出口 IP 可能变化。比如笔记本在 WiFi 和有线之间切换,云服务器弹性 IP 变更,或者多网卡场景下出口网卡不确定。MASQUERADE 每次发包时自动查找出口网卡的主 IP 做转换,不需要人工维护规则。如果用静态 SNAT(-j SNAT --to-source 192.168.1.100),IP 一变就得手动更新规则,否则容器立刻断网。
在生产环境中,如果宿主机 IP 确实固定不变,可以手动把 MASQUERADE 换成 SNAT 以获得微小的性能提升。但通常这个差距可以忽略不计,MASQUERADE 的自动适配能力更有价值。
4.3 容器的入站 DNAT
外部访问容器的端口时,数据包需要做 DNAT(目标地址转换):
# docker run -p 8080:80 nginx 创建的 DNAT 规则sudo iptables -t nat -L DOCKER -n -v
# Chain DOCKER (2 references)# DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80# ↑ 访问宿主机 8080 端口的流量转发到容器 80 端口4.4 完整的数据包路径
五、Docker 网络模式
5.1 四种网络模式
| 模式 | Namespace 策略 | 特点 | 性能 |
|---|---|---|---|
| bridge | 独立 NS + veth + bridge | 默认模式,有 NAT | 中等 |
| host | 共享宿主 NS | 无隔离,性能最好 | 最好 |
| none | 独立 NS,仅 loopback | 无网络 | — |
| container | 共享另一容器 NS | Pod 内共享 | 中等 |
# bridge 模式(默认)docker run -d nginx
# host 模式docker run --network=host -d nginx
# none 模式docker run --network=none -d nginx
# container 模式(共享另一个容器的网络栈)docker run --network=container:nginx_id -d myapp5.2 自定义 bridge 网络
# 创建自定义 bridge 网络docker network create --driver bridge --subnet 10.0.0.0/24 mynet
# 在自定义网络中运行容器docker run --network=mynet -d --name app1 nginxdocker run --network=mynet -d --name app2 redis
# 容器间可以通过名称通信(Docker DNS)docker exec app1 ping -c 3 app2# PING app2 (10.0.0.3): 56 data bytes六、CNI:容器网络接口
6.1 CNI 原理
CNI(Container Network Interface)是 CNCF 主办的通用容器网络接口规范,定义了容器网络配置的通用接口。Kubernetes、containerd、Podman 等都遵循它,但 CNI 本身独立于 Kubernetes:
{ "cniVersion": "0.4.0", "name": "bridge-network", "type": "bridge", "bridge": "cni0", "ipam": { "type": "host-local", "subnet": "10.244.0.0/24", "routes": [{"dst": "0.0.0.0/0", "gw": "10.244.0.1"}] }, "dns": {"nameservers": ["8.8.8.8"]}}6.2 CNI 插件的工作流程
6.3 常见 CNI 插件
| CNI 插件 | 类型 | 特点 |
|---|---|---|
| Bridge | L2 bridge | 最简单的 CNI 插件 |
| Flannel | Overlay (VXLAN) | 简单易用,跨主机通信 |
| Calico | BGP(默认)/ eBPF(可选) | 高性能,网络策略;eBPF 数据平面需显式启用 |
| Cilium | eBPF | 高性能,可观测性 |
| Weave | Overlay (加密) | 简单,加密通信 |
6.4 Pod 网络模型
Kubernetes 的 Pod 网络模型:同一 Pod 内的所有容器共享 Network Namespace:
# Pod 内的 pause 容器创建 Network Namespace# 其他容器加入 pause 的 Network Namespace
# 效果:# 1. 所有容器共享同一个 IP 地址# 2. 所有容器共享同一个端口空间# 3. 容器间可以通过 localhost 通信# 4. 容器间可以通过 IPC 通信七、动手实践
7.1 手动创建容器网络
#!/bin/bash# 手动创建容器网络(模拟 Docker bridge 模式)
# 1. 创建 Network Namespacesudo ip netns add container1
# 2. 创建 veth pairsudo ip link add veth-host1 type veth peer name veth-container1
# 3. 将一端放入容器 NSsudo ip link set veth-container1 netns container1
# 4. 创建 bridgesudo ip link add br-container type bridgesudo ip link set br-container upsudo ip addr add 10.0.0.1/24 dev br-container
# 5. 连接 veth 到 bridgesudo ip link set veth-host1 master br-containersudo ip link set veth-host1 up
# 6. 配置容器内的网络sudo ip netns exec container1 ip addr add 10.0.0.2/24 dev veth-container1sudo ip netns exec container1 ip link set veth-container1 upsudo ip netns exec container1 ip link set lo upsudo ip netns exec container1 ip route add default via 10.0.0.1
# 7. 配置 NATsudo iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -j MASQUERADEsudo sysctl -w net.ipv4.ip_forward=1
# 8. 测试连通性sudo ip netns exec container1 ping -c 3 8.8.8.8
# 9. 清理sudo ip netns delete container1sudo ip link delete br-containersudo iptables -t nat -D POSTROUTING -s 10.0.0.0/24 -j MASQUERADE7.2 网络诊断脚本
#!/bin/bash# 容器网络诊断脚本
CONTAINER=$1PID=$(docker inspect -f '{{.State.Pid}}' $CONTAINER 2>/dev/null)
if [ -z "$PID" ]; then echo "Container not found" exit 1fi
echo "=== 容器网络配置 ==="echo "Container: $CONTAINER (PID: $PID)"echo ""
echo "1. 网络接口"sudo nsenter -t $PID -n ip addr
echo ""echo "2. 路由表"sudo nsenter -t $PID -n ip route
echo ""echo "3. DNS 配置"sudo nsenter -t $PID -n cat /etc/resolv.conf
echo ""echo "4. 连通性测试"sudo nsenter -t $PID -n ping -c 2 -W 2 8.8.8.8 2>&1 | tail -3
echo ""echo "5. iptables NAT 规则"sudo iptables -t nat -L DOCKER -n -v 2>/dev/null | grep "$CONTAINER"
echo ""echo "6. veth pair 对应"sudo nsenter -t $PID -n ip link show eth0 2>/dev/null | grep "link/ether"八、容器网络性能优化
8.1 网络性能瓶颈
容器网络的性能瓶颈主要在三个地方:
| 瓶颈 | 原因 | 优化方案 |
|---|---|---|
| veth pair 开销 | 每个包经过两次内核协议栈 | 使用 host 网络或 SR-IOV |
| bridge 转发 | 软件二层转发 | 使用 OVS 硬件卸载 |
| iptables NAT | 规则匹配开销 | 使用 IPVS 或 eBPF 替代 |
8.2 网络模式性能对比
# 性能基准测试# 1. bridge 模式(默认)docker run --network=bridge --rm alpine iperf3 -c 192.168.1.1# 吞吐量: ~8 Gbps
# 2. host 模式docker run --network=host --rm alpine iperf3 -c 192.168.1.1# 吞吐量: ~10 Gbps (接近原生)
# 3. 性能差距来源# bridge 模式: veth pair + bridge + iptables NAT# host 模式: 直接使用宿主网络栈8.3 Cilium eBPF 网络优化
Cilium 用 eBPF 程序替代 iptables 做网络转发和策略执行。要理解它为什么快,需要先看 iptables 的性能瓶颈在哪里,再看 eBPF 在哪些钩子点介入、数据路径有什么不同。
iptables 的性能瓶颈
iptables 的核心问题是规则线性匹配。每条规则按顺序逐条检查,直到命中或遍历完整个链。规则少的时候没问题,但 Kubernetes 集群中 Service 数量动辄上千,对应的 iptables 规则可能数万条:
# 一个中等规模集群的 iptables 规则数量sudo iptables-save | wc -l# 50000+ (kube-proxy iptables 模式下每个 Service 约产生 8 条规则,5 万条对应约 6000 个 Service)
# 每个包都要遍历这些规则,O(n) 复杂度# 即使大部分规则与当前包无关,也必须逐条检查此外,iptables 的连接跟踪(conntrack)和 NAT 处理都在内核协议栈的固定位置执行,数据包必须走完整个协议栈路径,无法跳过不必要的处理阶段。
eBPF 的三个钩子点
Cilium 在三个关键位置挂载 eBPF 程序,分别拦截不同阶段的数据包:
| 钩子点 | 挂载位置 | 处理时机 | Cilium 用途 |
|---|---|---|---|
| XDP | 网卡驱动层 | 数据包刚从网卡收到,尚未创建 skb | DDoS 防护、早期丢包 |
| tc | 流量控制层(qdisc) | skb 创建后,进入协议栈之前/之后 | 服务负载均衡、NAT、策略执行 |
| cgroup | cgroup 钩子 | 进程发起 socket 操作时 | socket 级别策略、连接跟踪 |
数据路径对比:iptables vs eBPF
iptables 模式下,一个数据包从网卡到容器要走的完整路径:
Cilium eBPF 模式下,tc 钩子直接在数据包进入协议栈前完成转发决策,跳过了中间大量处理:
为什么 eBPF 更快
性能差距的根源不是”eBPF 本身比 iptables 快”,而是数据路径的根本不同:
-
O(1) 查找 vs O(n) 遍历:eBPF 程序用哈希表存储 Service 后端映射,一次查找就能定位目标,而 iptables 必须逐条规则匹配。规则越多,差距越大
-
跳过协议栈中间层:tc eBPF 在数据包进入协议栈前就完成 DNAT 和转发决策,数据包不需要经过 conntrack 查找、iptables 规则遍历、FORWARD 链处理等阶段。这减少了多次上下文切换和表查找
-
XDP 早期拦截:对于 DDoS 防护等场景,XDP 在网卡驱动层就丢弃恶意包,连 skb 都不创建。iptables 至少要等数据包进入协议栈才能处理
-
socket 级短路:cgroup eBPF 可以在应用发起 connect() 时就确定后端 Pod,直接建立连接,跳过整个内核侧的 Service 负载均衡流程
# Cilium 完全替代 kube-proxy 的安装方式helm install cilium cilium/cilium \ --namespace kube-system \ --set kubeProxyReplacement=strict
# 验证 eBPF 程序已挂载cilium bpf lb list# 展示 Service 后端的哈希表,查找复杂度 O(1)
# 查看 tc 钩子上的 eBPF 程序tc filter show dev eth0 ingress# filter protocol all pref 1 handle 0x1 bpf ...eBPF 程序由内核验证器(verifier)检查安全性,保证不会死循环或越界访问。但这也意味着 eBPF 程序的复杂度有限制(指令数上限、栈大小限制),不能做太复杂的逻辑。Cilium 通过程序链(tail call)机制绕过单程序限制,将逻辑拆分到多个 eBPF 程序中串联执行。
附、实践:手工搭建容器网络
本节用 ip 命令手工搭建一个最小容器网络,理解 veth pair、Network Namespace、IP 配置的完整链路。所有命令需要 root 权限。
附.1 创建 Network Namespace
ip netns add ns1ip netns add ns2ip netns listns2ns1Network Namespace 是容器网络的隔离基础,每个容器拥有独立的网络栈(接口、路由表、iptables 规则)。
附.2 创建 veth pair
veth pair 是一对虚拟网卡,像一根网线的两端,从一端发送的数据包会从另一端收到:
ip link add veth1 type veth peer name veth2ip link show veth127: veth1@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000 link/ether 12:30:f5:ac:41:b2 brd ff:ff:ff:ff:ff:ff附.3 将 veth2 移入 ns1
ip link set veth2 netns ns1
# 确认 veth2 已在 ns1 中ip netns exec ns1 ip link show1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:0026: veth2@if27: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000 link/ether 2e:a3:f7:94:33:a6 brd ff:ff:ff:ff:ff:ff link-netnsid 0veth2 的 link-netnsid 0 表示它连接到对端(veth1)所在的 Network Namespace。
附.4 配置 IP 地址并启用接口
# 宿主端ip addr add 10.0.0.1/24 dev veth1ip link set veth1 up
# ns1 端ip netns exec ns1 ip addr add 10.0.0.2/24 dev veth2ip netns exec ns1 ip link set veth2 upip netns exec ns1 ip link set lo up附.5 测试连通性
ping -c 2 10.0.0.2--- 10.0.0.2 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 1001msrtt min/avg/max/mdev = 0.064/0.633/1.203/0.569 ms宿主机通过 veth1 → veth2 与 ns1 中的网络栈通信,这就是容器网络的最小模型。Docker 的 bridge 网络模式在此基础上增加了 bridge(连接多个容器)和 iptables NAT(访问外网)。
实验结束后清理:ip link del veth1; ip netns del ns1; ip netns del ns2
九、本章小结
上一章讨论了容器的安全加固机制。
| 组件 | 功能 | 所在层 |
|---|---|---|
| Network Namespace | 网络栈隔离 | 内核 |
| veth pair | 跨 Namespace 通信 | 数据链路层 |
| bridge | 二层转发 | 数据链路层 |
| iptables/NAT | 地址转换 | 网络层 |
| CNI | 网络配置接口 | 插件层 |
容器网络问题排查时,先确认数据包走到了哪一步:tcpdump 抓宿主机接口确认 DNAT 是否命中,nsenter -n 进入容器 NS 确认 veth 对端是否收到,iptables -t nat -L -n -v 查看规则匹配计数。90% 的容器网络问题都是 iptables 规则缺失或 veth 未正确连接 bridge。
容器网络的核心是 Network Namespace + veth pair + bridge + iptables 的组合。CNI 插件将这些组件的配置自动化,让你不需要手动创建 veth pair 和配置路由。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






