在不改动宿主机路由的情况下测试命名空间内的 VPN 路由
三个模拟角色、三个隔离上下文,每条链路一个 veth。在触碰宿主机之前,先比较命名空间实验与宿主机路由实验。
两个实验环境可以模拟同一套 VPN 拓扑,却以完全不同的方式失败:一个活在网络命名空间里,另一个活在宿主机自己的路由上。命名空间版本把每个接口、路由和套接字都留在可删除的上下文里,而宿主机版本启动更快,却把状态留在身后。正是这些残留状态,让第二个实验环境在一个月后难以复现。先比较清理方式,因为它决定你能否把实验做两遍。
搭建同一套实验的两种方式,以及各自何时胜出
在宿主机路由实验里,你往正在使用的机器上添加一个隧道接口和若干路由,于是实验与你的 shell、包管理器和其它所有连接共担命运。在命名空间实验里,每个角色都有自己的接口、地址、路由和套接字栈,删除命名空间就把这一切抹掉。两种模型无所谓谁更正确,因为它们回答的是不同的问题。当主题是路由与转发时选命名空间,当主题是一个桌面客户端如何集成时选宿主机实验。
| Aspect | Namespace lab | Host-route lab |
|---|---|---|
| Isolation | Full stack per role | Shares the host stack |
| Cleanup | Delete named namespaces | Manual route and rule removal |
| Realism | Strong for routing | Needed for GUI clients |
把两者混在一起是常见的错误:拿只存在于宿主机上的客户端去测命名空间实验对两种布置都证明不了什么。在创建第一条链路之前,先决定你要问的是哪个问题。
每个角色一个上下文,刻意连接起来
每个角色建一个命名空间,把每对 veth 的两端分别移进拥有它的上下文。前四条命令足以看清形状:
ip netns add client
ip netns add gateway
ip link add veth-c type veth peer name veth-g
ip link set veth-c netns client给实验分配与宿主机上任何东西都不重叠的网段,并在开始前写下哪个命名空间拥有哪个地址。能跑起来的实验需要两条独立路径:一条外层路径让 VPN 端点抵达它的对端,一条返回路径供被保护的资源使用。把它们建模成两个决定,因为只搭出站一半的实验能通过握手测试,却会在应用测试中失败。
转发角色才是实验变得有意思的地方
必须有一个角色承担转发,也就是在该命名空间内启用转发,并加上设计所需要的那条规则。转发状态是按命名空间隔离的,因此宿主机上的设置对实验无关,实验里的设置也不会外泄,正如 网络命名空间文档 所述。WireGuard 的文档指出,一个接口和它的外层套接字可以分处不同的命名空间,这是个有用的技巧,也是把自己绕晕的绝佳方式。不要以为移动接口就会以同样的方式移动连接的所有部分。
发过一个测试包后检查转发计数器,因为一条从未匹配的规则和一条把包丢掉的规则,在路由表里看起来一模一样。只有计数器能把它们分开。
记录一条基线,然后只破坏一条路由
先记录当下存在什么,并把上下文写在每一行旁边。读命名空间,再读测试目标在其中一个里会走的路由,然后读承载它的链路上的计数器:
ip netns list
ip -n client route get 10.0.0.5
ip -n gateway -s link show veth-g第三条命令是大家常跳过的那条,而证据恰恰在那里:丢弃和错误计数器会告诉你数据包到底有没有到达转发角色。改动之后再记录同样三种视图,让 diff 而不是你的记忆成为证据。然后只破坏一样东西,要么删掉一条路由,要么在转发路径上加一条拒绝规则,并确认你预测的症状:握手完成却没有回包,或者本该超时却出现解析失败。只演示成功的实验教不了多少东西。
清理才让下一次运行可复现
把清理限定在你命名过的资源里,因为一把梭地重置防火墙或删除宿主机路由不是可复现,而是第二次事故。把拓扑、确切命令和你预期的观察存下来,让下一次运行复现同一个故障,而不是去猜上一个状态留在哪个上下文里。没有这份记录,实验就只是故事,而不是流程。
实验用本地生成的凭据,别把生产账号带进共享示例。然后逐个删除命名空间,并确认宿主机的视图没有变化——这正是选择这种模型的全部理由。一个你无法干净移除的实验,就是多绕几步的宿主机改动。