Configuring a ProxyVM VPN Gateway

Can you submit this as an pull request, perhaps to https://github.com/QubesOS/qubes-core-agent-linux?

This could be a silly question, but where is the NetworkManager documentation?

How to Set Up sys-vpn Qube with OpenVPN

Introduction

This tutorial creates a sys-vpn Qube with OpenVPN, killswitch, and DNS leak protection using nftables redirection.

To easy use this tutorial and copy the commands, you could transfer this tutorial in a txt file to dom0.

[user@dom0 ~]$ qvm-run --pass-io disp6666 'cat ~/Downloads/tutorial.txt' > ~/tutorial.txt

Step 1: Create and Update the Template

[user@dom0 ~]$ sudo qubes-dom0-update qubes-template-debian-12-minimal
[user@dom0 ~]$ sudo qubesctl --show-output --skip-dom0 --targets=debian-12-minimal state.sls update.qubes-vm

Step 2: Install Packages in the Template

[user@dom0 ~]$ qvm-run -u root debian-12-minimal \
"apt update -y && apt install -y --no-install-recommends \
qubes-core-agent-networking \
qubes-core-agent-passwordless-root \
openvpn \
nftables"
[user@dom0 ~]$ qvm-shutdown debian-12-minimal

Step 3: Create sys-vpn Qube

[user@dom0 ~]$ qvm-create --class AppVM --property template=debian-12-minimal \
--label orange --property netvm=sys-firewall --property provides_network=True \
--property autostart=True sys-vpn
[user@dom0 ~]$ qvm-start sys-vpn

Step 4: Prepare sys-vpn Qube with OpenVPN

Install packages in sys-vpn again:

[user@dom0 ~]$ qvm-run -u root sys-vpn "apt update -y && apt install -y openvpn nftables dnsutils"

If running, disable any auto-starting service that comes with the software package:

