动手:用 netns 和 veth 手搓一个容器网络
不用 K8s、不用 Docker,纯 ip 命令把两个"容器"连通,容器网络就没有秘密了。
学完这节你能做到
- 手工创建 netns、veth pair 和网桥,让两个命名空间互通
- 解释容器出网为什么需要 SNAT
- 在宿主机上找到某个 Pod 对应的 veth 和 netns
建议先学
跳过这几节会看不懂本节的部分推导
目标:不用容器,手搓一个容器网络
容器网络之所以让人觉得像魔法,是因为平时看到的都是成品。这一节把它拆开——
只用 ip 命令,让两个"容器"互通、并且能出网。做完这个实验,前面用过的
CNI、Service、次级网卡就都不再是黑盒了。
需要的环境:一台能 root 的 Linux(虚拟机也行),不需要装任何东西。
实验会创建命名空间、虚拟网卡和 NAT 规则。建议在虚拟机或测试机上做,别在生产机上练。 最后有完整的清理命令,全部对象都是临时的,重启后自动消失。
第一步:网络命名空间隔离了什么
# 创建两个命名空间,当作两个"容器"
ip netns add ns1
ip netns add ns2
ip netns list
进去看看里面有什么:
ip netns exec ns1 ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
只有一个没启动的 lo。 命名空间隔离的是整套网络栈:
| 隔离的东西 | 含义 |
|---|---|
| 网卡列表 | 看不到宿主机的任何网卡 |
| 路由表 | 空的,连默认路由都没有 |
| ARP / 邻居表 | 独立 |
| iptables / nftables 规则 | 独立 |
| socket 与端口空间 | 两个 ns 里可以各自监听 80,不冲突 |
/proc/sys/net 参数 | 部分独立 |
最后两条是关键:端口不冲突正是 K8s 网络模型第一条铁律(每个 Pod 独立 IP、 不共享宿主机端口空间)的实现基础。
第二步:veth pair —— 一根虚拟网线
veth 总是成对出现,一端进、另一端立刻出,像一根两头都是网卡的网线:
# 建一对,然后把两头分别塞进两个命名空间
ip link add veth1 type veth peer name veth2
ip link set veth1 netns ns1
ip link set veth2 netns ns2
配地址、启动:
ip netns exec ns1 ip addr add 10.99.0.1/24 dev veth1
ip netns exec ns1 ip link set veth1 up
ip netns exec ns1 ip link set lo up
ip netns exec ns2 ip addr add 10.99.0.2/24 dev veth2
ip netns exec ns2 ip link set veth2 up
ip netns exec ns2 ip link set lo up
验证:
ip netns exec ns1 ping -c 3 10.99.0.2
通了。这就是"同节点 Pod 到 Pod"的最小实现——包全程在内存里,没经过任何物理网卡。
一个包的旅程 里的「同节点 Pod → Pod」场景,
现在你手上就有一个真实的了。用 ip netns exec ns1 tcpdump -i veth1 抓一下,
能看到 ICMP 请求和响应,源目地址就是这两个 IP。
第三步:加网桥,接入更多"容器"
两两拉线不可扩展:3 个容器要 3 根,10 个要 45 根。真实方案是每个容器一根 veth 接到网桥上—— 网桥就是宿主机里的一台虚拟交换机。
先清掉上一步:
ip netns del ns1
ip netns del ns2
建网桥和两个命名空间:
# 网桥
ip link add br0 type bridge
ip addr add 10.99.0.254/24 dev br0 # 网桥自己也有地址,充当网关
ip link set br0 up
# 两个命名空间
for i in 1 2; do
ip netns add ns$i
ip link add veth$i type veth peer name br-veth$i
ip link set veth$i netns ns$i
ip link set br-veth$i master br0 # 宿主机侧那头插进网桥
ip link set br-veth$i up
ip netns exec ns$i ip link set lo up
ip netns exec ns$i ip addr add 10.99.0.$i/24 dev veth$i
ip netns exec ns$i ip link set veth$i up
ip netns exec ns$i ip route add default via 10.99.0.254 # 默认路由指向网桥
done
看一眼拓扑:
bridge link show # 网桥上挂了哪些口
ip netns exec ns1 ping -c 2 10.99.0.2 # 容器互通
ip netns exec ns1 ping -c 2 10.99.0.254 # 能到网关(宿主机)
br-veth1 ... master br0 state forwarding
br-veth2 ... master br0 state forwarding
forwarding 说明网桥在正常转发。这就是传统 CNI(bridge 模式)的数据平面原型。
第四步:让容器能出网
现在 ping 外网还不通:
ip netns exec ns1 ping -c 2 223.5.5.5 # 不通
两个原因,缺两样东西:
# 1. 宿主机要愿意转发别人的包
sysctl -w net.ipv4.ip_forward=1
# 2. 源地址 10.99.0.1 是私有地址,外网回不来 → 需要 SNAT
# 出宿主机时把源地址改写成宿主机的地址
ip -o -4 route show default | awk '{print $5}' # 找到出口网卡,假设是 eth0
iptables -t nat -A POSTROUTING -s 10.99.0.0/24 ! -o br0 -j MASQUERADE
再试:
ip netns exec ns1 ping -c 2 223.5.5.5 # 通了
容器出集群外做 SNAT 是合理的(私有地址出不去)。但如果容器到容器也做 SNAT, 就违反了 K8s 网络模型的第二、第四条铁律:对端看到的源地址会变成节点 IP, K8s 网络模型 那节讲过为什么这不能接受。
注意上面规则里的 ! -o br0:从网桥出去的(容器之间)不做 SNAT,
只有真正离开宿主机的才做。这一个条件就是"东西向免 NAT、南北向 NAT"的分界,
真实 CNI 也是这么处理的。
第五步:回到 K8s —— 找到某个 Pod 的 netns
真实集群里,Pod 的网络命名空间不是用 ip netns add 建的,所以 ip netns list 看不到它。
找它的标准套路:
# 1. 拿到 Pod 所在节点与容器 ID
kubectl get pod mypod -o wide
kubectl get pod mypod -o jsonpath='{.status.containerStatuses[0].containerID}'
# 2. 在那个节点上拿到容器主进程 PID(containerd)
crictl inspect <container-id> | grep -m1 '"pid"'
# 3. 用 nsenter 进它的网络命名空间
nsenter -t <pid> -n ip addr
nsenter -t <pid> -n ip route
nsenter -t <pid> -n ss -lntp
这套动作在排障时极其有用:容器镜像里往往没有 ip、ss、tcpdump,
但宿主机上有。 nsenter -n 让你用宿主机的工具去看容器的网络。
# 甚至可以在宿主机上抓 Pod 内部的包
nsenter -t <pid> -n tcpdump -i eth0 -nn -c 20
反过来,从宿主机找 Pod 对应的那根 veth:
# Pod 内看 eth0 的 peer 索引
nsenter -t <pid> -n ip -d link show eth0 | grep -o 'peer_ifindex.*'
# 宿主机上按索引找网卡
ip link | grep '^<索引>:'
nsenter -t <pid> -n ip addr # Pod 的地址与网卡
nsenter -t <pid> -n ip route # Pod 的路由表
nsenter -t <pid> -n ss -lntp # Pod 里谁在监听「Pod 里没装工具」这个借口从此不成立。后面《Pod 之间不通》那一关会反复用到。
清理
ip netns del ns1
ip netns del ns2
ip link del br0
iptables -t nat -D POSTROUTING -s 10.99.0.0/24 ! -o br0 -j MASQUERADE
sysctl -w net.ipv4.ip_forward=0 # 如果原本是 0
删命名空间会自动带走里面的 veth(veth 的另一头也会消失)。
检查点
两个网络命名空间里可以各自监听 80 端口而不冲突。这对应 K8s 网络模型的哪一条?
实验里的 SNAT 规则写了 ! -o br0。去掉这个条件会怎样?
Pod 镜像里没有 ip 和 tcpdump,要看它的路由表和抓它的包,怎么做?
这节课的落点
- 网络命名空间隔离整套网络栈,其中端口空间独立是 K8s「独立 IP」的实现基础
- veth pair 是一根虚拟网线,一端进另一端立刻出,纯内存操作
- 两两拉线不可扩展,真实方案是每容器一根 veth 接到网桥(宿主机里的虚拟交换机)
- 出网要两样:
ip_forward=1与 SNAT(MASQUERADE) ! -o br0这个条件就是东西向免 NAT、南北向 NAT 的分界- Pod 的 netns 用
ip netns list看不到,要走crictl inspect拿 PID +nsenter -n nsenter -t <pid> -n让你用宿主机的工具看容器的网络——"容器里没装工具"不再是借口
延伸资料
- The Kubernetes Networking Guide ↗
- k8s-in-action
network/README.md