网络命名空间 netns 隔离实战:不用 Docker 手搓 veth 通道、模拟独立路由器与出口 IP 隔离

为什么要在意网络命名空间

很多人第一次听说 network namespace(网络命名空间,后面简称 netns)是在 Docker 的文档里,觉得这是容器引擎的内部实现,跟自己维护一两台 VPS 的个人站长没关系。其实恰恰相反:netns 是 Linux 内核从 2.6.24 就带进来的原生能力,它把「一套独立的网络协议栈」变成了一个可以随时创建、随时销毁的对象。你不需要装 Docker、不需要跑 K8s,只要内核支持,就能用它做出一台「虚拟路由器」「独立测试网段」或者「隔离出口 IP 的代理容器」。

我自己的使用场景很典型:站点要迁移,新老服务器之间需要长时间互传数据,但我不想让测试流量和线上流量互相污染;同时还要用 iperf3 测新线路的真实带宽。如果直接在本机加 iptables 规则,规则一多就会跟线上防火墙打架,回滚还容易漏。用 netns 就干净得多——新建一个命名空间,里面的网卡、路由表、iptables 规则全是独立的,测完删除命名空间,所有东西瞬间消失,一行都不残留在主机上。

核心概念:netns 隔离了什么

netns 隔离的对象是「网络协议栈实例」。具体来说,下面这些东西在每个 netns 里都是各自独立的一份:

  • 网卡:每张网卡(包括虚拟网卡 veth、bridge、tun/tap)在任一时刻只属于一个 netns。用 ip link set dev xxx netns NAME 可以在命名空间之间搬移。
  • IP 地址与路由表:同一个 IP 可以同时存在于两个 netns 而互不冲突。
  • iptables / nftables 规则:每个 netns 有自己独立的规则集,这正是隔离的意义所在。
  • 连接跟踪表、socket 列表:ss -tulnp 在 netns 里看到的只是该命名空间的监听端口。
  • 本地回环 lo:注意,新建的 netns 里 lo 默认是 down 状态,需要手动 ip link set lo up,这是新手最常踩的坑。

但它不隔离的东西也要清楚:文件系统、进程 PID、用户、主机名都还是共享的(那些是 mount / pid / user / uts ns 的职责)。所以你会在 ip netns exec 出来的 shell 里看到和主机一样的 /etc/hosts,这是正常的。

最小可用示例:手搓一条隔离通道

下面这套命令在两台机器之间(或者同一台机器的两个 netns 之间)建立一条点对点链路。假设主机上网卡是 eth0,我们用命名空间 lab 模拟一台「独立的小机器」。

# 1) 创建命名空间
ip netns add lab

# 2) 创建一对 veth 虚拟网卡,一端留在主机(veth-host),一端塞进 lab(veth-lab)
ip link add veth-host type veth peer name veth-lab
ip link set veth-lab netns lab

# 3) 两端都启用,并配置地址(/30 的网段只有两个可用地址,正好点对点)
ip addr add 10.200.0.1/30 dev veth-host
ip link set veth-host up

ip netns exec lab ip addr add 10.200.0.2/30 dev veth-lab
ip netns exec lab ip link set veth-lab up
ip netns exec lab ip link set lo up      # 关键!别忘了 lo

# 4) 验证
ping -c 2 10.200.0.2
ip netns exec lab ping -c 2 10.200.0.1
ip netns exec lab ss -tulnp

到这一步,你就得到了一个完全独立的网络栈。ip netns exec lab 后面跟的命令,等价于「在这台虚拟机器里执行」。想进去交互式操作,可以用 ip netns exec lab bash,注意 shell 提示符没有任何变化,很容易忘记自己在哪个命名空间里——建议进之前 echo $PS1 记一下,或者给每个 netns 起 ip netns exec lab bash --rcfile /root/.labrc 这样带自定义提示符的 shell。

让 lab 能访问外网:把主机当路由器

