BETA RELEASE
Waiting for the Linux Gurus review. Looks good but not fully tested.
Features
- Killswitch-like protection when VPN is down
- VPN-only connectivity for all traffic
- DNS leak protection via namespace isolation
- AppVM DNS NAT compatibility
- IPv6 disabled
Usage
- Transfer the script from appvm to dom0 with:
[user@dom0 ~]$ qvm-run -p appvm 'cat ~/wireguard-namespace-qube.sh' > ~/wireguard-namespace-qube.sh - Place your WireGuard config to the desired folder:
[user@dom0 ~]$ mkdir -p wireguard
[user@dom0 ~]$ qvm-run -p appvm 'cat ~/wg0.conf' > ~/wireguard/wg0.conf - Make the script executable with:
[user@dom0 ~]$ chmod +x ~/wireguard-namespace-qube.sh - Run the script with:
[user@dom0 ~]$ bash ~/wireguard-namespace-qube.sh - Usage diagram:
sys-net ↔ sys-firewall ↔ sys-vpn ↔ appvms - Set the VPN target:
[user@dom0 ~]$ qvm-prefs appvm netvm sys-vpn - Start the VPN service:
[user@dom0 ~]$ qvm-start sys-vpn - Start the VPN target, e. g.:
[user@dom0 ~]$ qvm-run -q -a --service -- work qubes.StartApp+org.mozilla.firefox
NOTE: To create other services with different WireGuard configs, update the WG_CONFIG_NAME and VPN_SERVICE variables at the beginning of the script and rerun it.
wireguard-namespace-qube.sh
#!/bin/bash
################################################################################
# File Name : wireguard-namespace-qube.sh
# Description : WireGuard VPN setup for Qubes OS that routes all internet
# traffic through the wg0 interface using network namespace
# isolation to prevent IP and DNS leaks. Below set the desired
# values for your variables, note that you can rerun the
# script to create other VPN services.
# Usage : • Transfer this script from appvm to dom0 with:
# [user@dom0 ~]$ qvm-run -p appvm 'cat ~/wireguard-namespace-qube.sh' > ~/wireguard-namespace-qube.sh
# • Place your WireGuard config to the desired folder:
# [user@dom0 ~]$ mkdir -p wireguard
# [user@dom0 ~]$ qvm-run -p appvm 'cat ~/wg0.conf' > ~/wireguard/wg0.conf
# • Make the script executable with:
# [user@dom0 ~]$ chmod +x ~/wireguard-namespace-qube.sh
# • Run the script with:
# [user@dom0 ~]$ bash ~/wireguard-namespace-qube.sh
# Author : Me and the bois
# License : Free of charge, no warranty
# Last edited : 2025-10-28
################################################################################
# Safety first
set -euo pipefail
# Configuration (set default values here)
WG_CONFIG_NAME="wg0.conf"
VPN_SERVICE="sys-vpn"
WG_CONFIG_SOURCE="/home/user/wireguard"
BASE_TEMPLATE="debian-12-minimal"
CUSTOM_TEMPLATE="debian-vpn-template"
# Step 1: Verify and create base template
create_custom_template() {
echo -e "\nStep 1: Verifying and creating: $BASE_TEMPLATE..."
if ! qvm-check "$BASE_TEMPLATE"; then
echo -e "\nInstalling $BASE_TEMPLATE..."
sudo qubes-dom0-update "qubes-template-$BASE_TEMPLATE"
fi
qvm-shutdown --wait "$BASE_TEMPLATE" 2>/dev/null || true
if qvm-check "$BASE_TEMPLATE"; then
echo -e "\nUpdating $BASE_TEMPLATE..."
sudo qubesctl --show-output --skip-dom0 --targets="$BASE_TEMPLATE" state.sls update.qubes-vm
fi
echo -e "\nSetup complete"
}
# Step 2: Verify and create custom template with packages (change as needed)
install_packages() {
echo -e "\nStep 2: Creating and installing required packages in $CUSTOM_TEMPLATE..."
if qvm-check "$CUSTOM_TEMPLATE"; then
echo -e "\n$CUSTOM_TEMPLATE already exists"
echo -e "\nContinuing to $VPN_SERVICE creation..."
else
echo -e "\nCreating $CUSTOM_TEMPLATE by cloning from $BASE_TEMPLATE..."
qvm-clone "$BASE_TEMPLATE" "$CUSTOM_TEMPLATE"
qvm-prefs "$CUSTOM_TEMPLATE" label orange
echo -e "\nStarting $CUSTOM_TEMPLATE for package installation..."
qvm-start "$CUSTOM_TEMPLATE"
echo -e "\nConfiguring template..."
qvm-run -p -u root "$CUSTOM_TEMPLATE" "echo 'TERM=xterm' >> /etc/environment"
qvm-run -p -u root "$CUSTOM_TEMPLATE" "sed -i 's/^# *\(en_US.UTF-8\)/\1/' /etc/locale.gen"
qvm-run -p -u root "$CUSTOM_TEMPLATE" "locale-gen en_US.UTF-8"
echo -e "\nInstalling packages in $CUSTOM_TEMPLATE..."
qvm-run -p -u root "$CUSTOM_TEMPLATE" "
apt install -y --no-install-recommends \
qubes-core-agent-passwordless-root \
qubes-core-agent-networking \
wireguard-tools \
nftables \
iproute2 \
dnsutils \
less \
psmisc \
xfce4-terminal"
echo -e "\n$CUSTOM_TEMPLATE created successfully"
fi
qvm-shutdown --wait "$CUSTOM_TEMPLATE" 2>/dev/null || true
}
# Step 3: Create qube service
create_vpn_qube() {
echo -e "\nStep 3: Verifying and creating qube service: $VPN_SERVICE..."
if qvm-check "$VPN_SERVICE"; then
echo -e "\n$VPN_SERVICE already exists"
echo -e "\nExiting"
exit 1
else
echo -e "\nCreating $VPN_SERVICE..."
qvm-create \
--class AppVM \
--template "$CUSTOM_TEMPLATE" \
--label orange \
--property netvm=sys-firewall \
--property provides_network=True \
--property vcpus=1 \
--property memory=300 \
--property maxmem=400 \
--property autostart=False \
--property include_in_backups=False \
"$VPN_SERVICE"
echo -e "\nDisabling IPv6 in $VPN_SERVICE"
qvm-features "$VPN_SERVICE" ipv6 ''
echo -e "\nEnabling qubes-firewall service in $VPN_SERVICE"
qvm-service "$VPN_SERVICE" qubes-firewall on
echo -e "\nStarting $VPN_SERVICE to apply configuration..."
qvm-start "$VPN_SERVICE"
echo -e "\n$VPN_SERVICE created and configured"
fi
}
# Step 4: Comment out to configure via ip commands instead of wg-quick
comment_wg_config() {
echo -e "\nStep 4: Commenting out Address, DNS, and MTU in WireGuard config..."
local temp_config=$(mktemp)
cp "$WG_CONFIG_SOURCE/$WG_CONFIG_NAME" "$temp_config"
# Comment out the lines
sed -i 's/^Address/#Address/g' "$temp_config"
sed -i 's/^DNS/#DNS/g' "$temp_config"
sed -i 's/^MTU/#MTU/g' "$temp_config"
# Overwrite the original
mv "$temp_config" "$WG_CONFIG_SOURCE/$WG_CONFIG_NAME"
echo "WireGuard config automatically commented to configure via ip commands"
}
# Step 5: Copy the configuration file from dom0 to the VPN service qube
transfer_config() {
echo -e "\nStep 5: Transferring WireGuard configuration to the VPN service qube..."
if [[ ! -f "$WG_CONFIG_SOURCE/$WG_CONFIG_NAME" ]]; then
echo -e "\nError: Config file not found at $WG_CONFIG_SOURCE/$WG_CONFIG_NAME"
return 1
fi
echo -e "\nCopying..."
qvm-copy-to-vm "$VPN_SERVICE" "$WG_CONFIG_SOURCE/$WG_CONFIG_NAME"
sleep 2
qvm-run -p -u root "$VPN_SERVICE" "mkdir -p /rw/config"
qvm-run -p -u root "$VPN_SERVICE" "mkdir -p /rw/config/vpn"
qvm-run -p -u root "$VPN_SERVICE" "mv '/home/user/QubesIncoming/dom0/$WG_CONFIG_NAME' '/rw/config/vpn/'"
qvm-run -p -u root "$VPN_SERVICE" "chmod 600 '/rw/config/vpn/$WG_CONFIG_NAME'"
echo -e "\nConfig file moved to: /rw/config/vpn/$WG_CONFIG_NAME"
}
# Create WireGuard namespace setup script
create_wireguard_setup_script() {
echo -e "\nCreating WireGuard namespace setup script..."
local temp_script=$(mktemp)
cat > "$temp_script" </dev/null | grep -E "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\$" | head -1)
if [ -n "$IP" ] && [ "$IP" != "127.0.0.1" ]; then
echo -e "\nResolved $ENDPOINT to $IP using DNS $dns_server"
RESOLVED_IP="$IP"
break
fi
done
fi
if [ -z "$RESOLVED_IP" ]; then
echo -e "\nError: Could not resolve endpoint $ENDPOINT"
exit 1
fi
echo -e "\nSetting up network namespace..."
# Clean up any existing namespace
ip netns delete physical 2>/dev/null || true
# Create namespace and move interface
ip netns add physical
ip link set "$INTERFACE" netns physical
ip -n physical link add wg0 type wireguard
ip -n physical link set wg0 netns 1
# Reconfigure physical interface in namespace
ip -n physical addr add "$PHYSICAL_IP" dev "$INTERFACE"
ip -n physical link set "$INTERFACE" up
ip -n physical route add default via "$PHYSICAL_ROUTE" dev "$INTERFACE" onlink
# Create temporary config with resolved IP
cp "$WG_CONFIG" /tmp/wg-config-temp.conf
sed -i "s/$ENDPOINT:[0-9]*/$RESOLVED_IP:$PORT/g" /tmp/wg-config-temp.conf
# Configure WireGuard (remove conflicting lines)
sed -i "/^Address/d;/^DNS/d;/^MTU/d" /tmp/wg-config-temp.conf
wg setconf wg0 /tmp/wg-config-temp.conf
rm -f /tmp/wg-config-temp.conf
ip addr add "$WG_IP" dev wg0
ip link set wg0 up
# Set up routing - all traffic through WireGuard
ip route del default 2>/dev/null || true
ip route add default dev wg0
echo -e "\nWireGuard namespace setup complete - all traffic routed through VPN"
EOF
qvm-copy-to-vm "$VPN_SERVICE" "$temp_script"
qvm-run -p -u root "$VPN_SERVICE" "mv '/home/user/QubesIncoming/dom0/$(basename $temp_script)' '/rw/config/setup-wireguard-namespace.sh'"
qvm-run -p -u root "$VPN_SERVICE" "chown root:root '/rw/config/setup-wireguard-namespace.sh'"
qvm-run -p -u root "$VPN_SERVICE" "chmod 755 '/rw/config/setup-wireguard-namespace.sh'"
rm -f "$temp_script"
echo "WireGuard namespace setup script created successfully"
}
# Create NFTables generator script that uses exported variables
create_nftables_generator_script() {
echo -e "\nCreating NFTables generator script..."
local temp_script=$(mktemp)
cat > "$temp_script" < /rw/config/nftables.conf < "$temp_script" < "$temp_script" </dev/null || true"
qvm-run -p -u root "$VPN_SERVICE" "rmdir '/home/user/QubesIncoming' 2>/dev/null || true"
# Generate NFTables config (using export to make the variable available to the script)
echo -e "\nGenerating NFTables DNS configuration..."
qvm-run -p -u root "$VPN_SERVICE" "export WG_CONFIG_NAME='$WG_CONFIG_NAME'; /rw/config/nftables-generator.sh"
# Apply NFTables config
echo -e "\nApplying DNS redirection rules..."
qvm-run -p -u root "$VPN_SERVICE" "nft -f /rw/config/nftables.conf"
# Verify the rules were applied
echo -e "\nVerifying NFTables rules..."
qvm-run -p -u root "$VPN_SERVICE" "nft list chain ip nat vpn-dns-redirect"
# Setup WireGuard (using export to make the variable available to the script)
echo -e "\nSetting up WireGuard namespace..."
qvm-run -p -u root "$VPN_SERVICE" "export WG_CONFIG_NAME='$WG_CONFIG_NAME'; /rw/config/setup-wireguard-namespace.sh"
# Verify WireGuard is running
echo -e "\nVerifying WireGuard connection..."
qvm-run -p -u root "$VPN_SERVICE" "wg show"
# Configure menu items
qvm-features "$CUSTOM_TEMPLATE" menu-items "xfce4-terminal.desktop"
qvm-features "$VPN_SERVICE" menu-items "xfce4-terminal.desktop"
# Shutdown
qvm-shutdown --wait "$VPN_SERVICE" 2>/dev/null || true
}
# Main function
main() {
echo -e "\nStarting WireGuard VPN setup..."
create_custom_template
install_packages
create_vpn_qube
comment_wg_config
transfer_config
create_wireguard_setup_script
create_nftables_generator_script
create_main_startup_script
create_rclocal_entry
finalize
echo -e "\nSetup completed!"
}
# Run
main "$@"
⚠️ Because of rendering issues of html and markdown in complex scrips, here is ony the NFTables function:
create_nftables_generator_script
# Create NFTables generator script that uses exported variables
create_nftables_generator_script() {
echo -e "\nCreating NFTables generator script..."
local temp_script=$(mktemp)
cat > "$temp_script" < /rw/config/nftables.conf << NFTEOF
#!/usr/sbin/nft -f
# Set variables
define virtualif = $VIRTUALIF
define vpndns1 = $VPNDNS1
# Flush existing rules in qubes dnat-dns chain
flush chain qubes dnat-dns
# Table for NAT and vpn-dns-redirect chain
table ip nat {
chain vpn-dns-redirect {
type nat hook prerouting priority -100; policy accept;
ip daddr 10.139.1.1 udp dport 53 dnat to \$vpndns1
ip daddr 10.139.1.1 tcp dport 53 dnat to \$vpndns1
}
}
# Mangle table (for MSS clamping)
table ip mangle {
chain forward {
type filter hook forward priority 0; policy accept;
tcp flags syn tcp option maxseg size set rt mtu;
}
}
NFTEOF
echo "NFTables configuration generated successfully at /rw/config/nftables.conf"
EOF
qvm-copy-to-vm "$VPN_SERVICE" "$temp_script"
qvm-run -p -u root "$VPN_SERVICE" "mv '/home/user/QubesIncoming/dom0/$(basename $temp_script)' '/rw/config/nftables-generator.sh'"
qvm-run -p -u root "$VPN_SERVICE" "chown root:root '/rw/config/nftables-generator.sh'"
qvm-run -p -u root "$VPN_SERVICE" "chmod 755 '/rw/config/nftables-generator.sh'"
rm -f "$temp_script"
echo "NFTables generator script created successfully"
}
⚠️And here is the complete script:
wireguard-namespace-qube.sh.log (16.1 KB)
References
Core References
- forum.qubes-os.org - WireGuard VPN w/ namespace killswitch
- wireguard.com - Network namespaces (netns)
- Qubes OS - Data leaks - The Role of the Firewall - Documentation
- Qubes OS - Firewall - Documentation
- Qubes OS - Networking - Documentation
- qvm-firewall – Manage VM outbound firewall
- Qubes OS - Config files - Documentation
- Qubes OS - How to make any file persistent (bind-dirs) - Documentation
- Qubes OS - VPN - "Documentation"
- Configuring a ProxyVM VPN Gateway - Community Guides - Qubes OS Forum (post #58)
- Wireguard VPN setup - Solene - Community Guides - Qubes OS Forum
- VPN instructions for 4.2 - Solene - Community Guides - Qubes OS Forum
- sys-wireguard - Salt Formulas for Qubes OS - ben-grande (GitHub)
- Qubes VPN Support - Tasket (GitHub)
- Qubes Mirage Firewall - Mirage (GitHub)
- Qubes OS Forum - Qubes cli firewall
- MMS Clamp and MTU - Wireguard VPN setup - Solene - Community Guides - Qubes OS Forum (post #239)
- VPN instructions for 4.2 - oijawyuh - User support - Qubes OS Forum
- Tutorial 4.2 and 4.1 - Mullvad Wireguard with Qubes - Pawelek85 - Community Guides - Qubes OS Forum
DNS References
- How do you manage DNS topic? - Qubes OS Forum
- DNS in NAT and PR-QBS chain - Qubes OS Forum (post #12)
- Wireguard VPN setup - Qubes OS Forum (post #220)
- DNS nftables - how-to setup a sys-dns qube - Qubes OS Forum (post #57)
- PR-QBS replaced by dnat-dns - how-to setup a sys-dns qube - Qubes OS Forum (post #65)
- Race condition - how-to setup a sys-dns qube - DNS nftables leakage - Qubes OS Forum (post #153)
- Race condition - Wi-Fi hotspot from Qubes OS - Qubes OS Forum (post #39)
- Race condition - VPN instructions for 4.2 - Qubes OS Forum (post #45)
- Custom DNS for AppVM - Qubes OS Forum (post #2)
- VPN NetVM: No Internet in Downstream Qubes (Amnezia VPN, WireGuard-based) - Qubes OS Forum
DNSCrypt References
- how-to setup a sys-dns qube - Qubes OS Forum
- Help with dnscrypt-proxy - User Support - Qubes OS Forum
- DNSCrypt - Arch Wiki
- DNSCrypt (GitHub)
- DNSCrypt packaged - General Discussion - Qubes OS Forum
Wireguard
NFTables
- HOWTO Documentation Page - Nftables Wiki
- KernelNewbies - Nftables examples
- Nftables - Arch Wiki
- nft(8) manpage - Options (Debian Trixie)
- Scripting - Nftables Wiki
- How to use defined variables in nftables through terminal - Server Fault
- Hooks - Nftables Wiki
Other Qubes Network References
- Qubes OS Forum - VLESS obfuscation VPN
- Qubes OS Forum - Tor over VPN
- Qubes OS Forum - Curl-proxy / wget-proxy scripts in Templates so users can add GPG distro keys linked to added external repositories
Other Generic Network References
- Whonix Wiki - Whonix versus VPNs
- Whonix Wiki - Tunnels
- NLnet Labs - Unbound - Resolver for Home Networks
- Unbound - Pi-hole as All-Around DNS Solution
- Mullvad - DNS over HTTPS and DNS over TLS
- Configuring and managing networking - Red Hat Enterprise Linux 10
- openresolv - Configuration - Roy Marples
- openresolv - RESOLVCONF(8) - System Manager's Manual
- openresolv - RESOLVCONF.CONF(5) - File Formats Manual
- systemd - systemd-resolved.service and VPNs
- systemd - Network Configuration Synchronization Points