Wireguard VPN w/ namespace killswitch

Original forum link
https://forum.qubes-os.org/t/35168
Original poster
David
Created at
2025-07-31 10:40:59
Posts count
2
Likes count
0
Tags
networking, version-r42

Introduction

I found this interesting article about using wireguard's integration with network namespaces to provide a simple but rock solid "killswitch" that prevents network leaks.

The idea is that you can create the wireguard device (wg0 here) in one network namespace(physical) and then move it to another(init) and it "remembers" the original namespace as the one to send ciphertext.

image|630x221

Now because the plaintext network devices are in another namespace, nothing other than the wireguard device can communicate with applications in the "init" namespace.

Here's how I setup a vpn qube using this method.

Setup

Create a new qube providing network

Menu » Qubes Tools » Create Qubes VM:

And then OK. Then the qube settings window should show up (proceed to the next step).

Enable service qubes-firewall

In the qube settings window enable the qubes-firewall service.

Acquire a wireguard config

Get a wireguard config for your vpn. Here is an example of a typical wg-quick config file. Comment out everything except the uncommented fields in the example. This is necessary because we are going to setup the wireguard device directly, rather than use wg-quick. Here I'm commenting out the Address, MTU and DNS fields.

[Interface]
#Address = 192.168.71.2/24,fdc9:3c6b:21c7:e6bd::2/64
PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
ListenPort = 51821
#MTU = 1320
#DNS = 88.66.44.23

[Peer]
PublicKey = TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=
PresharedKey = /UwcSPg38hW/D9Y3tcS1FOV0K1wuURMbS0sesJEP5ak=
Endpoint = 88.66.44.22:51820
AllowedIPs = 0.0.0.0/0,::/0
Put this file in /rw/config/wg0.conf.

Create setup_vpn.bash script

Next, create a shell script that implements the steps from the linked article. You'll have to set the three variables at the top.

#!/bin/bash

set -eux

PHYSICAL_IP="10.137.0.17/32"
PHYSICAL_ROUTE="10.138.32.193"
WG_IP="192.168.72.2/32"

# create netns named "physical", move eth0 and create wg0
ip netns add physical 
ip link set eth0 netns physical
ip -n physical link add wg0 type wireguard
# move wg0 to default netns
ip -n physical link set wg0 netns 1

# eth0 lost its configuration when we moved it to the 
# physical namespace so reconfigure it
ip -n physical addr add "$PHYSICAL_IP" dev eth0
ip -n physical link set eth0 up
ip -n physical route add default via "$PHYSICAL_ROUTE" dev eth0 onlink

# setup wireguard
wg setconf wg0 /rw/config/wg0.conf
ip addr add "$WG_IP" dev wg0 
ip link set wg0 up
ip route add default dev wg0
Place this file in /rw/config/setup_vpn.bash.

Call setup_vpn.bash

Add this line to the bottom of (the currently empty except for comments) /rw/config/qubes-firewall-user-script:

bash /rw/config/setup_vpn.bash

Now if you restart the vpn qube, you should see wireguard configured with wg show and qubes configured to use the vpn qubes for networking should be able to ping ip addresses. Getting DNS to work is our final step.

Fix DNS

Finally we need to fix DNS by replacing the DNS nat rules. This isn't necessary for the killswitch, just to allow AppVMs using the vpn qubes from having to change their dns configuration.

Write this file to /rw/config/nftables.conf and set the two variables at the top:

nft -f /rw/config/nftables.conf

All done!

Now if you restart your vpn qube, wireguard should be configured and qubes using it for networking should have network and dns access.

Caveats

I'm pretty familiar with Linux networking and networking in general but I'm new to Qubes.