Hardened Qubes VPN ProxyVM support for Qubes 4.3, nftables, strict DNS, and IPv6 disablement

Updated fork:


Overview

I have published an updated fork of tasket/Qubes-vpn-support.

This work is based on the original Qubes VPN support project by @tasket. That project provided the core model and much of the structure for running a VPN client inside a dedicated Qubes ProxyVM. Credit belongs to @tasket for the original design and implementation.

I also want to credit @1choice for laying important groundwork for the shift toward nftables.

The goal of this fork is to update and harden the project for a modern Qubes OS 4.3 setup using nftables, with a stricter fail-closed security model.

Most existing VPN-in-Qubes approaches either rely heavily on manual firewall configuration, partially documented scripts, NetworkManager behavior, or older iptables assumptions. Those can work, but they are often less elegant, less complete, or harder to reason about under failure conditions.

This update keeps the Qubes-native ProxyVM model while making the firewall, DNS, IPv6, and startup behavior more explicit.


Primary Security Changes

  • Migration from legacy iptables handling to nftables
  • Downstream VMs do not get internet access before the VPN tunnel is up
  • Downstream forwarding is allowed only toward the VPN tunnel interface
  • Return traffic is allowed only with ct state established,related
  • Upstream clearnet forwarding from downstream VMs is explicitly blocked
  • IPv6 is intentionally disabled and dropped, rather than partially supported
  • Downstream DNS is DNATed only after VPN DNS is available
  • VPN DNS handling falls back to QubesDB on Qubes 4.3 where the older helper file is absent
  • OpenVPN hostname remotes can be resolved through configured VPN DNS before connection
  • WireGuard hostname endpoints can also be resolved through configured VPN DNS before connection
  • Deployment can be staged so config files, certs, and credentials can be added after ProxyVM creation
  • Startup checks verify the expected firewall state before the VPN client runs
  • OpenVPN and WireGuard are now split into explicit backend selections instead of sharing one auto-detected startup path

Full architecture and change documentation:


Tested Baseline

  • Qubes OS 4.3.0
  • debian-13-minimal template from the Qubes repository
  • OpenVPN 2.6.14
  • nftables firewall backend

OpenVPN remains the primary supported backend.

WireGuard support is now handled through a dedicated service rather than an optional systemd override. It should still be considered more operationally sensitive because WireGuard is kernel-driven and does not map as cleanly to the same userspace process-group egress model.


Quickstart

1. Install In The TemplateVM

In the TemplateVM:

cd Qubes-vpn-support
sudo bash ./install

Shut down the TemplateVM.


2. Create The VPN ProxyVM

Create a dedicated VPN ProxyVM from that template.

In Qubes settings for the VPN ProxyVM:

  • Enable provides network
  • Set the NetVM as appropriate for your topology
  • Enable the Qubes service vpn-handler if you are using the shipped service model

Start the VPN ProxyVM.


3. Initialize The VPN ProxyVM

In the VPN ProxyVM, select the backend explicitly.

For OpenVPN:

sudo /usr/lib/qubes/qubes-vpn-setup --config-openvpn

For WireGuard:

sudo /usr/lib/qubes/qubes-vpn-setup --config-wireguard

This prepares /rw/config/vpn/ for you and writes a persistent backend marker for that ProxyVM under one of:

/rw/config/vpn/backend-openvpn
/rw/config/vpn/backend-wireguard

4. Add VPN Provider Files

Add your provider files to:

/rw/config/vpn/

At minimum, provide:

/rw/config/vpn/vpn-client.conf

Also place any required certs, keys, CRLs, or other provider files in that directory.

If username/password authentication is needed for OpenVPN:

sudo /usr/lib/qubes/qubes-vpn-setup --userpass

For WireGuard, vpn-client.conf should be a wg-quick style config.


5. Configure Strict DNS

For strict hostname handling, configure VPN DNS in the provider config.

For OpenVPN:

setenv vpn_dns "X.X.X.X Y.Y.Y.Y"
remote vpn.example.net 1194

For WireGuard:

DNS = X.X.X.X, Y.Y.Y.Y
Endpoint = vpn.example.net:51820

For the strictest setup, use an IPv4 remote or endpoint address instead of a hostname.

Backend selection is now stored by the persistent marker files under /rw/config/vpn/, not by a WireGuard drop-in under /rw/config/qubes-vpn-handler.service.d/.


6. Start The Service

For OpenVPN:

sudo systemctl restart qubes-firewall.service
sudo systemctl restart qubes-vpn-handler.service
sudo systemctl status qubes-vpn-handler.service

For WireGuard:

sudo systemctl restart qubes-firewall.service
sudo systemctl restart qubes-wg-handler.service
sudo systemctl status qubes-wg-handler.service

7. Verify Before Attaching Downstream VMs

ip -br link
sudo nft list chain ip qubes custom-forward
sudo nft list chain ip qubes dnat-dns
cat /var/run/qubes/qubes-vpn-ns
cat /proc/sys/net/ipv6/conf/all/disable_ipv6
ls -l /rw/config/vpn/backend-openvpn /rw/config/vpn/backend-wireguard

Expected high-level results:

  • The VPN tunnel interface exists, for example tun0 for OpenVPN or the WireGuard interface name from the config
  • The tunnel interface is assigned to group 9
  • custom-forward contains downstream-to-VPN forwarding and stateful return rules
  • dnat-dns contains DNS DNAT rules after the VPN is up
  • /var/run/qubes/qubes-vpn-ns contains the VPN DNS values
  • IPv6 disablement reports 1
  • Exactly one backend marker exists under /rw/config/vpn/