[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"
[user@sys-vpn ~]# dpkg -L openvpn | grep systemd
[user@sys-vpn ~]# systemctl list-units | grep openvpn
[user@sys-vpn ~]# systemctl disable openvpn.service
[user@sys-vpn ~]# systemctl disable openvpn@.service
[user@sys-vpn ~]# systemctl disable openvpn-client@.service
[user@sys-vpn ~]# systemctl disable openvpn-server@.service

Download and extract config files accordingly your VPN provider.

Transfer your OpenVPN configuration file (usually ending in .ovpn) to the sys-vpn qube. You can do this by:

  • From disp6666 (DispVM) to sys-vpn (AppVM) with GUI:
  • Use Qube's file copy feature through the context menu by right-clicking on the file and selecting "Copy to other qube".
Alternatively, you could use other CLI methods
  • From Dom0 (1):
  • [user@disp6666 ~]$ tar -czvf configurations.tar.gz /home/user/configurations
    [user@dom0 ~]$ qvm-run --pass-io disp6666 'cat /home/user/configurations.tar.gz' > ~/configurations.tar.gz
    [user@dom0 ~]$ tar -xzvf /home/user/configurations.tar.gz -C /home/user/
    [user@dom0 ~]$ qvm-copy-to-vm sys-vpn configurations
    [user@dom0 ~]$ qvm-run -u root sys-vpn "mkdir -p /rw/config/vpn && mv /home/user/QubesIncoming/dom0/configurations/* /rw/config/vpn"
  • From Dom0 (2):
  • [user@dom0 ~]$ qvm-run -u root sys-vpn "mkdir -p /rw/config/vpn && cd /rw/config/vpn && curl --https-only -o configurations.zip 'https://myvpn.com/config' && unzip -q /rw/config/vpn/configurations.zip -d /rw/config/vpn && rm /rw/config/vpn/configurations.zip"
  • From whonix-dvm DispVM in CLI mode:
  • You need to modify the RPC policy to allow AppVM to run commands in the sys-vpn VM VM in CLI mode.

    [user@dom0 ~]$ nano /etc/qubes/policy.d/30-default.policy
    qubes.VMShell * @dispvm:whonix-dvm sys-vpn allow
    [user@dom0 ~]$ sudo systemctl restart qubesd
    [user@disp6666 ~]$ qvm-copy-to-vm sys-vpn /home/user/Downloads/configurations
    [user@disp6666 ~]$ qvm-run-vm sys-vpn "sudo mkdir -p /rw/config/vpn"
    [user@disp6666 ~]$ qvm-run-vm sys-vpn 'sudo mv /QubesIncoming/disp6666/configurations $HOME'
    [user@disp6666 ~]$ qvm-run-vm sys-vpn "sudo unzip -q /rw/config/vpn/configurations.zip -d /rw/config/vpn && sudo rm /rw/config/vpn/configurations.zip"

Note that files accompanying the main config such as *.crt and *.pem should also be placed in the /rw/config/vpn folder.

Configure the traffic route and your credentialsin the .ovpn config file, add or uncomment these lines:

[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"
[user@disp6666 ~]$ nano /rw/config/vpn/wg0.conf
redirect-gateway def1
auth-user-pass /rw/config/vpn/auth.txt #Should not use absolute paths
auth-nocache

Replace your credentials in auth like this:

[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"
[user@disp6666 ~]$ nano /rw/config/vpn/auth.txt

username
password

From dom0:

[user@dom0 ~]$ qvm-run -u root sys-vpn "echo -e 'username\npassword' | tee /rw/config/vpn/auth.txt"

Secure credential handling:

[user@dom0 ~]$ qvm-run -u root sys-vpn "chmod 600 /rw/config/vpn/auth.txt"

Step 5: Test Connection

Select VPN connection:

[user@dom0 ~]$ qvm-run -u root sys-vpn "openvpn --cd /rw/config/vpn --config xx_vpn_xxx.ovpn --daemon"

Alternatively, select a radom config:

[user@dom0 ~]$ qvm-run -u root sys-vpn "openvpn --cd /rw/config/vpn --config "$(find /rw/config/vpn -name '*.ovpn' | shuf -n 1)" --daemon"

Verify VPN connection before enabling killswitch:

[user@dom0 ~]$ qvm-run -u root sys-vpn "ip a && ping -c 3 9.9.9.9 && curl ipleak.net/json/ && cat /etc/resolv.conf && nslookup resolver1.opendns.com"

Step 6: Configure nftables Rules for DNS, Killswitch and ICMP

Based on: https://forum.qubes-os.org/t/configuring-a-proxyvm-vpn-gateway/19061/58.

Use this command to find services to disable:

[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"
[user@sys-vpn ~]# dpkg -L openvpn | grep systemd
[user@sys-vpn ~]# systemctl list-units | grep openvpn

To copy and paste in XTerm you need to edit the settings:

[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"
[user@sys-vpn ~]# dpkg -L openvpn | grep systemd
Theme for XTerm
! Theme for XTerm
! After editing, run 'xrdb -merge ~/.Xresources' to apply changes

! Font settings
XTermfaceName: DejaVu Sans Mono
XTerm
faceSize: 12
XTermrenderFont: true
XTerm
boldFont: DejaVu Sans Mono Bold
XTermcolorBDMode: true
XTerm
colorBD: red

! Cursor settings
XTermcursorColor: white
XTerm
cursorBlink: true

! Scrollbar
XTermscrollBar: true
XTerm
rightScrollBar: true
XTerm*scrollTtyOutput: false

! Behavior settings
XTermsaveLines: 2000
XTerm
selectToClipboard: true
XTermlocale: true
XTerm
utf8: 1
XTermvisualBell: true
XTerm
bellIsUrgent: false

! Color Theme
XTermforeground: #009900 ! Green text
XTerm
background: #000000 ! Pure black background
XTermcolor0: #000000 ! Black
XTerm
color1: #00AA00 ! Dark green
XTerm*color2: #00FF00 ! Bright green

! Keybindings for copy-paste
XTerm*VT100.translations: #override
Ctrl Shift C: copy-selection(CLIPBOARD) \n
Ctrl Shift V: insert-selection(CLIPBOARD) \n
~Shift : select-start() \n
~Shift : select-extend() \n
~Shift : select-end(PRIMARY) \n
Shift : select-end(CLIPBOARD)

! Debugging ‘xrdb -query | grep XTerm’

Create a script to handle the DNS:

[user@dom0 ~]$ qvm-run -u root sys-vpn "cat << EOF | tee /rw/config/vpn/qubes-vpn-handler.sh
case "$1" in
  
up)
# To override DHCP DNS, assign DNS addresses to 'vpn_dns' env variable before calling this script;
# Format is 'X.X.X.X  Y.Y.Y.Y [...]'
if [[ -z "$vpn_dns" ]] ; then
    # Parses DHCP foreign_option_* vars to automatically set DNS address translation:
    for optionname in ${!foreign_option_*} ; do
        option="${!optionname}"
        unset fops; fops=($option)
        if [ ${fops[1]} == "DNS" ] ; then vpn_dns="$vpn_dns ${fops[2]}" ; fi
    done
fi
  
nft flush chain inet user-vpn prerouting-nat
nft flush chain inet user-vpn forward-filter-dns
if [[ -n "$vpn_dns" ]] ; then
    # Set DNS address translation in firewall:
    for addr in $vpn_dns; do
    nft add rule inet user-vpn prerouting-nat iifgroup 2 udp dport 53 counter dnat ip to $addr
    nft insert rule inet user-vpn forward-filter-dns iifgroup 2 udp dport 53 ip daddr $addr counter accept

    nft add rule inet user-vpn prerouting-nat iifgroup 2 tcp dport 53 counter dnat ip to $addr
    nft insert rule inet user-vpn forward-filter-dns iifgroup 2 tcp dport 53 ip daddr $addr counter accept
    done
    su - -c 'notify-send "$(hostname): LINK IS UP." --icon=network-idle' user
else
    su - -c 'notify-send "$(hostname): LINK UP, NO DNS!" --icon=dialog-error' user
fi
  
;;
down)
su - -c 'notify-send "$(hostname): LINK IS DOWN !" --icon=dialog-error' user

# Restart the VPN automatically
sleep 5s
sudo /rw/config/rc.local
;;
esac
EOF"
[user@dom0 ~]$ qvm-run -u root sys-vpn "chmod +x /rw/config/vpn/qubes-vpn-handler.sh"

Configure client to use the DNS handling script, edit the openvpn config:

[user@dom0 ~]$ qvm-run -u root sys-vpn "cat << EOF | tee /rw/config/vpn/openvpn-client.ovpn
script-security 2
up 'qubes-vpn-handler.sh up'
down 'qubes-vpn-handler.sh down'
EOF"

Restart the client and test the connection again:

[user@dom0 ~]$ qvm-shutdown sys-vpn --wait
[user@dom0 ~]$ qvm-start sys-vpn

Set up nftables anti-leak rules:

[user@dom0 ~]$ qvm-run -u root sys-vpn "cat << EOF | tee /rw/config/vpn/qubes-vpn-firewall.sh
#!/bin/bash

set -e

# Add the `qvpn` group to system. The sync and sleep were in the original script but might not be needed.
groupadd -rf qvpn; sync; sleep 2s

nft add table inet user-vpn

nft 'add chain inet user-vpn forward-filter { type filter hook forward priority filter - 1; policy accept; }'

# Block forwarding of connections through upstream network device (in case the vpn tunnel breaks or packets are routed
# directly between upstream and tunnel interface).
nft add rule inet user-vpn forward-filter meta iifgroup 1 counter drop
nft add rule inet user-vpn forward-filter meta oifgroup 1 counter drop
nft add rule inet user-vpn forward-filter meta mark set 3000 counter

nft add chain inet user-vpn forward-filter-dns
nft add rule inet user-vpn forward-filter jump forward-filter-dns

# The up script adds a DNAT rule to prerouting-nat and a corresponding accept rule to forward-filter-dns. If the accept
# rule is not hit then the DNAT was not applied and the packet should be dropped else it will go through the tunnel with
# the AppVM DNS (DNS leak). This could happen if the tunnel is created after a packet passes prerouting-nat but before
# the routing decision is made. If Qubes adds a higher priority DNAT DNS rule then DNS will stop working but not leak.
nft add rule inet user-vpn forward-filter iifgroup 2 udp dport 53 counter drop
nft add rule inet user-vpn forward-filter iifgroup 2 tcp dport 53 counter drop

nft 'add chain inet user-vpn postrouting-filter { type filter hook postrouting priority filter - 1; policy accept; }'
# Tunnel broke after forward-filter and the reroute check redirected packet to oifgroup 1.
nft add rule inet user-vpn postrouting-filter meta mark 3000 oifgroup 1 counter drop

# Drop all output through the tunnel interface and only allow traffic from the `qvpn` group to the uplink interface. Our
# VPN client will run with group `qvpn`.
nft 'add chain inet user-vpn output-filter { type filter hook output priority filter - 1; policy drop; }'
nft add rule inet user-vpn output-filter meta oifgroup 1 skgid qvpn counter accept
nft add rule inet user-vpn output-filter meta oifgroup 2 counter accept
nft add rule inet user-vpn output-filter meta oifname "lo" counter accept
nft add rule inet user-vpn output-filter counter

# Block all IPv6 traffic
nft add rule inet user-vpn forward-filter meta nfproto ip6 counter drop
nft add rule inet user-vpn output-filter meta nfproto ip6 counter drop

# Or if you eventually need IPv6-over-VPN:
# nft add rule inet user-vpn forward-filter meta nfproto ip6 oifname != "tun0" counter drop

# Chain for up script.
nft 'add chain inet user-vpn prerouting-nat { type nat hook prerouting priority dstnat - 1; policy accept; }'

# Rely on Qubes rules for masquerade.
# Rely on Qubes rules for preventing inbound packets (they're dropped unless established,related).
EOF"
[user@dom0 ~]$ qvm-run -u root sys-vpn "chmod +x /rw/config/vpn/qubes-vpn-firewall.sh"

Step 7: Configure Autostart

VPN autostart with static OpenVPN config selection:

[user@dom0 ~]$ qvm-run -u root sys-vpn "cat << EOF | tee /rw/config/rc.local
#!/bin/bash
export VPN_CLIENT='openvpn'
VPN_OPTIONS='--cd /rw/config/vpn/ --config openvpn-client.ovpn --daemon'

sg qvpn -c "$VPN_CLIENT $VPN_OPTIONS"
su - --whitelist-environment=VPN_CLIENT -c 'notify-send "$(hostname): Starting $VPN_CLIENT..." --icon=network-idle' user
EOF"
[user@dom0 ~]$ qvm-run -u root sys-vpn "chmod +x /rw/config/rc.local"

Alternative: VṔN autostart with with random OpenVPN config selection:

[user@dom0 ~]$ qvm-run -u root sys-vpn "cat << 'EOF' | tee /rw/config/rc.local
#!/bin/bash
# Wait for network initialization
sleep 5

# Find all VPN configs
CONFIGS=($(find /rw/config/vpn -name '*.ovpn' -o -name '*.conf'))
if [ ${#CONFIGS[@]} -eq 0 ]; then
    su - -c 'notify-send "$(hostname): No VPN configs found!" --icon=dialog-error' user
    exit 1
fi

# Select random config
RANDOM_CONFIG=$(shuf -n1 -e "${CONFIGS[@]}")
su - -c 'notify-send "$(hostname): Connecting via ${RANDOM_CONFIG##*/}..." --icon=network-idle' user

# Start VPN with selected config
if ! sg qvpn -c "openvpn --cd /rw/config/vpn --config \"$RANDOM_CONFIG\" --daemon"; then
    su - -c 'notify-send "$(hostname): VPN failed to start!" --icon=dialog-error' user
    exit 1
fi

# Wait for connection
sleep 5

# Verify connection
if ! ping -c 1 -W 5 9.9.9.9; then
    su - -c 'notify-send "$(hostname): VPN connection failed!" --icon=dialog-error' user
    exit 1
fi

# Apply firewall rules
/rw/config/qubes-firewall-user-script

# Final notification
su - -c 'notify-send "$(hostname): VPN connected via ${RANDOM_CONFIG##*/}" --icon=network-idle' user
EOF"
[user@dom0 ~]$ qvm-run -u root sys-vpn "chmod +x /rw/config/rc.local"

Create systemd service in TemplateVM:

[user@dom0 ~]$ qvm-run -u root sys-vpn "cat << EOF | tee -a /lib/systemd/system/qubes-vpn-firewall.service
[Unit]
Description=Qubes VPN firewall updater
After=qubes-firewall.service
Before=qubes-network.service

[Service]
type=oneshot
ExecStart=/rw/config/vpn/qubes-vpn-firewall.sh

[Install]
RequiredBy=qubes-network.service
EOF"
[user@dom0 ~]$ qvm-run -u root sys-vpn "systemctl enable qubes-vpn-firewall.service"

Step 8: Final Steps

[user@dom0 ~]$ qvm-shutdown sys-vpn --wait
[user@dom0 ~]$ qvm-start sys-vpn

Remember that to use the VPN with other qubes you need to set their netvm to sys-vpn.

[user@dom0 ~]$ qvm-prefs work netvm sys-vpn

Verify the connection again:

[user@dom0 ~]$ qvm-run -u root sys-vpn "nft list ruleset"
[user@dom0 ~]$ qvm-run -u root sys-vpn "ip a && ping -c 3 9.9.9.9 && curl ipleak.net/json/ && cat /etc/resolv.conf && nslookup resolver1.opendns.com"

Check for DNS leaks in www.dnsleaktest.com with "Extended test"

You might need to adjust MTU settings if experiences connectivity issues (add 'tun-mtu 1500').

I guess this link is the one, but the link is broken.

https://web.archive.org/web/20221119200319/https://docs.fedoraproject.org/en-US/Fedora/23/html/Networking_Guide/sec-Establishing_a_VPN_Connection.html

2 Likes

I gave up OpenVPN. I want to set up sys-vpn Qube with Wireguard in Debian minimal template without Network Manager.

Looks like my WireGuard connection is established but no traffic is actually getting through.

Do I need to split tunnel, routing only specific traffic through allowed IPs in Wireguard config? How?


/rw/config/vpn/wg.conf 
[Peer] 
AllowedIPs = 0.0.0.0/1, 128.0.0.0/1

Do I need to add explicit route for Qubes internal network? How?


ip route add 10.137.0.0/16 via 10.137.0.1 dev eth0

Do I need to add strict DNS rules to avoid leakage in Wireguard config?


/rw/config/vpn/wg.conf 
[Interface]
PostUp = resolvectl dns %i 10.200.100.1 11.201.101.2; resolvectl domain %i ~.
PostDown = resolvectl revert %i

Im using Qubes >4.2

Could someone do a complete tutorial to address this issues with VPN? I follow these steps bellow and Im stuck at Step 5:


How to Set Up sys-vpn Qube with Wireguard in Debian minimal template without Network Manager

Introduction

This tutorial creates a sys-vpn Qube with Wireguard, killswitch, and DNS leak protection using nftables redirection in Debian minimal without Network Manager.

To follow this tutorial easily

To easily follow this tutorial and copy the commands, you could transfer this tutorial in a txt file to dom0:

[user@dom0 ~]$ qvm-run --pass-io disp6666 'cat ~/Downloads/tutorial.txt' > ~/tutorial.txt
[user@dom0 ~]$ cat ~/tutorial.txt

To make your job easier, you can preserve the terminal history in multiple instances:

# Don't erase history
[user@dom0 ~]$ shopt -s histappend
# Make the history save and reload after each command finishes
[user@dom0 ~]$ export PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"
# No duplicate entries
[user@dom0 ~]$ export HISTCONTROL=ignoredups:erasedups
# Remember the last 1000 commands
[user@dom0 ~]$ export HISTSIZE=1000
# Truncate commands after 2000 lines
[user@dom0 ~]$ export HISTFILESIZE=2000

Step 1: Create and Update the Template

[user@dom0 ~]$ sudo qubes-dom0-update qubes-template-debian-12-minimal
[user@dom0 ~]$ sudo qubesctl --show-output --skip-dom0 --targets=debian-12-minimal state.sls update.qubes-vm

Step 1: Create and Update the Template

[user@dom0 ~]$ sudo qubes-dom0-update qubes-template-debian-12-minimal
[user@dom0 ~]$ sudo qubesctl --show-output --skip-dom0 --targets=debian-12-minimal state.sls update.qubes-vm

Step 2: Install Packages in the Template

[user@dom0 ~]$ qvm-run -u root debian-12-minimal \
"apt update -y && apt install -y --no-install-recommends \
qubes-core-agent-networking \
qubes-core-agent-passwordless-root \
wireguard-tools \
nftables \
openresolv"
[user@dom0 ~]$ qvm-shutdown debian-12-minimal

Step 3: Create sys-vpn Qube

[user@dom0 ~]$ qvm-create --class AppVM --property template=debian-12-minimal \
--label orange --property netvm=sys-firewall --property provides_network=True \
--property autostart=True sys-vpn
[user@dom0 ~]$ qvm-start sys-vpn

Step 4: Prepare WireGuard Configuration

Install packages in sys-vpn again, including dnsutils and curl:

[user@dom0 ~]$ qvm-run -u root sys-vpn "apt update -y && apt install -y wireguard-tools nftables openresolv dnsutils curl"

If running, disable any auto-starting service that comes with the software package:

[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"
[user@sys-vpn ~]# dpkg -L wireguard-tools | grep systemd
[user@sys-vpn ~]# systemctl list-units | grep wg
[user@sys-vpn ~]# systemctl disable wg-quick@.service

Download and extract config files accordingly your VPN provider.

Transfer your WireGuard configuration file (usually ending in .conf) to the sys-vpn qube. You can do this by:

  • From disp6666 (DispVM) to sys-vpn (AppVM) with GUI:
  • Use Qube's file copy feature through the context menu by right-clicking on the file and selecting "Copy to other qube".
Alternatively, you could use other CLI methods
  • From Dom0 (1):
  • [user@disp6666 ~]$ tar -czvf configurations.tar.gz /home/user/configurations
    [user@dom0 ~]$ qvm-run --pass-io disp6666 'cat /home/user/configurations.tar.gz' > ~/configurations.tar.gz
    [user@dom0 ~]$ tar -xzvf /home/user/configurations.tar.gz -C /home/user/
    [user@dom0 ~]$ qvm-copy-to-vm sys-vpn configurations
    [user@dom0 ~]$ qvm-run -u root sys-vpn "mkdir -p /rw/config/vpn && mv /home/user/QubesIncoming/dom0/configurations/* /rw/config/vpn"
  • From Dom0 (2):
  • [user@dom0 ~]$ qvm-run -u root sys-vpn "mkdir -p /rw/config/vpn && cd /rw/config/vpn && curl --https-only -o configurations.zip 'https://myvpn.com/config' && unzip -q /rw/config/vpn/configurations.zip -d /rw/config/vpn && rm /rw/config/vpn/configurations.zip"
  • From whonix-dvm DispVM in CLI mode:
  • You need to modify the RPC policy to allow AppVM to run commands in the sys-vpn VM in CLI mode.

    [user@dom0 ~]$ nano /etc/qubes/policy.d/30-default.policy
    qubes.VMShell * @dispvm:whonix-dvm sys-vpn allow
    [user@dom0 ~]$ sudo systemctl restart qubesd
    [user@disp6666 ~]$ qvm-copy-to-vm sys-vpn /home/user/Downloads/configurations
    [user@disp6666 ~]$ qvm-run-vm sys-vpn "sudo mkdir -p /rw/config/vpn"
    [user@disp6666 ~]$ qvm-run-vm sys-vpn 'sudo mv /QubesIncoming/disp6666/configurations $HOME'
    [user@disp6666 ~]$ qvm-run-vm sys-vpn "sudo unzip -q /rw/config/vpn/configurations.zip -d /rw/config/vpn && sudo rm /rw/config/vpn/configurations.zip"

Step 5: Configure WireGuard

To copy and paste in XTerm you need to edit the settings (~/.Xresource):
[user@dom0 ~]$ qvm-run sys-vpn 'cat << "EOF" | tee /home/user/.Xresource
! Theme for XTerm
! To apply changes, run: xrdb -merge /home/user/.Xresource

! Font settings
XTerm*faceName: DejaVu Sans Mono
XTerm*faceSize: 12
XTerm*renderFont: true
XTerm*boldFont: DejaVu Sans Mono Bold
XTerm*colorBDMode: true
XTerm*colorBD: red

! Scrollbar
XTerm*scrollBar: true
XTerm*rightScrollBar: true
XTerm*scrollTtyOutput: false

! Behavior settings
XTerm*shell: /bin/bash
XTerm*saveLines: 2000
XTerm*locale: false
XTerm*utf8: 1
XTerm*visualBell: true
XTerm*bellIsUrgent: false

! Cursor settings
XTerm*cursorColor: white
XTerm*cursorBlink: true
XTerm*highlightSelection: true
XTerm*selectToClipboard: true

! Green text
XTerm*foreground: #009900
! Pure black background
XTerm*background: #000000
! Black
XTerm*color0: #000000
! Dark green
XTerm*color1: #00AA00
! Bright green
XTerm*color2: #00FF00

! Keybindings for copy-paste
XTerm*VT100.translations: #override \
  Ctrl Alt <Key>C: copy-selection(CLIPBOARD) \n\
  Ctrl Alt <Key>V: insert-selection(CLIPBOARD) \n\
  ~Shift <Btn1Down>: select-start() \n\
  ~Shift <Btn1Motion>: select-extend() \n\
  ~Shift <Btn1Up>: select-end(PRIMARY) \n\
  Shift <Btn1Up>: select-end(CLIPBOARD) \n\
  Ctrl <Key>L: clear-saved-lines() \n\
  Ctrl <Key>C: string(0x03) \n\
  Ctrl <Key>D: string(0x04)

! Debugging: xrdb -query | grep XTerm
EOF'

Apply the changes:

[user@dom0 ~]$ qvm-run sys-vpn 'xrdb -merge /home/user/.Xresource'

Edit the Wireguard configuration file if needed.

A WireGuard config looks like:


[Interface]
Address = 10.200.100.8/24
DNS = 10.200.100.1 11.201.101.2
PrivateKey = oK56DE9Ue9zK76rAc8pBl6opph+1v36lm7cXXsQKrQM=
PostUp = resolvectl dns %i 10.200.100.1 11.201.101.2; resolvectl domain %i ~.
PostDown = resolvectl revert %i
MTU = 1500
[Peer]
PublicKey = GtL7fZc/bLnqZldpVofMCD6hDjrK28SsdLxevJ+qtKU=
PresharedKey = /UwcSPg38hW/D9Y3tcS1FOV0K1wuURMbS0sesJEP5ak=
AllowedIPs = 0.0.0.0/0
Endpoint = demo.wireguard.com:51820

Start an interactive terminal in sys-vpn and perform a basic connectivity test before set firewall rules:

[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"

# Start
[user@sys-vpn ~]# wg-quick up /rw/config/vpn/yourconfig.conf # Connect
[user@sys-vpn ~]# ping -c 4 9.9.9.9      # Test basic connectivity
[user@sys-vpn ~]# curl ipleak.net/json/  # Check public IP
[user@sys-vpn ~]# wg show                # Verify status

# DNS test
[user@sys-vpn ~]# cat /etc/resolv.conf
[user@sys-vpn ~]# resolvconf -l

# Stop
[user@sys-vpn ~]# wg-quick down /rw/config/vpn/yourconfig.conf

Step 6: Create Connection Script

[user@dom0 ~]$ qvm-run -u root sys-vpn "cat << 'EOF' | tee /rw/config/vpn/wg-up.sh
#!/bin/bash

# Wait for network to be ready
while ! ping -c1 10.139.1.1 &>/dev/null; do
  sleep 1
done

# Start WireGuard
wg-quick up /rw/config/vpn/wg0.conf

# Apply firewall rules
/rw/config/vpn/qubes-vpn-firewall.sh

# Notification
su - -c 'notify-send "WireGuard VPN Connected" --icon=network-idle' user
EOF"
[user@dom0 ~]$ qvm-run -u root sys-vpn "chmod +x /rw/config/vpn/wg-up.sh"

Step 7: Configure nftables Rules

[user@dom0 ~]$ qvm-run -u root sys-vpn "cat << 'EOF' | tee /rw/config/vpn/qubes-vpn-firewall.sh
#!/bin/bash

set -e

# Add the qvpn group
groupadd -rf qvpn

# Create tables and chains
nft add table inet user-vpn
nft 'add chain inet user-vpn forward-filter { type filter hook forward priority filter - 1; policy accept; }'

# Block forwarding of connections through upstream network device
nft add rule inet user-vpn forward-filter meta iifgroup 1 counter drop
nft add rule inet user-vpn forward-filter meta oifgroup 1 counter drop
nft add rule inet user-vpn forward-filter meta mark set 3000 counter

# DNS handling
nft add chain inet user-vpn forward-filter-dns
nft add rule inet user-vpn forward-filter jump forward-filter-dns
nft add rule inet user-vpn forward-filter iifgroup 2 udp dport 53 counter drop
nft add rule inet user-vpn forward-filter iifgroup 2 tcp dport 53 counter drop

# Postrouting rules
nft 'add chain inet user-vpn postrouting-filter { type filter hook postrouting priority filter - 1; policy accept; }'
nft add rule inet user-vpn postrouting-filter meta mark 3000 oifgroup 1 counter drop

# Output rules
nft 'add chain inet user-vpn output-filter { type filter hook output priority filter - 1; policy drop; }'
nft add rule inet user-vpn output-filter meta oifgroup 1 skgid qvpn counter accept
nft add rule inet user-vpn output-filter meta oifgroup 2 counter accept
nft add rule inet user-vpn output-filter meta oifname \"lo\" counter accept
nft add rule inet user-vpn output-filter counter

# IPv6 blocking (change as needed)
nft add rule inet user-vpn forward-filter meta nfproto ip6 counter drop
nft add rule inet user-vpn output-filter meta nfproto ip6 counter drop

# NAT prerouting
nft 'add chain inet user-vpn prerouting-nat { type nat hook prerouting priority dstnat - 1; policy accept; }'
EOF"
[user@dom0 ~]$ qvm-run -u root sys-vpn "chmod +x /rw/config/vpn/qubes-vpn-firewall.sh"

Redo the connection test after firewall setup.

Step 8: Configure Autostart

[user@dom0 ~]$ qvm-run -u root sys-vpn "cat << 'EOF' | tee /rw/config/rc.local
#!/bin/bash
# Wait for network initialization
sleep 5

# Start WireGuard
/rw/config/vpn/wg-up.sh

# Verify connection
if ! ping -c 1 -W 5 9.9.9.9; then
  su - -c 'notify-send "WireGuard VPN connection failed!" --icon=dialog-error' user
  exit 1
fi

# Final notification
su - -c 'notify-send "WireGuard VPN connected" --icon=network-idle' user
EOF"
[user@dom0 ~]$ qvm-run -u root sys-vpn "chmod +x /rw/config/rc.local"

Step 9: Final Steps

Restart VPN qube:

[user@dom0 ~]$ qvm-shutdown sys-vpn --wait
[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"
[user@sys-vpn ~]# wg show

Set other qubes to use the VPN and verify the connection with client qube:

[user@dom0 ~]$ qvm-prefs work netvm sys-vpn
[user@dom0 ~]$ qvm-run -u root sys-vpn "curl ipleak.net/json/"

References

  1. Qubes OS VPN Documentation
  2. wg(8) — Linux manual page
  3. wg-quick(8) — Linux manual page
  4. Qubes firewall guide
  5. nftables documentation

I believe more advanced users should give feedback on this.

VPN configuration is very important, the Qubes team should give priority to establishing a reliable procedure for this.

The tutorial should focus on privacy during vpn installation:

  • Use tor to communicate with the VPN provider (Mullvad and IVPN), dont leak IP during singup (DNS/IPv6 leaks?).
  • Install the packages using onion repositories, dont leak metadata from downloads ( torsocks in the template ?)
  • Paying with Monero (XMR), prepaid cards or fiat money for the VPN provider, with fake email (ProtonMail via Tor), avoid payment linkability. Use feather-wallet in your own node.
  • Avoid leaking credentials. Handle credentials offline, like key generation and secure transfer.

Others points:

  • IPv6 Support ?

qvm-features sys-vpn ipv6 ‘’

# IPv6 blocking (change as needed)
nft add rule inet user-vpn forward-filter meta nfproto ip6 counter drop
nft add rule inet user-vpn output-filter meta nfproto ip6 counter drop
  • Do I need to add strict DNS rules to avoid leakage in Wireguard config? Like
/rw/config/vpn/wg.conf 
[Interface]
PostUp = resolvectl dns %i 10.200.100.1 11.201.101.2; resolvectl domain %i ~.
PostDown = resolvectl revert %i
  • dnscrypt-proxy in sys-whonix ?
qvm-run -u root sys-vpn 'echo "nameserver 127.0.0.1" > /etc/resolv.conf && chattr +i /etc/resolv.conf'
  • Add scripts in ?

/rw/config/network-hooks.d/

  • Logging ?

nft add rule inet user-vpn forward-filter meta nfproto ipv4 counter log prefix "VPN-traffic: " accept

qvm-run -u root sys-net ‘tcpdump -i eth0 not port 51820’

The Final Solution:

How to Set Up sys-vpn Qube with Wireguard in Debian minimal template without Network Manager

Introduction

This tutorial creates a sys-vpn Qube with Wireguard, killswitch, and DNS leak protection using nftables redirection in Debian minimal without Network Manager.

To follow this tutorial easily

To easily follow this tutorial and copy the commands, you could transfer this tutorial in a txt file to dom0:

[user@dom0 ~]$ qvm-run --pass-io disp6666 'cat ~/Downloads/tutorial.txt' > ~/tutorial.txt
[user@dom0 ~]$ cat ~/tutorial.txt

To make your job easier, you can preserve the terminal history in multiple instances:

# Don't erase history
[user@dom0 ~]$ shopt -s histappend
# Make the history save and reload after each command finishes
[user@dom0 ~]$ export PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"
# No duplicate entries
[user@dom0 ~]$ export HISTCONTROL=ignoredups:erasedups
# Remember the last 1000 commands
[user@dom0 ~]$ export HISTSIZE=1000
# Truncate commands after 2000 lines
[user@dom0 ~]$ export HISTFILESIZE=2000

Step 1: Create and Update the Template

Create and Update the Debian Minimal Template

[user@dom0 ~]$ sudo qubes-dom0-update qubes-template-debian-12-minimal
[user@dom0 ~]$ sudo qubesctl --show-output --skip-dom0 --targets=debian-12-minimal state.sls update.qubes-vm
If necessary onionize Debian repositories
[user@dom0 ~]$ qvm-run -u root debian-12-minimal \
"apt update -y && apt install -y apt-transport-tor"
[user@dom0 ~]$ qvm-run -u root debian-12-minimal \
'cat << "EOF" | tee /etc/apt/sources.list.d/qubes-r4.list
  # Main qubes updates repository
  #deb [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg ] https://deb.qubes-os.org/r4.2/vm bookworm main
  #deb-src [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg ] https://deb.qubes-os.org/r4.2/vm bookworm main
  # Qubes updates candidates repository
  #deb [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg] https://deb.qubes-os.org/r4.2/vm bookworm-testing main
  #deb-src  [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg ]  https://deb.qubes-os.org/r4.2/vm bookworm-testing main
  # Qubes security updates testing repository
  #deb [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg] https://deb.qubes-os.org/r4.2/vm bookworm-securitytesting main
  #deb-src  [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg ] https://deb.qubes-os.org/r4.2/vm bookworm-securitytesting main
  # Qubes experimental/unstable repository
  #deb [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg] https://deb.qubes-os.org/r4.2/vm bookworm-unstable main
  #deb-src  [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg ] https://deb.qubes-os.org/r4.2/vm bookworm-unstable main
  # Qubes Tor updates repositories
  # Main qubes updates repository
  deb [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg] tor+http://deb.qubesosfasa4zl44o4tws22di6kepyzfeqv3tg4e3ztknltfxqrymdad.onion/r4.2/vm bookworm main
  #deb-src  [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg ] tor+http://deb.qubesosfasa4zl44o4tws22di6kepyzfeqv3tg4e3ztknltfxqrymdad.onion/r4.2/vm bookworm main
  # Qubes updates candidates repository
  #deb [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg] tor+http://deb.qubesosfasa4zl44o4tws22di6kepyzfeqv3tg4e3ztknltfxqrymdad.onion/r4.2/vm bookworm-testing main
  #deb-src  [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg ] tor+http://deb.qubesosfasa4zl44o4tws22di6kepyzfeqv3tg4e3ztknltfxqrymdad.onion/r4.2/vm bookworm-testing main
  # Qubes security updates testing repository
  #deb [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg] tor+http://deb.qubesosfasa4zl44o4tws22di6kepyzfeqv3tg4e3ztknltfxqrymdad.onion/r4.2/vm bookworm-securitytesting main
  #deb-src  [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg ] tor+http://deb.qubesosfasa4zl44o4tws22di6kepyzfeqv3tg4e3ztknltfxqrymdad.onion/r4.2/vm bookworm-securitytesting main
  # Qubes experimental/unstable repository
  #deb [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg] tor+http://deb.qubesosfasa4zl44o4tws22di6kepyzfeqv3tg4e3ztknltfxqrymdad.onion/r4.2/vm bookworm-unstable main
  #deb-src  [arch=amd64 signed-by=/usr/share/keyrings/qubes-archive-keyring-4.2.gpg ] tor+http://deb.qubesosfasa4zl44o4tws22di6kepyzfeqv3tg4e3ztknltfxqrymdad.onion/r4.2/vm bookworm-unstable main
  EOF'
[user@dom0 ~]$ qvm-run -u root debian-12-minimal \
"apt update -y && apt upgrade -y"

Step 2: Install Packages in the VPN Template

[user@dom0 ~]$ qvm-create --class TemplateVM --template debian-12-minimal --property netvm=sys-firewall --label orange debian-12-minimal-vpn
[user@dom0 ~]$ qvm-shutdown debian-12-minimal
[user@dom0 ~]$ qvm-run -u root debian-12-minimal-vpn \
"apt update -y && apt install -y --no-install-recommends \
qubes-core-agent-networking \
qubes-core-agent-passwordless-root \
nftables \
openresolv \
wireguard-tools"
[user@dom0 ~]$ qvm-run -u root debian-12-minimal-vpn \
'cat << "EOF" | tee /etc/apt/apt.conf.d/90no-recommends-no-suggests
APT::Install-Recommends "0";
APT::Install-Suggests "0";
EOF'
[user@dom0 ~]$ qvm-shutdown debian-12-minimal-vpn

Step 3: Create sys-vpn Service Qube

[user@dom0 ~]$ qvm-create --class AppVM --property template=debian-12-minimal-vpn \
--label orange --property netvm=sys-firewall --property provides_network=True \
--property autostart=True sys-vpn

Step 4: Prepare WireGuard Configuration

Install packages in sys-vpn again, including dnsutils libnotify curl and libnotify:

[user@dom0 ~]$ qvm-run -u root sys-vpn \
  "apt update -y && apt install -y wireguard-tools nftables openresolv dnsutils curl libnotify"

If running, disable any auto-starting service that comes with the software package:

[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"
[user@sys-vpn ~]# dpkg -L wireguard-tools | grep systemd
[user@sys-vpn ~]# systemctl list-units | grep wg
[user@sys-vpn ~]# systemctl disable wg-quick@.service

Download and extract config files accordingly your VPN provider.

Transfer your WireGuard configuration file (usually ending in .conf) to the sys-vpn qube. You can do this by:

  • From disp6666 (DispVM) to sys-vpn (AppVM) with GUI:
  • Use Qube's file copy feature through the context menu by right-clicking on the file and selecting "Copy to other qube".
Alternatively, you could use other CLI methods
  • From Dom0 (1):
  • [user@disp6666 ~]$ tar -czvf configurations.tar.gz /home/user/configurations
    [user@dom0 ~]$ qvm-run --pass-io disp6666 'cat /home/user/configurations.tar.gz' > ~/configurations.tar.gz
    [user@dom0 ~]$ tar -xzvf /home/user/configurations.tar.gz -C /home/user/
    [user@dom0 ~]$ qvm-copy-to-vm sys-vpn configurations
    [user@dom0 ~]$ qvm-run -u root sys-vpn \
      "mkdir -p /rw/config/vpn && mv /home/user/QubesIncoming/dom0/configurations/* /rw/config/vpn"
  • From Dom0 (2):
  • [user@dom0 ~]$ qvm-run -u root sys-vpn "mkdir -p /rw/config/vpn && cd /rw/config/vpn && curl --https-only -o configurations.zip 'https://myvpn.com/config' && unzip -q /rw/config/vpn/configurations.zip -d /rw/config/vpn && rm /rw/config/vpn/configurations.zip"
  • From whonix-dvm DispVM in CLI mode:
  • You need to modify the RPC policy to allow AppVM to run commands in the sys-vpn VM in CLI mode.

    [user@dom0 ~]$ nano /etc/qubes/policy.d/30-default.policy
    qubes.VMShell * @dispvm:whonix-dvm sys-vpn allow
    [user@dom0 ~]$ sudo systemctl restart qubesd
    [user@disp6666 ~]$ qvm-copy-to-vm sys-vpn /home/user/Downloads/configurations
    [user@disp6666 ~]$ qvm-run-vm sys-vpn "sudo mkdir -p /rw/config/vpn"
    [user@disp6666 ~]$ qvm-run-vm sys-vpn 'sudo mv /QubesIncoming/disp6666/configurations $HOME'
    [user@disp6666 ~]$ qvm-run-vm sys-vpn "sudo unzip -q /rw/config/vpn/configurations.zip -d /rw/config/vpn && sudo rm /rw/config/vpn/configurations.zip"

Step 5: Configure WireGuard

To copy and paste in XTerm you need to edit the settings (~/.Xresource):

Use "Ctrl + Alt + C / V" to copy and paste and avoid conflicts with Qubes Global Clipboard.

Apply the changes with:

[user@dom0 ~]$ qvm-run sys-vpn 'xrdb -merge /home/user/.Xresource'
[user@dom0 ~]$ qvm-run sys-vpn \
'cat << "EOF" | tee /home/user/.Xresource
! Theme for XTerm
! To apply changes, run: xrdb -merge /home/user/.Xresource

! Font settings
XTerm*faceName: DejaVu Sans Mono
XTerm*faceSize: 12
XTerm*renderFont: true
XTerm*boldFont: DejaVu Sans Mono Bold
XTerm*colorBDMode: true
XTerm*colorBD: red

! Scrollbar
XTerm*scrollBar: true
XTerm*rightScrollBar: true
XTerm*scrollTtyOutput: false

! Behavior settings
XTerm*shell: /bin/bash
XTerm*saveLines: 2000
XTerm*locale: false
XTerm*utf8: 1
XTerm*visualBell: true
XTerm*bellIsUrgent: false

! Cursor settings
XTerm*cursorColor: white
XTerm*cursorBlink: true
XTerm*highlightSelection: true
XTerm*selectToClipboard: true

! Green text
XTerm*foreground: #009900
! Pure black background
XTerm*background: #000000
! Black
XTerm*color0: #000000
! Dark green
XTerm*color1: #00AA00
! Bright green
XTerm*color2: #00FF00

! Keybindings for copy-paste
XTerm*VT100.translations: #override \
  Ctrl Alt <Key>C: copy-selection(CLIPBOARD) \n\
  Ctrl Alt <Key>V: insert-selection(CLIPBOARD) \n\
  ~Shift <Btn1Down>: select-start() \n\
  ~Shift <Btn1Motion>: select-extend() \n\
  ~Shift <Btn1Up>: select-end(PRIMARY) \n\
  Shift <Btn1Up>: select-end(CLIPBOARD) \n\
  Ctrl <Key>L: clear-saved-lines() \n\
  Ctrl <Key>C: string(0x03) \n\
  Ctrl <Key>D: string(0x04)

! Debugging: xrdb -query | grep XTerm
EOF'

Verify your current network topology:

[user@dom0 ~]$ qvm-ls -n

Write down your Qubes current network topology:


  # sys-vpn → sys-firewall (10.137.0.18)
  sys-vpn                    Running  sys-firewall  10.137.0.18   -       10.138.32.97
  # sys-firewall → sys-net (10.138.32.97)
  sys-firewall               Running  sys-net       10.138.32.97  -       10.137.0.6
  # sys-net → physical network (10.137.0.6)
  sys-net                    Running  -             10.137.0.6    -       -
  

Write down the VPN tunnel address:

[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"
[user@sys-vpn ~]$ cat /rw/config/vpn/yourconfig.conf
  [Interface]
  Address = 10.14.0.2/16
  (...)

Create the inter-qube networking rules in sys-firewall:

[user@dom0 ~]$ qvm-run -u root sys-firewall \
  "nft add rule ip qubes custom-forward ip saddr 10.137.0.18 ip daddr 10.14.0.2 ct state new,established,related counter accept"
[user@dom0 ~]$ qvm-run -u root sys-firewall \
  "nft add rule ip qubes custom-forward ip saddr 10.14.0.2 ip daddr 10.137.0.18 ct state new,established,related counter accept"
[user@dom0 ~]$ qvm-run -u root sys-firewall \
  "nft add rule ip qubes custom-forward ip saddr 10.137.0.18 udp dport 51820 ct state new,established,related counter accept"

Update your Wireguard configuration file:

[user@dom0 ~]$ qvm-run -u root sys-vpn \
"cat << 'EOF' | tee /rw/config/vpn/yourconfig.conf
[Interface]
Address = 10.14.0.2/16
PrivateKey = [private-key]
DNS = 9.9.9.9, 8.8.8.8
MTU = 1420
# Add explicit route for Qubes internal network
PostUp = ip route add 10.137.0.0/16 via 10.137.0.1 dev eth0
PostUp = sysctl -q net.ipv4.conf.all.src_valid_mark=1
[Peer]
PublicKey = [public-key]
AllowedIPs = 0.0.0.0/0
Endpoint = demo.wireguard.com:51820
EOF"

Start an interactive terminal in sys-vpn and perform a basic connectivity test before set firewall rules:

[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"

# Connection tests
[user@sys-vpn ~]# wg-quick up /rw/config/vpn/yourconfig.conf  # Connect
[user@sys-vpn ~]# ping -c 4 9.9.9.9
[user@sys-vpn ~]# ping -s [MTU] -M do 9.9.9.9
[user@sys-vpn ~]# curl ipleak.net/json/
[user@sys-vpn ~]# curl --interface [interface] ipleak.net/json/
[user@sys-vpn ~]# ip route show table all
[user@sys-vpn ~]# wg show  
[user@sys-vpn ~]# cat /etc/resolv.conf
[user@sys-vpn ~]# resolvconf -l
[user@sys-vpn ~]# nft list ruleset
[user@sys-vpn ~]# wg-quick down /rw/config/vpn/yourconfig.conf  # Disconnect
[user@dom0 ~]$ qvm-run -u root sys-firewall "xterm"

# Connection tests
[user@sys-firewall ~]# nft list ruleset | grep -A5 'custom-forward'
  

Configure persistent rules in sys-firewall:

[user@dom0 ~]$ qvm-run -u root sys-firewall \
"cat << 'EOF' | tee /rw/config/qubes-firewall-user-script.sh
#!/bin/sh
nft add rule ip qubes custom-forward ip saddr 10.137.0.18 ip daddr 10.14.0.2 ct state new,established,related counter accept
nft add rule ip qubes custom-forward ip saddr 10.14.0.2 ip daddr 10.137.0.18 ct state new,established,related counter accept
nft add rule ip qubes custom-forward ip saddr 10.137.0.18 udp dport 51820 ct state new,established,related counter accept
EOF"
[user@dom0 ~]$ qvm-run -u root sys-firewall "chmod +x /rw/config/qubes-firewall-user-script.sh"

Step 6: Create Connection Script

[user@dom0 ~]$ qvm-run -u root sys-vpn \
"cat << 'EOF' | tee /rw/config/vpn/wg-up.sh
#!/bin/bash

# Wait for network to be ready
while ! ping -c1 10.137.0.1 &>/dev/null; do
  sleep 1
done

# Start WireGuard
wg-quick up /rw/config/vpn/yourconfig.conf

# Apply firewall rules
/rw/config/vpn/qubes-vpn-firewall.sh

# Notification
su - -c 'notify-send "WireGuard VPN Connected" --icon=network-idle' user
EOF"
[user@dom0 ~]$ qvm-run -u root sys-vpn "chmod +x /rw/config/vpn/wg-up.sh"

Step 7: Configure nftables Rules

[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"
# Write down eth0 (Qubes default network interface) and wg0 (WireGuard interface)
ip link
# Assign eth0 to group 1 (Upstream, normal Qubes net)
echo 1 > /sys/class/net/eth0/group
# Assign your wg0 to group 2 (VPN tunnel)
echo 2 > /sys/class/net/wg0/group
  
[user@dom0 ~]$ qvm-run -u root sys-vpn \
"cat << 'EOF' | tee /rw/config/vpn/qubes-vpn-firewall.sh
#!/bin/bash

set -e

# Add the qvpn group
groupadd -rf qvpn

# Create tables and chains
nft add table inet user-vpn
nft 'add chain inet user-vpn forward-filter { type filter hook forward priority filter - 1; policy accept; }'

# Block forwarding of connections through upstream network device
nft add rule inet user-vpn forward-filter meta iifgroup 1 counter drop
nft add rule inet user-vpn forward-filter meta oifgroup 1 counter drop
nft add rule inet user-vpn forward-filter meta mark set 3000 counter

# DNS handling
nft add chain inet user-vpn forward-filter-dns
nft add rule inet user-vpn forward-filter jump forward-filter-dns
nft add rule inet user-vpn forward-filter iifgroup 2 udp dport 53 counter drop
nft add rule inet user-vpn forward-filter iifgroup 2 tcp dport 53 counter drop

# Postrouting rules
nft 'add chain inet user-vpn postrouting-filter { type filter hook postrouting priority filter - 1; policy accept; }'
nft add rule inet user-vpn postrouting-filter meta mark 3000 oifgroup 1 counter drop

# Output rules
nft 'add chain inet user-vpn output-filter { type filter hook output priority filter - 1; policy drop; }'
nft add rule inet user-vpn output-filter meta oifgroup 1 skgid qvpn counter accept
nft add rule inet user-vpn output-filter meta oifgroup 2 counter accept
nft add rule inet user-vpn output-filter meta oifname \"lo\" counter accept
nft add rule inet user-vpn output-filter counter

# IPv6 blocking (change as needed)
nft add rule inet user-vpn forward-filter meta nfproto ip6 counter drop
nft add rule inet user-vpn output-filter meta nfproto ip6 counter drop

# NAT prerouting
nft 'add chain inet user-vpn prerouting-nat { type nat hook prerouting priority dstnat - 1; policy accept; }'
EOF"
[user@dom0 ~]$ qvm-run -u root sys-vpn "chmod +x /rw/config/vpn/qubes-vpn-firewall.sh"

Redo the connection test after firewall setup.

Step 8: Configure Autostart

[user@dom0 ~]$ qvm-run -u root sys-vpn \
"cat << 'EOF' | tee /rw/config/rc.local
#!/bin/bash
# Wait for network initialization
sleep 5

# Start WireGuard
/rw/config/vpn/wg-up.sh

# Verify connection
if ! ping -c 1 -W 5 9.9.9.9; then
  su - -c 'notify-send "WireGuard VPN connection failed!" --icon=dialog-error' user
  exit 1
fi

# Final notification
su - -c 'notify-send "WireGuard VPN connected" --icon=network-idle' user
EOF"
[user@dom0 ~]$ qvm-run -u root sys-vpn "chmod +x /rw/config/rc.local"

Step 9: Final Steps

Restart VPN qube:

[user@dom0 ~]$ qvm-shutdown sys-vpn --wait
[user@dom0 ~]$ qvm-run -u root sys-vpn "xterm"
[user@sys-vpn ~]# curl ipleak.net/json/
[user@sys-vpn ~]# wg show

Configure Client VMs:

[user@dom0 ~]$ qvm-prefs work netvm sys-vpn
[user@dom0 ~]$ qvm-run -u root work "xterm"
[user@work ~]$ curl ipleak.net/json/

References

  1. Qubes OS VPN Documentation
  2. Qubes OS Firewall Documentation
  3. wg(8) — Linux manual page
  4. wg-quick(8) — Linux manual page
  5. nftables documentation
1 Like

Does anybody know if GitHub - tasket/Qubes-vpn-support: VPN configuration in Qubes OS is abandoned ?

Have been relying on this for years and work great with the pull requests but they are still not merged.