Skip to content

VM WireGuard + ProtonVPN (Gateway “VPN-only”)

Questo documento descrive come creare una VM gateway (Ubuntu/Debian) che:

  • si connette a ProtonVPN via WireGuard
  • fa da gateway per altre VM/LXC (es. aria2, jdownloader, ecc.)
  • applica un kill-switch: se WireGuard/VPN cade, nessun traffico esce verso Internet (niente leak)

Riferimenti ufficiali


0) Variabili (adatta ai tuoi IP)

Esempio basato su una LAN 192.168.110.0/24:

  • VPN_GW_VM_IP: 192.168.110.50
  • LAN_GW (router/pfSense): 192.168.110.1
  • LAN_IF (interfaccia LAN sulla VM): ens18 (può essere ens19, enp1s0, ecc.)
  • WG_IF (interfaccia WireGuard): wg0
  • DOWNLOADER_HOSTS: client che useranno questo gateway (es. 192.168.110.32, 192.168.110.33, …)

Approccio più semplice: sui client downloader imposti gateway = 192.168.110.50.


1) Proxmox: crea la VM

  1. Crea una VM Ubuntu Server 22.04/24.04 o Debian 12.
  2. 1 NIC su vmbr0 (o il bridge della VLAN corretta).
  3. Imposta IP statico sulla VM (es. 192.168.110.50/24) e gateway 192.168.110.1.

2) VM: installa WireGuard e tool

Ubuntu/Debian

Bash
apt update
apt install -y wireguard nftables curl ca-certificates

3) ProtonVPN: genera e scarica la config WireGuard

  1. Vai su account ProtonVPN → Downloads → WireGuard configuration
  2. Genera una config (scegli server/paese, ecc.)
  3. Scarica il file .conf (contiene PrivateKey, Address, DNS, Endpoint, PublicKey, AllowedIPs, …)

Guida Proton: https://protonvpn.com/support/wireguard-configurations

⚠️ Se stai rigenerando una config per sostituire una esistente (es. dopo il problema descritto in §10.3), il file scaricato da Proton è pensato per un client "normale" e non contiene Table = 51820 né i blocchi PostUp/PostDown dello split-routing. Devi riportarli a mano dal file precedente, altrimenti il forwarding per i client downloader smette di funzionare anche con l'handshake riuscito. Vedi §10.3 per il file completo di esempio.

Consigliato: aggiungi anche PersistentKeepalive = 25 nel blocco [Peer] — mantiene aperto il mapping NAT del tuo router/ISP anche con traffico intermittente, riducendo il rischio che il tunnel smetta di ricevere risposte dopo periodi di inattività.


4) Importa la config e avvia wg-quick

  1. Copia la config dentro la VM, poi:
Bash
mkdir -p /etc/wireguard
chmod 700 /etc/wireguard
cp /root/proton.conf /etc/wireguard/wg0.conf
chmod 600 /etc/wireguard/wg0.conf
  1. Avvio manuale di test:
Bash
wg-quick up wg0
wg show
  1. Abilita all’avvio:
Bash
systemctl enable --now wg-quick@wg0

Riferimento wg-quick: https://man7.org/linux/man-pages/man8/wg-quick.8.html


5) Abilita IP forwarding (la VM deve fare routing)

Bash
cat >/etc/sysctl.d/99-vpn-gw.conf <<'EOF'
net.ipv4.ip_forward=1
EOF

sysctl -p /etc/sysctl.d/99-vpn-gw.conf

6) nftables: NAT + Kill-Switch (VPN-only)

Obiettivo:

  • accetto forward LAN -> wg0
  • blocco LAN -> Internet via LAN_IF (bypass)
  • NAT (masquerade) in uscita su wg0

6.1) Crea /etc/nftables.conf

Sostituisci ens18 con la tua interfaccia LAN reale se diversa.

Bash
cat >/etc/nftables.conf <<'NFT'
flush ruleset

define LAN_IF = "ens18"
define WG_IF  = "wg0"
define LAN_NET = 192.168.110.0/24

table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;

    ct state established,related accept
    iif "lo" accept

    # Consenti SSH dalla LAN (opzionale ma consigliato)
    iifname $LAN_IF ip saddr $LAN_NET tcp dport 22 accept

    # ICMP dalla LAN (ping)
    iifname $LAN_IF ip protocol icmp accept

    # (Opzionale) accetta traffico in ingresso dalla VPN
    iifname $WG_IF accept
  }

  chain forward {
    type filter hook forward priority 0; policy drop;

    ct state established,related accept

    # LAN -> VPN OK
    iifname $LAN_IF oifname $WG_IF accept

    # Kill-switch: vieta LAN -> WAN (bypass) se non passa da wg0
    iifname $LAN_IF oifname $LAN_IF drop
  }

  chain output {
    type filter hook output priority 0; policy accept;
  }
}

table ip nat {
  chain postrouting {
    type nat hook postrouting priority 100; policy accept;

    # NAT verso VPN
    oifname $WG_IF ip saddr $LAN_NET masquerade
  }
}
NFT

6.2) Abilita nftables

Bash
systemctl enable --now nftables
nft -f /etc/nftables.conf
nft list ruleset | head -n 60

Doc NAT/masquerade: https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/security_guide/sec-configuring_nat_using_nftables


7) Client (VM/LXC downloader): passare dal gateway VPN

