你想测一条路由规则、一份 overlay 配置,或者一条防火墙策略。你不想为此开三台虚拟机、 把它们连起来、再等云镜像开机。你也不需要。Linux 能给你一整套隔离的网络栈——网卡、路由、 ARP 表、防火墙规则——那就是一个网络命名空间(network namespace);而 veth 对 可以像一根网线那样,把两个这样的栈接在一起。这样重复几遍,你就有了一张住在单台机器里的 小型多主机网络:能互相 ping 的主机、中间的一台交换机,以及一台带 NAT、能出公网的 路由器。
这是我做 Linux 网络 实验时用的那套流程,而它也正是容器网络底下跑的同一套机制。
我们会建两个命名空间,用一对 veth 把它们连起来,加一台网桥,配好 NAT 让其中一个
能上网,然后用 ping、ip route 和 tcpdump 逐项验证。之后再讲它到底有什么用、又会在
哪里坑你。
为什么网络命名空间可行:一个内核,多个网络栈
一个网络命名空间,是内核对整个网络栈的一份拷贝:它有自己的网卡、自己的路由表、自己的
iptables/nftables 规则、自己的 socket 空间。命名空间里的进程只看得到自己那一个空间。
这正是容器所获得的隔离——Docker、Podman、Kubernetes 都是把容器的网络放进一个命名空间。
当你执行 ip netns exec ns1 ping 10.0.0.2 时,你就在 ns1 的世界里,而 ns1 的内核网络
是完全真实的,只是被默认命名空间(宿主机)隔开了。
它们很便宜。没有虚拟机、没有 hypervisor、没有第二个内核。创建一个命名空间实际上相当于
一次 mount --bind,它和其余一切共享同一个内核。
第 1 步——创建两个命名空间
这里所有操作都需要 CAP_NET_ADMIN,所以用 sudo 跑(或者在特权容器里跑)。ip netns
是前端命令;它还会挂载一个按命名空间划分的 /var/run/netns 句柄,让你可以用名字来引用
一个命名空间。
# 创建两台隔离的主机
sudo ip netns add ns1
sudo ip netns add ns2
# 确认
ip netns list
正是这个句柄让你可以写 ip netns exec ns1 ...,而不必去摆弄 PID。每个命名空间一开始只有
一个 lo 环回接口,而且它的环回是处于 down 状态的。记住这一点——它是那个经典 bug 的
根源。
sudo ip netns exec ns1 ip link show
第 2 步——接上 veth 对
一对 veth 就是两个被死死绑在一起的虚拟网卡:从一端进去的,会从另一端出来,像一根 两端各带一个插头的网线。你先创建这对网卡,然后把两端分别挪进两个命名空间。
# 一根有两个头的网线,两头一开始都在宿主机
sudo ip link add veth1 type veth peer name veth2
# 一头插进 ns1,另一头插进 ns2
sudo ip link set veth1 netns ns1
sudo ip link set veth2 netns ns2
现在两端各住在不同的网络栈里。宿主机已经看不到 veth1 或 veth2 了;它们只存在于各自的
命名空间内。这就是全部的诀窍——一根跨两个网络栈的线。
第 3 步——分配地址并 ping 通
配置地址、把网卡拉起。环回不管怎样都得 up,因为很多软件默认它一定存在。
# ns1
sudo ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth1
sudo ip netns exec ns1 ip link set veth1 up
sudo ip netns exec ns1 ip link set lo up
# ns2
sudo ip netns exec ns2 ip addr add 10.0.0.2/24 dev veth2
sudo ip netns exec ns2 ip link set veth2 up
sudo ip netns exec ns2 ip link set lo up
两端都在同一个 /24 上拉起后,内核会自动装上一条直连路由(给网卡加地址就会为该子网
创建路由)。跨这根线 ping:
$ sudo ip netns exec ns1 ping -c2 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.061 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.063 ms
ttl=64 说明没有任何路由参与——这就是同一根线上的两台主机。看一下路由表,弄清它为什么
能通:
$ sudo ip netns exec ns1 ip route
10.0.0.0/24 dev veth1 proto kernel scope link src 10.0.0.1
就一条直连路由,没有默认路由。ns1 够得着 ns2,是因为两者都落在 10.0.0.0/24 这个
“on-link” 范围里。如果你把它们的地址指到不同子网、中间又没有路由器,它立刻就通不了了——
而这正是要记住的教训。
第 4 步——在中间放一台网桥
一对 veth 是一条点对点的线,所以它只能连两台主机。要造一台能连好几台的交换机,就用 network bridge(网桥)——一个软件二层交换机。在宿主机里创建它,然后把每个命名空间那 一端的网卡挂进去做从属。
# 宿主机里的一台虚拟交换机
sudo ip link add br0 type bridge
sudo ip link set br0 up
# 再建两个命名空间"主机"
sudo ip netns add ns3
sudo ip link add veth3 type veth peer name br-veth3
sudo ip link set veth3 netns ns3
sudo ip link set br-veth3 master br0 # 靠宿主机那端插进交换机
sudo ip link set br-veth3 up
# ns3 接上交换机
sudo ip netns exec ns3 ip addr add 10.0.0.3/24 dev veth3
sudo ip netns exec ns3 ip link set veth3 up
sudo ip netns exec ns3 ip link set lo up
现在 ns3 和 ns1、ns2 处在同一个二层网段上——只要它们也在交换机上,三者就都能互相
ping,网桥会学习它们的 MAC。网桥的行为就像一台非网管交换机:对未知目的泛洪、学习源 MAC、
然后转发。要把 ns1/ns2 也挪上来,就给每个命名空间各建一根靠宿主机的 veth,同样
master 到 br0 上。
第 5 步——加 NAT,让命名空间能上公网
现在是好玩的部分:给一个命名空间配上网关,让它能到达外面的世界。宿主机就是我们的路由器。 需要两件事:在宿主机上开启 IP 转发,以及在宿主机的出网卡上对命名空间的源地址做 伪装(masquerade,即 NAT)。
先给这个”局域网”建一条专用链路/命名空间,并让宿主机在这条线上占一只脚。我为命名空间规划
10.99.0.0/24,让宿主机拥有 10.99.0.1:
# 一根从宿主机通进命名空间的 veth,这个命名空间将作为我们的"外部"主机
sudo ip netns add ns4
sudo ip link add veth4 type veth peer name host-veth4
sudo ip link set veth4 netns ns4
sudo ip addr add 10.99.0.1/24 dev host-veth4
sudo ip link set host-veth4 up
# ns4 这一侧
sudo ip netns exec ns4 ip addr add 10.99.0.2/24 dev veth4
sudo ip netns exec ns4 ip link set veth4 up
sudo ip netns exec ns4 ip link set lo up
# ns4 把宿主机当作它的网关
sudo ip netns exec ns4 ip route add default via 10.99.0.1
接着把宿主机变成一台 NAT 路由器。先找到宿主机真正的出网卡——我这里假设是 eth0。
# 1. 让内核允许转发
sudo sysctl -w net.ipv4.ip_forward=1
# 2. 改写从 eth0 出去的命名空间流量的源地址
sudo nft add table ip nat
sudo nft 'add chain ip nat postrouting { type nat hook postrouting priority srcnat; }'
sudo nft add rule ip nat postrouting ip saddr 10.99.0.0/24 oifname "eth0" masquerade
如果你用的是旧工具链,iptables 的等价写法就一行:
sudo iptables -t nat -A POSTROUTING -s 10.99.0.0/24 -o eth0 -j MASQUERADE
现在 ns4 可以按 IP ping 通公网了:
$ sudo ip netns exec ns4 ping -c2 1.1.1.1
64 bytes from 1.1.1.1: icmp_seq=1 ttl=57 time=12.4 ms
注意 ttl=57:这些包被路由过了(在宿主机那里从 64 掉一跳,然后真实互联网又吃掉几跳)。
DNS 还没有配——那是另一件事,所以 ping 1.1.1.1 能通,而 ping example.com 要等你往这个
命名空间的 /etc/resolv.conf 里写了东西才行。
用 ip route、ping 和 tcpdump 验证
三件工具足以证明一切。ip route 告诉你这个网络栈以为自己能怎么走;ping 证明数据通路
真的存在;tcpdump 让你直接看到那些包本身。
# ns4 眼里的世界是什么样的?
sudo ip netns exec ns4 ip route
# default via 10.99.0.1 dev veth4
# 10.99.0.0/24 dev veth4 proto kernel scope link src 10.99.0.2
# 观察这个"发卡弯":包到达 veth4 时还没被 NAT,
# 出发经过 eth0 时已经变成 NAT 过的样子
sudo ip netns exec ns4 tcpdump -ni veth4 icmp
“啊哈”时刻就发生在 tcpdump 里。在 ns4 内部,看一个发往 1.1.1.1 的包的源地址:它是
10.99.0.2,私有地址。在宿主机上,tcpdump -ni eth0 icmp 会显示同一个包,源地址是
203.0.113.7——宿主机的地址。这一对抓包就是 NAT 的全部:一次按连接的源地址改写,再加上
返程方向的反向翻译。它和我
在《NAT 穿透到底是怎么工作的》里拆开的机制完全一样,
只不过在这里你能亲眼看着它跑。
它到底有什么用
这可不是什么花架子;它是不建实验室就能测网络代码的方式。
- overlay 和 VPN 配置。 你可以在单台主机上搭出一张由”远端”命名空间组成的假 WAN, 在碰真实硬件之前,先测 overlay 的路由安装、MTU 处理和 NAT 穿透行为。这是在可复现的 沙盒里快速验证某个东西——比如一张私有 mesh VPN—— 的最快办法。
- 复现路由 bug。 “我在局域网里好好的,一走隧道就不行”,通常都是 MTU、路由或者反向 路径过滤的问题。命名空间让你能重现出一模一样的拓扑,几秒钟内二分定位。
- CI。 内核命名空间创建起来廉价又快速,这正是网络测试套件(以及容器运行时、overlay 的集成测试)用它的原因。并行 CI 任务之间的隔离,白送。
- 学习。 想真正看见”默认网关”只是条路由、“网桥”只是台学 MAC 的交换机、“NAT”只是条 改写规则,没有比它更好的办法了。
会浪费你一下午的坑
- 环回默认是 down 的。 如果命名空间里
ping 127.0.0.1不通,那就是你忘了ip link set lo up。没有它,很多守护进程会悄无声息地挂掉。 - 命名空间重启就没了。 它们不持久。要么在脚本里重建,要么接受
/run下的ip netns句柄重启后消失。但凡需要可复现,就把整套设置包进一个能重跑的 shell 脚本。 ip netns exec还是ip -n。 两者都行;ip -n ns1 addr更短,但没法跑任意命令。 当你要跑非ip的程序时,用ip netns exec。- 转发默认是关的。 一个当路由器用的命名空间需要
net.ipv4.ip_forward=1。没有它,NAT 悄无声息地什么也不做。 rp_filter与非对称路径。 开着严格反向路径过滤时,来自命名空间的包可能因为返程 路由”讲不通”而被丢掉。当你刻意搭非对称拓扑时,就把它放松 (net.ipv4.conf.*.rp_filter)。- 没有 DNS,也没有 ARP 帮忙。 在你写进去之前,命名空间没有解析器;而网桥不会在子网 之间做路由——它只在一个子网内做交换。
- 防火墙默认策略。 如果命名空间连不到宿主机或公网,把命名空间和宿主机两边的
nft list ruleset都看一遍;任何一边有个DROP策略就足以掐死它。 - 清理。
sudo ip netns delete ns1一次就把命名空间和它的网卡全删掉。删吧——忘了删会 在宿主机里堆下一堆 veth 的残留端。
一段话总结
一个网络命名空间就是 Linux 网络栈的一份私有拷贝,一对 veth 就是两个命名空间之间的
一根网线。连两个命名空间,你就有了两台主机;在几台之间放一台网桥,你就有了交换机;
再加上 ip_forward 和一条 MASQUERADE 规则,宿主机就成了带 NAT 的路由器。用
ip route、ping 和 tcpdump 来验证——这三样会告诉你:网络栈是否相信自己能路由、包是否
真的在流动、以及它们在线上长什么样。这里没有任何一步需要 hypervisor、第二台机器或云账号:
它离你只有一条 ip netns 命令,而它正是容器和 overlay 所依赖的同一块底座。