默认情况下 lab 只能 ping 通 10.200.0.1,出不去。要让它借主机的出口上网,需要三步:

# 1) 在 lab 里加默认路由,指向主机那一端
ip netns exec lab ip route add default via 10.200.0.1

# 2) 主机开启 IPv4 转发(当路由器)
sysctl -w net.ipv4.ip_forward=1

# 3) 做 SNAT,把 lab 出来的流量伪装成主机的地址
iptables -t nat -A POSTROUTING -s 10.200.0.0/30 -o eth0 -j MASQUERADE

# 顺带放开 FORWARD 链(很多云主机的 FORWARD 默认是 DROP)
iptables -A FORWARD -i veth-host -o eth0 -j ACCEPT
iptables -A FORWARD -i eth0 -o veth-host -m state --state RELATED,ESTABLISHED -j ACCEPT

# 验证
ip netns exec lab ping -c 2 1.1.1.1
ip netns exec lab curl -s https://ifconfig.me

最后那条 curl 特别有用:如果返回的是主机的外网 IP,说明 SNAT 生效了。整个过程中,lab 里跑的进程用的是自己的路由表和连接跟踪表,和主机上其他服务完全隔离,不会在 ss 里串味。

实战场景一:用 netns 做带宽与线路测试

测带宽最怕的是把线上业务带的卡顿。把测试流量塞进独立 netns,就能设定明确的出口路径,还不会污染主机的路由表和 iptables 计数。

# lab 侧起 iperf3 服务端
ip netns exec lab iperf3 -s -p 5201 &

# 从另一台机器打流,测的就是 veth 这条路(本机环回,用来验证隔离是否成功)
iperf3 -c 10.200.0.2 -p 5201 -t 10

更常见的用法是反过来:主机上有多个出口 IP / 多张网卡(比如一块走 CN2、一块走普通线路),想固定某条线路跑批量下载或 rsync。可以建一个 netns,把特定网卡搬进去,然后在里面跑任务:

ip netns add line2
ip link set eth1 netns line2
ip netns exec line2 ip addr add 203.0.113.7/24 dev eth1
ip netns exec line2 ip link set eth1 up
ip netns exec line2 ip route add default via 203.0.113.1
ip netns exec line2 rsync -avP /data/ user@target:/backup/

好处是这台机器上其它进程完全不知道 eth1 去哪了,测试任务结束后 ip link set eth1 netns 1(netns 1 是主机默认命名空间)就能把网卡还回去,路由表和地址都还保留着,非常方便。

实战场景二:临时给某个进程换出口 IP,不动主机配置

有时候只是想临时用某个 IP 发一次请求做验证(比如确认某地域的 CDN 节点是否生效),又不想改主机默认路由。可以借用 netns 配合 setns 的思路——最省事的做法是在 netns 里跑一个 socks5 代理,本机按需走代理:

ip netns exec line2 ss -tlnp        # 确认代理端口在 netns 里监听
# 用 socat 之类做转发,或者直接 ip netns exec line2 curl https://example.com

因为 ip netns exec 是这个进程的「一次性外衣」,进程退出后命名空间里的状态该留的留、该清的清,对主机没有任何副作用。

排查三件套:怎么知道当前在哪个 netns

运维里最常见的问题不是「怎么建」,而是「这个进程到底在哪个命名空间里」。三个命令解决:

# 1) 列出所有命名空间
ip netns list

# 2) 查看某个进程所属的 netns(inode 号是唯一标识)
ls -l /proc/<PID>/ns/net
# 输出形如 net:[4026532520],方括号里的数字就是 netns 的 inode

# 3) 反查这个 inode 对应哪个命名空间的 veth
ip -n lab link show
ip link show veth-lab                # 在主机查会报 "Device does not exist",说明它在别的 ns 里