Opzione A (semplice, robusta): imposta gateway = VPN_GW_VM_IP

Sul client downloader:

  • IP statico (es. 192.168.110.32/24)
  • Gateway: 192.168.110.50
  • DNS: come preferisci (vedi note DNS)

Test sul client:

Bash
ip route
curl -4 -m 10 https://api.ipify.org

Opzione B (avanzata): policy routing “solo per certi IP”

Se vuoi lasciare il gateway dei client su pfSense ma forzare “alcuni IP” via VPN:

  • serve policy routing (mark + routing table) sulla VM.

(Implementazione da fare su richiesta perché dipende da sub-reti e obiettivi.)


8) Verifiche “a prova di paranoia”

8.1) Verifica che la VM stia usando la VPN

Sulla VM gateway:

Bash
curl -4 -m 10 https://api.ipify.org
wg show
ip route

⚠️ Attenzione: questo curl eseguito dalla VM stessa passa dalla tabella di routing principale (default via LAN_GW dev LAN_IF), non dalla VPN — lo split-routing di questo playbook instrada via wg0 solo il traffico forwardato dai client (quello che entra da LAN_IF), non il traffico generato localmente dalla VM. Un curl riuscito qui conferma solo che la VM ha accesso diretto a Internet, non che il tunnel funzioni. Per testare davvero il tunnel, usa wg show (bytes received) o il curl dal client in §7.

8.2) Verifica kill-switch

  1. Spegni WireGuard sulla VM:
Bash
wg-quick down wg0
  1. Sul client downloader prova:
Bash
curl -4 -m 5 https://api.ipify.org

Atteso: timeout / fail (niente traffico → niente leak)

  1. Riattiva:
Bash
wg-quick up wg0

9) Note DNS (anti “DNS leak”)

Se i client usano DNS “di LAN” (es. AdGuard/pfSense), le query DNS restano in LAN. Se vuoi che anche il DNS segua la VPN:

  • imposta sui client un DNS raggiungibile via VPN (o usa quello indicato nella config Proton)
  • oppure fai “DNS proxy” sulla VM gateway (unbound/dnsmasq) e fai puntare i client a quello.

10) Troubleshooting rapido

10.1) WireGuard

Bash
systemctl status wg-quick@wg0 --no-pager
wg show
journalctl -u wg-quick@wg0 -n 200 --no-pager

10.2) Routing / NAT

Bash
sysctl net.ipv4.ip_forward
nft list ruleset
ip -4 route

Verifica path “da client”:

Bash
ip -4 route get 1.1.1.1
curl -4 -m 10 https://api.ipify.org

10.3) Sintomo: il tunnel invia ma non riceve mai nulla (0 B received)

Come si presenta:

  • wg show mostra transfer: 0 B received, N KiB sent — i bytes sent crescono ma received resta sempre a zero, e non compare mai la riga latest handshake.
  • Nella chain forward di nftables, il contatore della regola LAN→wg0 (uscita) cresce normalmente, ma quello della regola wg0→LAN (ritorno) resta fermo a 0 bytes 0 packets.
  • wg-quick down wg0 && wg-quick up wg0 non cambia nulla: torna subito a 0 B received.
  • Il client dietro il gateway (LXC/VM downloader) riesce a pingare il gateway ma va in timeout su qualsiasi traffico verso Internet.

Come isolare la causa (in ordine, il più veloce per primo):

  1. Escludi il clock: date -u e timedatectl status — se System clock synchronized: no, risolvi prima l'NTP (un orologio sfasato fa scartare silenziosamente l'handshake per via del timestamp anti-replay di WireGuard).
  2. Escludi il routing/rete generale: ping -c 5 <IP_ENDPOINT_SERVER> dalla VM. Se risponde con latenza normale, la rete verso quell'host funziona a livello IP — il problema è isolato all'handshake WireGuard stesso, non a router/ISP.
  3. Conferma con tcpdump: mentre generi traffico dal client, tcpdump -ni <LAN_IF> udp port 51820 sulla VM. Se vedi solo pacchetti in uscita verso l'endpoint (i retry di handshake, ogni ~5s) e mai una risposta, il server non sta rispondendo a questa chiave — non è un problema locale di nftables/routing (tcpdump cattura prima del filtro).
  4. Prova un endpoint/chiave diversi: genera una nuova config WireGuard dal pannello ProtonVPN (anche per un server diverso) e sostituiscila. Se con la nuova chiave l'handshake si completa subito, la causa era la chiave/peer revocato o scaduto lato Proton sul vecchio server — capita ad esempio dopo rotazioni interne o inattività prolungata, e il server droppa l'handshake senza inviare alcun errore esplicito, da qui il "silenzio totale" invece di un rifiuto visibile.

Fix: sostituisci la config come descritto nella nota in §3, ricordando di riportare a mano Table = 51820 e i blocchi PostUp/PostDown (il file scaricato da Proton non li include), e aggiungi PersistentKeepalive = 25 per prevenire ricadute dovute a scadenza del mapping NAT.


11) Hardening minimo consigliato

  • SSH solo da LAN (già in nftables)
  • niente servizi extra sulla VM
  • aggiornamenti:
Bash
apt update && apt upgrade -y

Appendix: perché wg-quick e non “app Proton”

wg-quick è perfetto quando vuoi una VM “router/gateway” senza UI, gestibile via systemd. WireGuard quickstart: https://www.wireguard.com/quickstart/