- create a new netns, call it 'physical' say
- move your physical device's interface to 'physical'
- create wg interface in 'physical'
- move wg interface to init ns
It works because it 'remembers' where it was created and remains bound to it for sending encrypted packets over the physical device, but is now the only interface available to processes by default (i.e. without a superuser making them use 'physical' ns directly).
Not long enough; want to read more: https://www.wireguard.com/netns
I hope some future part covers the nsfs and bind mount thing for persisting namespaces past a specific process. It took me way, way to long to catch this passage in the man page:
Bind mounting (see mount(2)) one of the files in this directory to somewhere else in the filesystem keeps the corresponding namespace of the process specified by pid alive even if all processes currently in the namespace terminate.
The first part seemed like a simple enough idea until I remembered that traditional IP routing is based on destination address alone. Although I could establish multiple VPN sessions, give each one its own network interface, and configure each client to bind to a different interface (and therefore send packets with a different source address), the routing table had no way to know that it should send CLIENT_A's packets to the server via IFACE_A and CLIENT_B's packets to the same server via IFACE_B.
Policy-based routing looked like a promising solution, but it would require hooking the VPN startup sequence to find each new tunnel interface as it was created, using elevated privileges to install a special routing policy for each interface, and reliably cleaning up the routing tables when each client was done. This seemed overly complicated, and a poor fit for unprivileged users.
Linux namespaces, along with tools that are already in the debian/ubuntu repos, let me do everything I wanted. I ended up writing a couple shell scripts that create N namespaces, give each one a veth connected to a shared bridge device with internet access, establish a namespaced VPN session via each veth, and let the VPN take control of its namespace's default route.
That by itself would have been enough, since any network client running within a namespace is at the mercy of that namespace's routing table, and therefore goes through its own VPN session. I went a couple steps further, though: I added firewall rules within each namespace, to prevent any leaks if the VPN failed or a routing mistake crept in. I also decided to launch a lightweight proxy server in each namespace instead of running my network clients directly within, allowing me to conveniently start and stop my network clients outside of the namespaces and independently of the namespace setup/teardown process.
With the addition of user namespaces, the steps that would normally require root can be run as a fake uid 0 by an unprivileged user. With the help of lxc-user-nic, the one privileged operation that must be run outside the namespaces (adding each veth to the bridge) can be done by an unprivileged user as well. With the help of dumb-init (or tini), stopping the parent process of any namespace automatically cleans up everything within, including the routing table, firewall rules, veth, VPN, and proxy.
This arrangement solved all the problems I set out to solve, and the exercise was a good way to get familiar with namespaces. I might some day convert my hackish shell script implementation to python, perhaps with a little GUI, and see if I can combine it with Firefox containers. Per-container or per-tab VPN sessions in a web browser would be neat.