Only attach downstream AppVMs to the VPN ProxyVM after verifying the tunnel and DNS rules.


Testing And Feedback

This is security-sensitive networking code. Please test carefully before relying on it for important workloads.

Useful things to test:

  • Downstream VM has no internet before VPN connection
  • Downstream DNS does not work before VPN connection
  • Downstream traffic works after VPN connection
  • Downstream DNS is redirected to VPN DNS after connection
  • Traffic stops when the VPN service is stopped or restarted
  • IPv6 is unavailable
  • OpenVPN reconnects behave as expected
  • WireGuard startup and reconnect behavior work as expected with the dedicated qubes-wg-handler.service
  • Behavior with non-standard NetVM chains, such as Mirage Firewall or another ProxyVM

If you find bugs, edge cases, provider-specific issues, or Qubes-version differences, please open an issue or submit a pull request.

The intent is to make this easier to audit and more robust for the community while preserving the basic architecture that made the original Qubes-vpn-support project useful.

3 Likes

IPv6 is the internet. Those who like to travel a lot this decade know that many places are IPv6-only and IPv4 is reached through tunnels. In enough places already IPv4 noticeably is not.

On many networks you can still use IPv4 from behind the local router and the packets are going to get translated by 464xlat. The packets get emitted as IPv6 when they leave the router then as rewritten as IPv4 when they leave the ISP.

Other places use dns46 and nat46 to move packets for EOL devices.

Some Mullvad customers connect to Mullvad’s Wireguard endpoints using IPv6 and use mostly IPv6 in tunnels. Most of Mullvad’s customers don’t know they are using IPv6. More of Mullvad’s customers using Qubes who know what they are doing with Linux distributions know they are using IPv6 than ones who do not.

A good litmus test for whether one is a legitimate IT professional (of any kind, operating in commerce or other) is the health of IPv6 on any networks one is responsible for.

IPv4 is kept alive solely by the hobbyist demographic.

Encouraging the disabling of IPv6 is actively harmful not just to the internet as a whole but specifically to the users. By encouraging others to disable IPv6, you are causing harm to users naive enough to follow such ill advice.

Hey demosios.

Just wanted to drop a note saying thanks for putting in the effort to release the update for R4.3.

Went relatively smoothly. Only gotcha was a no-start + forever zenity messaging which turned out to be the following.

  1. VPN provider pushing an IPv6 address and route to the client.
  2. The hardened script disables/drops IPv6 in the kernel
  3. OpenVPN process lacks permission to add an IPv6 address to the tun0 interface.
  4. OpenVPN treats it as a fatal error and immediately exits.
  5. qubes-vpn-handler service sees the crash and restarts.
  6. rise repeat => loop of zenity notifications.

Quick fix was to add following to ovpn config files to continue using IPv4.

pull-filter ignore “tun-ipv6”
pull-filter ignore “ifconfig-ipv6”
pull-filter ignore “route-ipv6”

What places, specifically? I don’t think I’ve ever come across something like that before and I’m curious.

I think your rant has its place, but I question whether this is it. Do Qubes users need to be legitimate IT professionals now? Should @demosios be one to create a hardened VPN qube template? How will Qubes users disabling IPv6 for VPN connections be harmful for the internet? If its going to get translated anyway, as you noted, why should it matter?

From the user-side and being definitely not a legitimate IT professional, my experience with disabling IPv6 has been that there have been times where I couldn’t access hosts with only IPv6 addresses. Not the end of the world but annoying.

My concern with IPv6 is that I do not know it as well as IPv4. My estimation is that its more complex and has more gotchas in terms of not leaking packets. From a security/privacy perspective, it makes sense to me to disable it.

I think your rant applies more to network operators and service operators.

1 Like

Thank you all for the responses. As this is open-source, anyone wishing to enable IPV6 functionality is free to do and they are welcome to post the change here in detail for the rest of us. I am not a ‘legitimate IT professional’.

2 Likes

@de_dust2 There are tons of reasons to disable IPv6 for privacy focused users, from risks of IP leakages to OS fingerprinting leaks. I would understand you if it is a common Ubuntu app that forces the crowd to disable IPv6, but in this case disabling IPv6 is totally understandable. Let the crowd make IPv6 more secure and common, while accept that privacy focused users are not ready for it yet until it is more battle tested.

1 Like

I guess I’m the first to notice that issues haven’t been enabled for the repo. :upside_down_face:

Considering that others appear to have had success following the instructions, I’m thinking I must have gotten off track somewhere. I’ve not yet gotten things working and running into some issues.

I’ve configured the proxyvm for openvpn. And when I get to restarting the qubes-vpn-handler service, it fails to start because /var/run/qubes-service/vpn-handler does not exist. How is that supposed to be created? I don’t see anything in the qubes-vpn-support repo that creates that file. This documentation suggests that qvm-service could be used, but qvm-service --enable qubes-vpn-handler True does not create it. If I create the file manually, I can start the service. I’m not very familiar with services, so am I misreading that page?

What was I supposed to do instead of creating the file manually?

Also, is it intentional that a failure in check_dependencies during install is ignored? If so, perhaps the return code should be explicitly ignored. Along these lines, it would probably be good to enable set -e so that the script dies on unhandled errors. This way the user knows things didn’t complete successfully and doesn’t expect things to work.