如果 ip netns list 输出为空但 /proc/*/ns/net 里能看到多个 inode,说明有命名空间是被「进程持有」但没在 /var/run/netns/ 里挂载(Docker 就是这样),这种看不到名字,只能靠 inode 对比。

五个必踩的坑与防范

  1. lo 没 up。新建 netns 后 lo 是 down 的,任何依赖 127.0.0.1 的程序都会失败,表现为「端口明明监听了却连不上」。记住每个 netns 都要 ip link set lo up。
  2. 删掉 netns 后网卡不回来。如果你把一张物理网卡搬进 netns,然后 ip netns delete,这张网卡会连同命名空间一起消失,本质上是回到主机默认 ns 但处于 down 且无地址的状态。跑 ip link 能找到它,重新 ip link set eth1 up 并配地址即可,但如果它承载着你的管理 IP,就会失联——所以永远不要把管理网卡搬进临时 netns。
  3. FORWARD 链默认 DROP。云主机(尤其是带安全组的)默认 iptables -P FORWARD DROP,只加 MASQUERADE 是不够的,必须显式放行 FORWARD,否则 ping 通网关但出不去。
  4. 命名空间里的服务监听 0.0.0.0 也访问不到。这不是 bug,是隔离生效了。要在主机访问 netns 里的服务,必须靠 veth 的对端地址,或者用 ip netns exec 进去访问。
  5. 命名空间会随进程一起消失。只有挂载在 /var/run/netns/NAME 上的命名空间才会在无进程引用时保留。ip netns add 会自动做这个挂载,所以手动建的一般不会丢;但如果是通过 unshare -n 临时的,进程一退就没了,别指望它持久。

清理:一定要有删除脚本

测试完不清理会累积垃圾 veth 和 iptables 规则。建议写成一个可反复执行的清理脚本,先删规则再删命名空间(顺序反了规则会残留):

#!/bin/bash
NS=lab
# 先撤掉 iptables 规则(不存在就忽略)
iptables -t nat -D POSTROUTING -s 10.200.0.0/30 -o eth0 -j MASQUERADE 2>/dev/null
iptables -D FORWARD -i veth-host -o eth0 -j ACCEPT 2>/dev/null
iptables -D FORWARD -i eth0 -o veth-host -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null
# 再删命名空间(对端 veth 会自动消失)
ip netns delete $NS 2>/dev/null
echo "cleaned. remaining:"
ip netns list
ip link show type veth 2>/dev/null | grep -c veth

脚本里用 -D 而不是 -F,是为了只删自己加的规则,不影响线上其它规则。2>/dev/null 让重复执行不报错,这对无人值守的 cron 清理任务很重要。

常见问题

Q:netns 和 Docker 的 network 是什么关系?
A:Docker 的每个容器默认就活在一个 netns 里,docker network 这一层是在 netns 之间加 bridge 和 veth 的编排。理解了裸 netns,再看 Docker 网络就是「富余的一层封装」;反过来,用 docker inspect 拿到容器 PID 后 nsenter -t <PID> -n ss -tulnp 直接钻进容器的网络栈,排查容器网络问题时比 exec 更直接。

Q:性能损耗大吗?
A:netns 本身只是内核里一个数据结构,几乎没有额外开销。真正有成本的是 veth 对——每包要过一次软件网络栈。在同机测环回场景下,veth 大致能跑到 20~40 Gbps,对个人站点的带宽来说完全不是瓶颈。

Q:重启后还在吗?
A:不在。netns 是运行时对象,重启即消失。要自动化持久化,得写 systemd unit 用 ExecStart 里跑创建脚本,或用 ip netns add 配合 systemd 的 NetworkNamespacePath=。

netns 的价值不在于「高级」,而在于它把「隔离」这件事变得可逆——出问题的第一反应永远是删掉命名空间重来,而不是在大堆 iptables 规则里找一个写错的 -s。对个人站长来说,这是一种成本极低但收益明显的运维工具,值得花半小时摸熟。

Last modification:September 28th, 2026 at 09:24 pm

Leave a Comment