Opening a shell connected to wireguard, but not the rest of your system (Linux)

Published on October 6, 2024

Problem: you’re trying to access a blocked website and want to curl something so the request goes over a VPN. Sometimes you can solve this with --proxy variants but let’s say that’s not an option. Maybe you’re using software other than curl. What you’re looking for is a shell that any command you run, even software not aware of proxies or VPNs, gets routed over the VPN.

This is surprisingly easy to do, but requires some prior know-how.

Let’s assume you have a wg-quick-compatible wireguard config file like this,

[Interface]
PrivateKey = 
Address = 1.2.3.4/32,aaaa:bbbb:cccc:dddd:eeee:ffff:0000/128

[Peer]
PublicKey = 
AllowedIPs = 0.0.0.0/0,::0/0
Endpoint = 5.6.7.8:51820

Simply run,

modprobe wireguard
ip netns add vpn
ip link add dev wg0 type wireguard
ip link set wg0 netns vpn
ip netns exec vpn bash

# Inside shell launched by last command
wg setconf wg0 <(wg-quick strip wireguard.conf)
ip address add dev wg0 '1.2.3.4/32'
ip address add dev wg0 'aaaa:bbbb:cccc:dddd:eeee:ffff:0000/128'
ip link set up dev wg0
ip route add default dev wg0
ip link set up dev lo
curl https://api.ipify.org/ # Test internet, returns external IP address

These commands will likely need to be run as root, but then you can downgrade with sudo.

The Explanation

We start with modprobe wireguard to ensure the wireguard kernel module is loaded. If something isn’t working you may also want to add echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control which will enable more debugging info to dmesg.

Next we create a named network namespace1 (abbreviation netns) with ip netns add vpn. Every process on Linux (since 2.6.24) is attached to a netns that determines how traffic is routed to and from processes. There is always a default netns which often is the only one.

From the documentation,

A network namespace is logically another copy of the network stack, with its own routes, firewall rules, and network devices.

You may notice the default netns doesn’t show up when running ip netns list. This is because it isn’t a named network namespace, which is a higher level concept managed by the ip command. The ip command manages a list of named namespaces in /var/run/netns.

Now we create the wireguard device with ip link add dev wg0 type wireguard. The device name is wg0 but we could’ve chosen other names as well. Then we move the device to the netns we just created with ip link set wg0 netns vpn.

The wireguard device has the nice feature that it remembers what netns it got created in, not just the one it’s currently in. When the wireguard device connects to peers it does so via the netns it was created in. This is important because our “vpn” netns doesn’t have internet – the default namespace does – so we need some sort of connection to the default netns. This also means you don’t have to mess with configuring the default netns and possibly messing up other automated configurations.

Next we set the peers and private keys. First wg-quick strip wireguard.conf removes the Address field from the config file above, since this field is not supported by wg setconf. Then the result gets passed as wg setconf wg0 <output of previous wg-quick strip>.

Since the Address field isn’t supported by wg setconf this needs to be setup manually. We do this with ip address add dev wg0 <addr> for each address. If you have other “Interface” fields you want to set that aren’t supported, such as DNS, you can see the wg-quick source and copy how that’s setup.

Then we need to “turn on” the wireguard device with ip link set up dev wg0. Sometimes this will fail if there’s a problem with your config. We then turn on the local device, which is created automatically with ip link set up dev lo.

You can use wg show at this point for debugging, to inspect the peers and detect if traffic is flowing through the peers. Note even if everything is setup correctly. If not traffic is passing through (as there won’t be any at this point) the connection to the peers will appear silent, but once they connect it should show inbound and outbound traffic statistics.

Last piece is to setup the netns’s routes. The routes define how outgoing traffic should be routed, which in this case will be to the wireguard device. We can set this up with ip route add default dev wg0. We don’t have to add a route for localhost since this will automatically work.

The last command curl https://api.ipify.org/ is there to test everything.

Opening a Browser

You could do something like,

sudo ip netns exec vpn sudo -u eric google-chrome-stable

To open a browser running in the netns. This should work but could be a bit dangerous if leaks are a potential problem. For one if the browser could decide to use an existing process that is in the correct netns. You also have to make sure DNS is setup correctly, or use DNS over HTTPS.

Isolating only a single set of processes in a VPN will be more dangerous, because you can’t make an effective “kill switch”.

Sharing services

If you listen on a port in the default netns you will not be able to access from the vpn netns or vice versa. We can solve this by setting up a veth.

Let’s say on the default netns I start a server,

python -m http.server 8000  # Start web sever on port 8000

We can’t connect to this as localhost in the default netns. Instead, on the default netns create a veth pair,

ip link add veth0 type veth peer name veth0_peer

Then add the peer to the netns,

ip link set veth0_peer netns vpn

On each side the IP address needs to be setup. We’ll give the default namespace IP 10.255.255.0 and the vpn netns IP 10.255.255.1.

ip address add dev veth0 '10.255.255.0/31'
ip netns exec vpn ip address add dev veth0_peer '10.255.255.1/31'

ip link set veth0 up
ip netns exec vpn ip link set veth0_peer up

Now this should work,

ip netns exec vpn curl http://10.255.255.0:8000/

The veth0 and veth0_peer devices can be thought of as either ends of an ethernet cable, capable of passing traffic back and forth between them. If we want more flexibility we hook one end up to a bridge which allows, for example, many different network namespaces to be connected together using a single subnet. I found this article helpful in understanging how that’s usually set up.

Cleanup

All the resources used by the our vpn will get cleaned up when the namespace is deleted. To do this,

  1. Delete all processes using the netns, for instance kill $(ip netns pids vpn)
  2. Remove the netns with ip netns del vpn

The netns gets cleaned up by the kernell when there’s no more references to it, which could be either the file ip maintains in /var/run/netns or a process currently using the namespace. When the netns is removed you should notice veth0 no longer shows up in ip address.


  1. Linux namespaces are a broad topic and there’s more types than just network namespaces; for this reason I’m being careful not to refer to a network namespace as simply a namespace↩︎