Test VPN routing in namespaces without touching host routes
Three simulated roles, three isolated contexts, and one veth per link. Compare a namespace lab with a host-route lab before touching the host.
Two labs can model the same VPN topology and fail in completely different ways: one lives in network namespaces, the other on the host's own routes. The namespace version keeps every interface, route and socket in a context you can delete, while the host version is faster to start and leaves state behind. That leftover state is why the second lab is hard to repeat a month later. Compare cleanup first, because it decides whether you can run the experiment twice.
Two ways to build the same lab, and when each wins
In a host-route lab you add a tunnel interface and routes to the machine you are working on, so the experiment shares fate with your shell, your package manager and every other connection. In a namespace lab each role gets its own stack of interfaces, addresses, routes and sockets, and deleting the namespace removes all of it. Neither model is more correct, because they answer different questions. Choose namespaces when the subject is routing and forwarding, and a host lab when the subject is how one desktop client integrates.
| 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 |
Mixing the two is the usual mistake: a namespace lab tested against a host-only client proves nothing about either arrangement. Decide which question you are asking before you create the first link.
One context per role, connected on purpose
Create one namespace per role and move each end of every veth pair into the context that owns it. The first four commands are enough to see the shape:
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 clientGive the lab ranges that overlap nothing on the host, and write down which namespace owns which address before starting. A working lab needs two separate paths: an outer one for the VPN endpoint to reach its peer, and a return path for the resource being protected. Model them as two decisions, because a lab that builds only the outbound half passes a handshake test and fails an application test.
The forwarding role is where labs get interesting
One role has to forward, which means enabling forwarding inside that namespace and adding whatever rule the design needs. Forwarding state is per namespace, so the host setting is irrelevant to the lab and the lab setting does not leak outward, as the network namespaces documentation describes. WireGuard documents that an interface and its outer socket can live in different namespaces, which is a useful trick and an excellent way to confuse yourself. Do not assume moving an interface moves every part of the connection identically.
Check the forwarding counter after a test packet, because a rule that never matched and a rule that dropped look identical in the route table. Only the counters separate them.
Record a baseline, then break one route
Start by recording what exists, with the context written next to each line. Read the namespaces, then the route a test destination would take inside one of them, then the counters on the link that carries it:
ip netns list
ip -n client route get 10.0.0.5
ip -n gateway -s link show veth-gThe third command is the one people skip, and it is where the evidence lives: the dropped and errors counters tell you whether a packet reached the forwarding role at all. Capture the same three views after a change, and let the diff be the evidence rather than your memory of it. Then break exactly one thing, either by deleting a route or by adding a deny rule on the forwarding path, and confirm the symptom you predicted: a completed handshake with no reply, or a resolution failure instead of a timeout. A lab that only demonstrates success teaches very little.
Cleanup is what makes the next run repeatable
Keep cleanup scoped to the resources you named, because a blanket firewall reset or a host route removal is not reproducibility, it is a second incident. Save the topology, the exact commands and the observations you expected, so the next run reproduces the same failure instead of guessing which context held the previous state. Without that record the lab is a story rather than a procedure.
Use credentials generated locally for the lab, and keep production accounts out of shared examples. Then delete each namespace and confirm the host view is unchanged, which is the whole reason for choosing this model. A lab you cannot remove cleanly is a host change with extra steps.