SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor

WireGuard: come funziona il cryptokey routing

Scopri perché AllowedIPs è insieme tabella di routing e ACL, come funziona l’handshake Noise e come leggere ogni file wg0.conf senza confusione.

Come funziona WireGuard, in un'unica idea

WireGuard associa ogni pacchetto a una chiave pubblica. Questo meccanismo si chiama instradamento tramite chiavi crittografiche, cryptokey routing, e costituisce l'intero progetto: la riga AllowedIPs accanto a un peer è la tabella di routing per i pacchetti in uscita dalla macchina e l'elenco di controllo degli accessi per i pacchetti ricevuti da quel peer. Una sola impostazione, due funzioni. Se si interpreta AllowedIPs in questo modo, ogni file di configurazione di WireGuard diventa leggibile.

Non esiste una tabella delle sessioni indicizzata per indirizzo IP e non esiste un database degli utenti. Un peer è una chiave pubblica associata all'insieme degli indirizzi che quella chiave può usare. L'handshake e i timer servono a mantenere valida questa associazione mentre cambia la rete sottostante. Se si vuole creare prima il tunnel funzionante e affrontare poi la teoria, lo si può configurare seguendo una VPN WireGuard self-hosted sul proprio VPS, quindi tornare qui quando una riga di configurazione sembra non avere senso.

AllowedIPs è una tabella di routing e un elenco di controllo degli accessi

Consideriamo prima la direzione in uscita. Il kernel instrada il pacchetto verso il dispositivo wg0 nel modo consueto, tramite la tabella di routing principale. WireGuard confronta quindi l'indirizzo di destinazione del pacchetto con un'unica tabella che contiene i prefissi consentiti di tutti i peer, applicando il longest prefix match. La corrispondenza identifica un peer, che a sua volta identifica una chiave pubblica, una chiave di sessione e un endpoint UDP. Il pacchetto viene cifrato per quel peer e inviato all'endpoint.

Se il AllowedIPs di nessun peer include la destinazione, il pacchetto non viene inviato, perché non esiste alcuna chiave con cui cifrarlo.

ping: sendmsg: Required key not available

Questo errore ha una sola causa: l'indirizzo che si è tentato di raggiungere non è elencato tra quelli consentiti da alcun peer. Un errore diverso, ping: sendmsg: Destination address required, indica che un peer è stato trovato, ma WireGuard non dispone di un endpoint per quel peer, perché non ne è stato configurato alcuno e non ne è stato ancora appreso nessuno.

Passiamo ora alla direzione in ingresso. Un pacchetto UDP arriva sulla porta di ascolto. WireGuard individua la sessione tramite l'indice del destinatario presente nell'header, verifica il contatore rispetto a una finestra mobile anti-replay, quindi decifra e autentica il payload. Solo dopo legge il pacchetto interno; l'indirizzo di origine di quel pacchetto deve rientrare nel AllowedIPs del peer che lo ha inviato. In caso contrario, il pacchetto viene scartato. Con il debug dinamico abilitato, il kernel stampa il motivo in una riga come questa:

wg0: Packet has unallowed src IP (10.8.0.9) from peer 2 (203.0.113.10:51820)

Per questo un peer sul lato server riceve un /32. Un peer configurato con AllowedIPs = 10.8.0.2/32 può inviare pacchetti da 10.8.0.2 e da nessun altro indirizzo. Scrivendo invece 0.0.0.0/0 in quel campo, si consente a quel singolo client di iniettare pacchetti dichiarando qualsiasi indirizzo di origine all'interno del tunnel, incluso quello di un altro client.

I prefissi sovrapposti vengono risolti in base alla specificità, perché la ricerca usa il longest prefix match. I prefissi identici configurati su due peer si comportano in modo diverso: la voce viene spostata al peer configurato per ultimo e il primo peer smette di ricevere quel traffico senza che venga stampato alcun errore. wg show wg0 allowed-ips stampa la tabella effettivamente presente nel kernel, che è quella rilevante quando il file su disco e lo stato in esecuzione non sono più allineati.

Lettura di un file di configurazione considerando il routing tramite chiavi crittografiche

Il lato server:

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <server private key>

[Peer]
PublicKey = <laptop public key>
AllowedIPs = 10.8.0.2/32

Il lato client:

[Interface]
Address = 10.8.0.2/32
PrivateKey = <laptop private key>

