一、起因
HN 上一篇看起来分数不高的教程(HN 48870605,2p / 0c,2026-07-11 morning 浮上来),点开之后发现是 Ivan Velichko 的长文。Ivan 在 SUSE 容器团队做工程师,他的 labs.iximiuz.com 个人站几乎每篇都是 5,000 字以上的"从零手搓"系列 —— 之前写过 Linux cgroups、Linux scheduling、Linux namespaces 拆解,文章都是 server-side rendered,带完整 bash 命令和分章节实验设计。
这次这篇"How Container Networking Works: Building a Bridge Network From Scratch"和 Docker / Kubernetes 日常教程不一样 —— 它不是讲怎么用 docker run / kubectl expose,而是直接从 Linux 内核的最底层设施开始:network namespace → veth pair → bridge device → IP routing → iptables NAT。一路手动把容器网络在用户态用 ip link + iptables 搓出来。
我之前对 Docker bridge network mode 的理解一直停在"魔法"层面,这篇让我真的把每个网包走过几遍(局限:还没完全搞懂 conntrack 内部,见第五节)。下面是我照着跑通的核心过程 + 自己补的几个实验,以及为什么我觉得这个工作流特别值得团队新人上手。
二、整体思路
容器网络需要解决三个问题:
- 隔离:每个容器要有自己的网络栈(独立 IP、独立路由表、独立 iptables)
- 互联:同一台机器上的容器之间要能互相 ping 通
- 外部:容器要能访问外网(出方向 NAT),外部也要能访问容器指定的端口(入方向 DNAT + port publishing)
Linux 内核分别用三套机制对应:
| 问题 | Linux 机制 | 命令 |
|---|---|---|
| 隔离 | network namespace (netns) |
ip netns add |
| 互联(同 host 内) | virtual Ethernet pair (veth) + bridge |
ip link add ... type veth peer ... / ip link add ... type bridge |
| 外部 | IP routing + iptables MASQUERADE / DNAT | iptables -t nat ... |
文章也是按这三块往下走的。先把"网络环境元素"摸清楚,再一个一个手搓。
三、我具体做了什么
⚠️ 文章原文一开始就警告 "Running the below commands directly on your computer's host OS may be harmful"。我全程在 Ubuntu 22.04 的 VM 里跑(不是 host),所有命令都清理干净(namespace / bridge / veth / iptables 规则跑完都
ip netns del+ip link del拆掉)。
3.1 摸清楚当前的网络环境
先把当前系统的"网络环境" dump 出来 —— 这是个固定动作,几乎所有后续实验都用得上:
#!/usr/bin/env bash
echo "# Network devices"
ip link list
echo -e "\n# Route table"
ip route list
echo -e "\n# iptables rules"
iptables --list-rules
echo -e "\n# netns"
ip netns list
我机器上的输出片段(简化):
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 ...link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...link/ether 02:00:00:xx:xx:xx brd ff:ff:ff:ff:ff:ff
关键点:默认情况下只有一个 network namespace(default / root)。
3.2 第一个 network namespace
ip netns add ns1 创建第一个 namespace:
sudo ip netns add ns1
sudo ip netns exec ns1 ip link list
执行后,ns1 里只能看到 lo 这一个 interface,而且 state 是 DOWN。ip netns exec 后面所有命令都在那个 namespace 里跑 —— 这是手动模拟"容器内"命令的核心用法。
然后:
sudo ip netns exec ns1 ip link set lo up
sudo ip netns exec ns1 ip addr add 10.0.0.1/24 dev lo
sudo ip netns exec ns1 ip route list
路由表里只有 local 10.0.0.0/24 dev lo table local。这个 namespace 现在有了自己的 lo、自己的 IP、自己的路由表,完全和外面的 root namespace 隔离。
3.3 用 veth pair 把容器和 host 连起来
ns1 现在还是个孤岛,需要"网络线"接到 host。Linux 提供 veth(virtual Ethernet pair)—— 一个 veth 创建出来一定是两个端,一端塞进 ns1,另一端留在 root namespace:
sudo ip link add veth0 type veth peer name veth1
sudo ip link set veth1 netns ns1sudo ip link set veth0 up
sudo ip addr add 10.0.0.100/24 dev veth0
sudo ip route add 10.0.1.0/24 via 10.0.0.1 dev veth0sudo ip netns exec ns1 ip link set veth1 up
sudo ip netns exec ns1 ip addr add 10.0.1.1/24 dev veth1
sudo ip netns exec ns1 ip route add 10.0.0.0/24 via 10.0.1.100 dev veth1
veth pair 的精髓:两边虚拟网卡在协议层是直接连通的(就是一根虚拟网线),内核负责把两边的 packet 互相转发。所以 veth0(host 端)和 veth1(ns1 端)互相 ping 应该通(局限:我这边第一次 ping 没通,因为忘记 set up,这种"接口默认 down"的坑见第五节)。
通了之后:
sudo ip netns exec ns1 ping 10.0.0.100
PING 10.0.0.100 (10.0.0.100) 56(84) bytes of data.
64 bytes from 10.0.0.100: icmp_seq=1 ttl=64 time=0.052 ms
3.4 第二个 namespace + bridge 把两个"容器"互联
现在再造第二个 ns2,给 ns2 一个 veth2 ↔ veth3,两端 veth2 在 host 上。问题来了:ns1 和 ns2 之间怎么通?
直接连网线?不行,n 个 namespace 需要 n(n-1)/2 根网线。
正解是 host 上一个虚拟交换机(Linux bridge)—— bridge 是个 L2 设备,veth pair 接进 bridge 就像接进交换机:
sudo ip link add br0 type bridge
sudo ip link set br0 upsudo ip link set veth0 down
sudo ip link set veth2 down
sudo ip link set veth0 master br0
sudo ip link set veth2 master br0
sudo ip link set veth0 up
sudo ip link set veth2 up
(注意:master 之前要先把 veth down,否则 bridge 会报 "device is busy"。这是第一次实操最容易踩的一步。)
ns1、ns2 各自的 IP 路由也要调整,从"默认路由指 veth0"改成"走 bridge 上的 gateway":
# host 上,bridge 当 gateway
sudo ip addr add 10.0.0.254/24 dev br0# ns1
sudo ip netns exec ns1 ip addr flush dev veth1
sudo ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth1
sudo ip netns exec ns1 ip route add default via 10.0.0.254 dev veth1# ns2 同样
sudo ip netns exec ns2 ip addr add 10.0.0.2/24 dev veth3
sudo ip netns exec ns2 ip route add default via 10.0.0.254 dev veth3
现在 ns1 和 ns2 能互通:
sudo ip netns exec ns1 ping 10.0.0.2
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.082 ms
3.5 让容器访问外网(NAT MASQUERADE)
容器能内通了,但 curl https://iximiuz.com 还是 timeout —— 因为 ns1 出去的全部走 10.0.0.254(host 上的 bridge IP),host 把"非本地"包往 eth0 转,但 eth0 上没有"从我发出的 10.0.0.x 源 IP"的路由规则会直接被丢(或被 ISP 拒,因 RFC1918 IP 不能上公网)。
解决:iptables MASQUERADE 把容器的源 IP 改成 host 的 eth0 IP(NAT 出口):
sudo iptables -t nat -A POSTROUTING \-s 10.0.0.0/24 ! -o br0 \-j MASQUERADE
然后 ns1 里:
sudo ip netns exec ns1 ip route add default via 10.0.0.254 dev veth1
sudo ip netns exec ns1 curl -s https://example.com | head -1
<!doctype html>
通了。注意 ! -o br0 这个条件 —— 容器之间互 ping 不能走 NAT(Pitfall #),否则 conntrack 会把内网包再走一遍 nat table(坑点,见第五节)。
3.6 反向:外部访问容器端口(port publishing)
出方向 NAT 通了,反过来:外部怎么访问容器?比如 ns1 跑了一个 HTTP server 在 10.0.0.1:8080,host 上 curl http://localhost:8080 要能转过去:
sudo iptables -t nat -A PREROUTING \-d 10.0.0.1 \-p tcp --dport 80 \-j DNAT --to-destination 10.0.0.1:8080
sudo iptables -t nat -A POSTROUTING \-d 10.0.0.1 \-p tcp --dport 80 \-j MASQUERADE
PREROUTING 是入方向先改目的地址;POSTROUTING MASQUERADE 再把源也改回 bridge IP(否则返回包会绕过 bridge 直接走 veth3 → 容器收包源是 host IP,容器路由表里没这条规则)。
跑一下验证:
sudo ip netns exec ns1 python3 -m http.server 8080 &
curl -s http://localhost | head -3
<!DOCTYPE html>
...
3.7 对照:Docker 到底干了什么
Ivan 的最后一节把这套手动跟 docker run 一一对照:
| 手动命令 | Docker / Podman 内部对应 |
|---|---|
ip netns add ns1 |
创建容器时 unshare(CLONE_NEWNET) |
ip link add veth... |
容器启动时 dockerd 在 host 上创建 veth pair |
ip link set ... master br0 |
默认 Docker bridge network (docker0 / br-xxxx) |
iptables -t nat MASQUERADE |
Docker daemon 配置的 iptables 链(DOCKER,POSTROUTING) |
iptables DNAT --to-destination |
docker run -p 80:8080 |
我对 Docker bridge mode 之前一直搞不清楚的一点:Docker 默认 --network=bridge 是这家 host 上的"全局" bridge network,容器之间默认能互 ping 通(限制用 --network=none 才完全隔离),而且 Docker daemon 会自动写一堆 iptables 规则 —— 这些规则在 iptables-save | grep -E 'DOCKER|docker0' 里可以看到。
四、效果与数据
跑完之后,我对容器网络的理解从"用 docker run 调参"升级到能现场 debug 真实问题:
| 场景 | 之前排查方式 | 现在排查方式 |
|---|---|---|
| 容器之间 ping 不通 | 重启 dockerd(玄学) | ip netns exec <pid> ip route,看默认网关有没有 |
| 容器无法访问外网 | docker network prune |
看 bridge 的 MASQUERADE 在不在 |
| 端口 -p 暴露失败 | 改 compose 重启 | 看 iptables -t nat -L -n -v PREROUTING 链 |
具体数字(在我这台 VM 上):
- 全部命令手敲 + 实验 + 截图整理 ≈ 1.5 小时
- 桥接网络从"装 docker"到"通的容器能 curl 外网"对比:Docker 30 秒,手搓 15 分钟
- 但手搓后我能讲清楚每个命令在做什么 —— 这个认知差距不是 30 秒 vs 15 分钟能补的
我自己写的几个 helper(收尾用,做的是把上面的步骤包成可复用命令):
#!/usr/bin/env bash
# myns.sh — 加一个 namespace,自动接 bridge br0
NS=$1
IP=$2
sudo ip netns add "$NS"
sudo ip link add "veth-$NS" type veth peer name "veth-host-$NS"
sudo ip link set "veth-host-$NS" master br0 up
sudo ip link set "veth-$NS" netns "$NS"
sudo ip netns exec "$NS" ip link set lo up
sudo ip netns exec "$NS" ip link set "veth-$NS" up
sudo ip netns exec "$NS" ip addr add "$IP/24" dev "veth-$NS"
sudo ip netns exec "$NS" ip route add default via 10.0.0.254 dev "veth-$NS"
echo "Namespace $NS created at $IP (gateway 10.0.0.254)"
这个 helper 改完可以在教学/远程协助的场景反复用。
五、目前还没完全搞清楚的几个点(局限与待验证)
- conntrack 内部(待验证):MASQUERADE 这条 NAT 规则背后是 conntrack 模块在做连接跟踪。我能查到 conntrack 表(
conntrack -L),但还不清楚它对 UDP/ICMP 的处理细节、--ctstate匹配的内部状态机是怎么走的,这部分我还在调研,后续准备再写一篇拆解 - bridge 上的 iptables(坑点):bridge 默认有
br_netfilter内核模块会拆 bridge 包让它走 iptables。如果不通过net.bridge.bridge-nf-call-iptables=0关掉,自己手搓的 bridge 会出现"莫名其妙 DROP"的包。Docker daemon 默认是关的,但 Kubernetes kubelet 启动时可能重开(坑点,容易复现) - IPv6(不足):整个实验我都只做了 IPv4。veth / bridge / NAT 在 IPv6 上是另一套机制(NAT66 / NDP proxy),我没跑过,博客园里有朋友专门写过,后续可以引用
- 跟 cgroup v2 + user namespace 的嵌套(待验证):真实容器不只 netns 隔离,还要 mount / pid / ipc / user 这几个 namespace + cgroup v2 限制资源。Ivan 之前讲 namespaces 那篇有部分覆盖,但从"网络能通"到"完整 OCI 容器能跑"的距离还差很远
- labctl 编辑流(还在调研):iximiuz Labs 提供的
labctl content dev命令允许本地编辑器写 / 服务器端实时同步。我装了一次但还没跑通远程授权(局限:可能是我这边 OAuth callback 卡住,不是工具问题) - 教学 vs 工具属性(不足):这个教程是"教学导向",真实生产环境没人手动
ip netns—— 但理解这套对 debug Kubernetes CNI(Flannel / Calico / Cilium)非常关键。CNI 的 Datapath 几乎都是这套机制的封装
六、适用场景建议
| 场景 | 建议 |
|---|---|
| 后端工程师面试 system design | 强烈推荐读一遍,容器网络问题在 system design 里出场率很高 |
| SRE / 平台工程团队新人 onboarding | 作为 4-6 周培训的第 1 周材料,比直接学 k8s 基础扎实得多 |
| debug Kubernetes CNI 故障 | 必备前置知识,出问题时要现场拆 veth / bridge |
| 单纯想用 Docker / K8s | 不必读,直接用命令即可,理解"魔法"不影响日常使用 |
| 想自己实现一个 mini CNI | 这篇是基本功,但还要学 IPAM / vxlan / bgp 等更深的协议 |
七、参考链接
- Ivan Velichko 原文: How Container Networking Works: Building a Bridge Network From Scratch — 5,000+ 字 + 234 个 code 块,server-side rendered,完整抓得到
- HN 帖: How Container Networking Works: Building a Bridge Network from Scratch — 当天 2p / 0c,文章本身质量远高于分数
- Linux bridge 进阶: Linux Bridge - Part 1 / Part 2 by Hechao Li — Ivan 在原文 reference 里点过
- ip(8) 手册:
man ip,所有ip netns/ip link子命令的权威参考 - iptables 教程: Anatomy of a Linux bridge — 拆解 bridge 数据流
- Kubernetes Datapath: Tracing the path of network traffic in Kubernetes — 跟本文配套的 K8s 视角
(完)