[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

La stessa parola chiave ha un effetto opposto sui due lati. Sul client indica di "inviare ogni destinazione a questo peer". Sul server indica di "accettare da questo peer soltanto questo indirizzo". L'asimmetria dipende dai valori, non dal ruolo.

Metà di queste chiavi non appartiene affatto al protocollo. Address, DNS, MTU, PostUp e SaveConfig appartengono a wg-quick, lo script shell che attiva l'interfaccia. Il kernel non le vede. wg-quick strip wg0 stampa la configurazione ridotta effettivamente caricata dallo strumento wg, ed è il modo più rapido per vedere questa separazione.

Cosa fa realmente l'handshake

L'handshake di WireGuard è Noise_IKpsk2, definito nel Noise Protocol Framework. La parte IK è quella utile per un sysadmin: la chiave pubblica statica del responder è già nota all'initiator, perché è il valore PublicKey nel blocco [Peer], e l'initiator invia la propria chiave pubblica statica all'interno del primo messaggio, cifrata. Non c'è quindi alcuno scambio di certificati né un round trip per verificare l'identità. Un osservatore passivo non può sapere quale chiave sta effettuando la chiamata, a meno che non disponga della chiave privata del responder.

Il costo è un round trip. Il messaggio di inizializzazione è di 148 byte, la risposta è di 92 byte e subito dopo inizia il flusso dei dati. Ogni peer genera una nuova coppia di chiavi effimere Curve25519 per ogni handshake e le chiavi di sessione derivano da una catena di risultati Diffie-Hellman che combina le chiavi statiche e quelle effimere. Le chiavi private effimere vengono quindi eliminate, garantendo la forward secrecy: se qualcuno registra oggi il traffico e ruba la chiave privata del server il prossimo anno, non può comunque leggere ciò che ha registrato.

Un'inizializzazione dell'handshake contiene un timestamp TAI64N e ogni peer memorizza il timestamp più recente ricevuto dall'altro peer, quindi un'inizializzazione riprodotta viene rifiutata. I pacchetti dati contengono un contatore a 64 bit usato come nonce e il destinatario conserva una finestra scorrevole dei contatori ricevuti di recente. In questo modo gestisce replay e riordinamenti significativi senza mantenere uno stato di connessione come quello di TCP.

Le chiavi di sessione non durano a lungo e i timer sono definiti nel codice compilato, non configurabili.

ChartWireGuard protocol timers, in seconds
The data behind this chart
[
  {
    "label": "REKEY_TIMEOUT",
    "seconds": 5,
    "notes": "resend a handshake initiation that got no answer"
  },
  {
    "label": "KEEPALIVE_TIMEOUT",
    "seconds": 10,
    "notes": "send a keepalive after receiving data and sending none back"
  },
  {
    "label": "REKEY_ATTEMPT_TIME",
    "seconds": 90,
    "notes": "give up on the handshake and report the peer as down"
  },
  {
    "label": "REKEY_AFTER_TIME",
    "seconds": 120,
    "notes": "sender begins a fresh handshake for a new session key"
  },
  {
    "label": "REJECT_AFTER_TIME",
    "seconds": 180,
    "notes": "the old session key is refused and traffic stops"
  }
]

Questi sono valori costanti della specifica del protocollo, non misurazioni. Il 5 determina l'intero ciclo di vita della sessione. Dopo 120 secondi di utilizzo, il mittente avvia un nuovo handshake. Dopo 180 secondi, la vecchia chiave viene rifiutata definitivamente, quindi il traffico si interrompe finché non viene completato un nuovo handshake. Un'inizializzazione che non riceve risposta viene ritrasmessa ogni 5 secondi e abbandonata dopo 90 secondi. Per questo wg show stampa latest handshake come età relativa e un tunnel integro e sotto carico mantiene questo valore basso. Se l'età aumenta mentre si sta inviando attivamente traffico, significa che gli handshake stanno fallendo, non che il tunnel è inattivo.

Perché un peer non ha il ruolo di client o server

Entrambe le estremità eseguono lo stesso codice e usano lo stesso formato di configurazione. Non esiste una modalità server. L'asimmetria percepita deriva da Endpoint, mentre Endpoint è facoltativo.

Un peer con un endpoint configurato può avviare un handshake. Un peer senza endpoint attende, quindi apprende l'indirizzo e la porta dell'altra estremità dal primo pacchetto autenticato correttamente. Questo endpoint appreso viene memorizzato e aggiornato ogni volta che arriva un pacchetto valido da un nuovo indirizzo. È così che funziona il roaming: un laptop che passa dal Wi-Fi alla rete mobile mantiene lo stesso tunnel, perché una sessione viene identificata tramite chiave e indice, non tramite indirizzo IP. Nulla si riconnette, perché in senso TCP non era mai stata stabilita alcuna connessione.

Lo stesso meccanismo porta a una conseguenza importante: il peer con l'indirizzo pubblico conserva sempre l'ultimo IP pubblico noto dell'altra estremità e wg show lo visualizza.

Primitive fisse, senza negoziazione

In WireGuard non esiste un elenco di ciphersuite. ChaCha20-Poly1305 per la cifratura autenticata, Curve25519 per l'accordo sulle chiavi, BLAKE2s per l'hashing, HKDF per la derivazione delle chiavi. Tutte le implementazioni usano queste primitive: non esiste quindi una fase di negoziazione da analizzare né un percorso di downgrade verso un'opzione più debole. Il compromesso è concreto: se una di queste primitive viene compromessa, la correzione richiede una nuova versione dell'intero protocollo e un aggiornamento su entrambe le estremità, non una modifica della configurazione. Questa scelta elimina gran parte del codice e delle modalità di errore presenti in un tunnel basato su TLS. È soprattutto questo il punto del confronto in WireGuard rispetto a OpenVPN.

Perché la porta non risponde a uno scanner

Ogni messaggio di handshake contiene un campo chiamato mac1. È un MAC (codice di autenticazione del messaggio) calcolato sul messaggio usando una chiave derivata dalla chiave pubblica statica del responder. Un mittente che non conosce tale chiave pubblica non può generare un mac1 valido e il ricevitore scarta il pacchetto senza inviare alcuna risposta. Nessun errore, nessun reset, nessun messaggio ICMP.

Il risultato osservabile è una scansione UDP che non riceve alcuna risposta.

sudo nmap -sU -p 51820 vpn.example.com

nmap segnala open|filtered, la stessa risposta che restituisce per una porta i cui pacchetti vengono scartati silenziosamente da un firewall. La porta si comporta allo stesso modo sia quando WireGuard è in ascolto sia quando non lo è, almeno per chi non possiede già la chiave pubblica.

Un secondo campo, mac2, gestisce la pressione dovuta agli attacchi di tipo denial of service. Quando il ricevitore è sotto carico, risponde a un'iniziazione valida con una risposta cookie di 64 byte associata all'indirizzo sorgente del mittente e rifiuta di eseguire operazioni costose con la chiave pubblica finché il mittente non restituisce tale cookie. In questo modo verifica che l'indirizzo sorgente sia reale prima di dedicargli risorse della CPU. Il meccanismo si attiva soltanto sotto carico.

Perché 0.0.0.0/0 trasforma un peer nella route predefinita

Poiché AllowedIPs è la tabella di routing, AllowedIPs = 0.0.0.0/0, ::/0 assegna a quel peer qualsiasi destinazione. Questa è l’intera configurazione del full tunnel.

Il routing che consente il funzionamento è più interessante della riga in sé. Una route predefinita semplice tramite wg0 creerebbe un loop, perché anche il pacchetto UDP crittografato che trasporta il traffico deve uscire dalla macchina e corrisponderebbe alla propria route predefinita. wg-quick evita il problema con il policy routing. Contrassegna i pacchetti in uscita generati da WireGuard con un fwmark, inserisce la route predefinita del tunnel in una tabella di routing separata e aggiunge regole che vi inoltrano soltanto il traffico senza contrassegno. Esegui ip rule show per vedere il risultato:

32764:	from all lookup main suppress_prefixlength 0
32765:	not from all fwmark 0xca6c lookup 51820
32766:	from all lookup main

0xca6c è 51820 in formato esadecimale e 51820 è anche il numero della tabella. La regola suppress_prefixlength 0 fa sì che la tabella principale ignori la propria route predefinita. In questo modo le route specifiche, come quella della subnet locale, mantengono la precedenza, mentre tutto il resto passa alla tabella del tunnel. Un split tunnel non richiede nulla di tutto questo: un elenco più ristretto, come AllowedIPs = 10.8.0.0/24, 10.20.0.0/16, diventa una serie di route ordinarie nella tabella principale.

Un full tunnel, da solo, non risolve la risoluzione dei nomi. Il resolver appreso dal client tramite la rete locale rimane in genere in uso e la relativa route è più specifica. Si tratta di un’attività separata, descritta in DNS che esce dal tunnel WireGuard.

A cosa serve realmente PersistentKeepalive

WireGuard non invia nulla quando non c’è traffico. Nessun heartbeat, nessun aggiornamento della sessione, nulla sulla rete. Questo riduce il consumo della batteria, è utile anche nello scenario dello scanner descritto sopra, ma impedisce il funzionamento di una configurazione specifica.

Un peer dietro NAT (network address translation) o dietro un firewall stateful è raggiungibile dall’esterno solo mentre sul dispositivo esiste un mapping creato da un pacchetto in uscita. I mapping UDP hanno spesso una durata iniziale di circa 30 secondi. Quando il mapping scade, il middlebox scarta i pacchetti provenienti dal lato pubblico e il tunnel sembra inattivo finché il peer dietro NAT non invia qualcosa. PersistentKeepalive = 25 invia un pacchetto autenticato vuoto ogni 25 secondi, quindi resta al di sotto della durata comune più breve e mantiene aperto il mapping.

Impostalo sul peer dietro NAT. Un server con un indirizzo pubblico e una porta UDP aperta non ne ha bisogno; impostarlo anche sul server aggiunge soltanto traffico. Non confonderlo con il keepalive automatico, che viene inviato 10 secondi dopo che un peer riceve dati e non ha nulla da inviare in risposta. Questo keepalive è sempre attivo e non può essere configurato.

Instradare la rete LAN attraverso il tunnel non è una funzionalità di WireGuard

Supponiamo che il peer B si trovi sulla rete domestica 192.168.50.0/24 e che il peer A debba raggiungerla. Devono essere configurati correttamente due sistemi distinti, e solo uno dei due è WireGuard.

La parte di WireGuard: aggiungere 192.168.50.0/24 a AllowedIPs di B su A. In questo modo A instrada il prefisso verso B e accetta da B i pacchetti con quegli indirizzi sorgente. Senza questa configurazione, il routing basato su cryptokey non dispone di una chiave per la destinazione né dell'autorizzazione per la sorgente.

La parte del kernel: su B, net.ipv4.ip_forward deve essere impostato su 1, altrimenti il kernel scarta tutti i pacchetti decifrati che non sono destinati a B. La catena forward del firewall di B deve consentire il traffico. Gli host della LAN devono avere una route di ritorno verso 10.8.0.0/24, oppure B deve applicare il source NAT affinché le risposte tornino attraverso B.

Il compito di WireGuard termina quando consegna il pacchetto decifrato al kernel. Tutto ciò che avviene dopo riguarda il normale routing e filtraggio di Linux. Per questo il problema si manifesta nei contatori nft list ruleset o in ip -s link show wg0, non in wg show. Se preferisci gestire i peer tramite un'interfaccia web, eseguire wg-easy in Docker genera le configurazioni dei peer, mentre le regole di forwarding devono comunque essere configurate sull'host.

Perché WireGuard risiede nel kernel

wg0 è un driver di dispositivo di rete. I pacchetti lo raggiungono tramite il normale stack di routing, vengono cifrati nel contesto softirq e lo lasciano attraverso un socket UDP senza mai passare nello userspace. Da qui deriva il throughput. Per questo il modulo resta di circa quattromila righe di codice: è abbastanza piccolo da poter essere revisionato e integrato nel kernel Linux mainline 5.6 a marzo 2020. Ubuntu 24.04 e Debian 13 lo includono, quindi manca soltanto il pacchetto wireguard-tools.

Il fatto che sia un'interfaccia normale ha conseguenze pratiche. tcpdump -ni wg0 mostra i pacchetti interni in chiaro, mentre tcpdump -ni eth0 udp port 51820 mostra quelli esterni cifrati. Confrontarli permette di capire subito quale direzione non funziona. netfilter e il traffic shaping trattano wg0 come qualsiasi altro collegamento. Quando il modulo del kernel non è disponibile, ad esempio nella virtualizzazione dei container che condivide il kernel dell'host, wireguard-go implementa lo stesso protocollo nello userspace tramite un dispositivo TUN. Questo comporta una riduzione effettiva del throughput, perché ogni pacchetto attraversa due volte il confine tra kernel e userspace.

Cosa non protegge WireGuard

Il modello delle minacce è volutamente ristretto e un protocollo così silenzioso può indurre a conclusioni troppo ottimistiche. Va dichiarato con chiarezza.

  • Non nasconde che stai usando WireGuard. I messaggi di handshake hanno dimensioni fisse, il primo byte indica il tipo di messaggio e il trasporto usa UDP. La deep packet inspection lo riconosce facilmente e una rete che non consente le VPN può bloccarlo. L'offuscamento è stato escluso intenzionalmente.
  • Non nasconde il volume o la temporizzazione del traffico. I payload vengono sottoposti a padding soltanto fino al limite di 16 byte, quindi un osservatore vede comunque quando invii dati e, approssimativamente, quanti.
  • Conserva l'ultimo endpoint noto. Il peer con l'indirizzo pubblico memorizza l'indirizzo IP pubblico corrente dell'altra parte e wg show lo visualizza. Insieme a un indirizzo del tunnel fisso nella configurazione, questo costituisce un identificatore stabile che segue l'utente da una rete all'altra. Sul tuo VPS è normale. È anche il motivo per cui i servizi commerciali aggiungono un livello sopra il protocollo.
  • Autentica una chiave, non una persona. Il peer è chiunque possieda il file della chiave privata. Mantieni /etc/wireguard con modalità 700 e i file delle chiavi con modalità 600.
  • Non esistono né un elenco di revoca né una scadenza. L'accesso termina quando elimini la voce del peer da ogni server che la contiene, mentre le chiavi statiche restano valide finché non le rimuovi.

Niente di tutto questo rende WireGuard debole. Lo rende essenziale, ed è proprio questo il suo scopo: autentica e cifra, mentre lascia la gestione delle identità e l'assegnazione degli indirizzi a ciò che costruisci sopra di esso. Un livello di coordinamento del tipo descritto in WireGuard a confronto con Tailscale serve esattamente a colmare questa lacuna, usando lo stesso piano dati appena descritto.

FAQ

Che cos'è il cryptokey routing in WireGuard?

Il cryptokey routing è la regola che associa ogni pacchetto a una chiave pubblica. Ogni voce peer contiene un elenco di prefissi in AllowedIPs. In uscita, WireGuard seleziona il peer confrontando la destinazione del pacchetto con l'elenco di ogni peer e applicando la corrispondenza con il prefisso più lungo; l'elenco funziona quindi come una tabella di routing. In ingresso, dopo la decrittografia e l'autenticazione del pacchetto, il relativo indirizzo sorgente interno deve rientrare nello stesso elenco del peer; in caso contrario il pacchetto viene scartato. L'elenco funziona quindi anche come una lista di controllo degli accessi. WireGuard non ha una configurazione di routing separata né un firewall interno separato, perché lo stesso elenco svolge entrambe le funzioni.

Devo configurare PersistentKeepalive su entrambi i peer?

No. Configuralo sul lato che si trova dietro NAT (network address translation) o dietro un firewall stateful, che di solito è il client. WireGuard non invia nulla quando è inattivo, quindi la mappatura che consente al lato remoto di raggiungere quel peer scade, spesso entro un minuto, e il tunnel appare quindi non raggiungibile in una sola direzione. PersistentKeepalive = 25 invia un pacchetto autenticato vuoto ogni 25 secondi e mantiene aperta la mappatura. Un peer con un indirizzo pubblico e una porta UDP aperta non ne ha bisogno.

Perché il ping attraverso il tunnel restituisce "Required key not available"?

Perché l'indirizzo di destinazione non rientra in AllowedIPs di alcun peer. Il cryptokey routing non ha quindi trovato una chiave con cui cifrare il pacchetto e il kernel ne ha rifiutato l'invio. Esegui wg show wg0 allowed-ips e confronta l'output con l'indirizzo sottoposto a ping. L'errore simile Destination address required indica un problema diverso: è stato trovato un peer corrispondente, ma WireGuard non dispone di un endpoint per quel peer, perché non ne è stato configurato alcuno e da quel peer non è ancora arrivato nessun pacchetto autenticato.

Un firewall può rilevare e bloccare WireGuard?

Sì. WireGuard autentica e cifra il traffico e non tenta di camuffarsi. I messaggi di handshake hanno dimensioni fisse di 148 e 92 byte, il primo byte di ogni messaggio ne identifica il tipo e il trasporto usa UDP; per questo l'ispezione approfondita dei pacchetti identifica il protocollo senza difficoltà. Le reti che bloccano UDP o che rilevano le impronte dei protocolli ne interromperanno il funzionamento. Nascondere il tunnel significa incapsularlo in qualcos'altro; è necessario quindi uno strumento separato, non un'impostazione di WireGuard.