Come instradare una sola applicazione tramite VPN senza tutta la macchina
Nessun desktop non gestito offre un interruttore VPN per processo. Scegliete lo strato che seleziona il traffico: un socket vincolato, una regola UID, un profilo del firewall o un salto SOCKS.
Il client VPN si connette, il browser funziona, e l’unico comando che ti interessa davvero esce ancora dalla tua connessione di casa. Oppure accade il contrario: il tunnel prende la rotta predefinita e il file server locale sparisce dal desktop. Mi è capitato con un agente di build che doveva raggiungere uno specchio interno di artefatti mentre il resto dell’host restava sulla LAN dell’ufficio, e il client offriva esattamente due scelte: tutto il traffico o niente. La richiesta nei due sensi è la stessa, questo programma e non tutta la macchina, e la maggior parte dei client non ha una casella per questo, perché la tabella di routing non ha mai saputo cos’è un programma.
Perché la tabella di routing non vede il tuo programma
Quando un processo invia un pacchetto, lo stack cerca l’indirizzo di destinazione nella tabella di routing. Né il nome dell’eseguibile né la porta entrano in quella ricerca. I nomi di dominio falliscono per lo stesso motivo: un tunnel porta il traffico a un indirizzo, e quando un nome si è risolto in uno, il routing è l’unica leva che il kernel ha ancora. Per questo «VPN solo per questo sito» finisce per significare un proxy, e «VPN solo per questo programma» significa uno di un piccolo insieme di meccanismi.
| Mechanism | What selects | Covers | On tunnel drop |
|---|---|---|---|
| Socket bind | source address | a client you control | connections fail |
| UID rule (Linux) | owning user | every process of one account | connections fail |
| Firewall profile (Windows) | adapter profile | one program | fails closed |
| Network namespace | namespace membership | everything started inside | connections fail |
| SOCKS hop | app proxy setting | one app or one browser profile | connections fail |
Nessuno di questi è una rotta per processo. Ognuno sposta la decisione a uno strato che vede il programma, l’utente o la configurazione dell’applicazione stessa.
Una sola destinazione è ancora una rotta, non una regola di programma. Su macOS la ricetta abituale è togliere la spunta a «Invia tutto il traffico tramite VPN» e aggiungere una rotta host: sudo route add -host internal-tuner.example -interface ppp0, rimossa di nuovo con sudo route delete -host internal-tuner.example -interface ppp0. Leggi prima il nome dell’interfaccia da ifconfig, perché ppp0 è solo un esempio. I caratteri jolly non funzionano in una rotta host, quindi un servizio che parla con più nomi ha bisogno di una voce per nome, e la regola copre ogni processo della macchina, non quello che avevi in mente.
Vincola il socket all’indirizzo del tunnel
Quando controlli il client, il binding è il selettore meno costoso. Un socket vincolato all’indirizzo locale del tunnel esce da quell’interfaccia finché lì esiste una rotta verso la destinazione, e tutto il resto della macchina mantiene il suo percorso normale. La sottigliezza è che il binding sceglie l’indirizzo sorgente mentre la tabella di routing sceglie ancora l’uscita. Il solo binding non sposta il traffico che non ha una rotta nel tunnel, ed è l’errore dietro la maggior parte delle segnalazioni «l’ho vincolato e continua a uscire dalla parte sbagliata».
import socket
s = socket.socket()
s.bind(("10.8.0.2", 0))
s.connect(("build.internal.example", 443))Gli strumenti di livello superiore espongono la stessa idea: curl --interface 10.8.0.2, CURLOPT_INTERFACE in PHP e l’argomento source_address nei client HTTP di Python. L’indirizzo deve appartenere al tunnel, e un errore di battitura qui fallisce come un errore di connessione ordinario, non come una lamentela sul routing. Vincolare a un nome di dispositivo invece che a un indirizzo è un privilegio di Linux e non si trasferisce a Windows o macOS, per questo gli script portabili passano l’indirizzo.
Linux: fai combaciare il proprietario, non il nome del programma
Il kernel può far combaciare l’utente proprietario di un socket, così un account dedicato diventa il selettore. Metti il tunnel nella sua tabella di routing e invia lì solo il traffico di quell’account:
ip rule add uidrange 1001-1001 lookup 100
ip route add default dev tun0 table 100Questo funziona solo se il tunnel non è già la rotta predefinita. Avvia il client con --route-nopull, oppure filtra il reindirizzamento spinto con --pull-filter ignore "redirect-gateway" su OpenVPN 2.4 e successivi, poi aggiungi tu stesso la voce nella tabella. Altrimenti esistono entrambe le predefinite e vince quella con priorità più bassa. La versione deliberata di ciò è puntare la rotta predefinita al gateway del tunnel, o farlo da uno script di disconnessione: tiene il traffico fuori da Internet quando il tunnel cade, ma è un interruttore di sicurezza, non un selettore, e resta finché qualcuno non lo rimuove.
La versione più forte è un namespace di rete. Creala, esegui il client al suo interno e avvia l’applicazione con ip netns exec. Niente fuori dal namespace vede l’interfaccia del tunnel, quindi l’applicazione non può ricadere per caso sulla rotta dell’host. Il costo è reale: il namespace ha bisogno di una coppia veth per raggiungere la rete fisica e di un proprio file di risoluzione, perché il namespace di mount non condivide le impostazioni di risoluzione dell’host.
Due modalità di guasto spiegano la maggior parte delle perdite. La risoluzione dei nomi avviene ancora nel resolver dell’host, quindi la query esce fuori dal tunnel anche quando la connessione TCP entra dentro. E IPv6 ha bisogno della propria ip -6 rule, perché una regola v4 non la copre.
Windows e macOS: delimita il firewall o cedi un proxy
Anche Windows non ha un selettore di rotta per applicazione, ma le sue regole del firewall possono essere limitate a un profilo, e un adattatore VPN può essere classificato come «pubblico» mentre la LAN di casa resta «privata». Crea regole di blocco in uscita per il programma sui profili «privato» e «dominio», lascia «pubblico» aperto, e l’applicazione può raggiungere la rete solo attraverso il tunnel. Imposta prima la categoria di rete della connessione, poi conferma che la regola ha combaciato nel pannello di monitoraggio del firewall. È un blocco, non una rotta: fallisce chiuso quando il tunnel cade, e non può fermare un’applicazione che ignora il firewall.
macOS segue la stessa logica con altri strumenti. Un listener SOCKS raggiunto via SSH è l’opzione meno invasiva, perché ssh -D 1080 user@host crea una sola porta locale e solo le applicazioni configurate per usarla prendono quella strada. Una regola pf può bloccare un programma per utente o gruppo, ma ancora non può scegliere il tunnel solo per quel programma. Tunnelblick non può filtrare per dominio affatto, perché le rotte lavorano sugli indirizzi; gli utenti di OpenConnect ricorrono a vpn-slice quando il server spinge un elenco di prefissi troppo ampio.
Due aggiramenti coprono ciò che gli strumenti della piattaforma mancano. Le estensioni del browser che cambiano proxy per dominio sono la risposta pratica per «solo questo sito», perché agiscono allo strato applicativo dove i nomi esistono ancora; coprono un browser e nient’altro. La soluzione brutale è eseguire il client del fornitore in una macchina virtuale così che l’ospite possieda il cambio di rotta mentre l’host mantiene il suo percorso, al costo di due stack di rete da mantenere.
Un salto SOCKS per gli strumenti senza opzione di binding
La maggior parte delle applicazioni può essere puntata a un proxy HTTP o SOCKS anche quando non sa nulla delle interfacce. Un client SSH ne fornisce uno senza alcuna modifica lato server:
ssh -N -D 127.0.0.1:9999 [email protected]Tutto ciò che è configurato per usare quella porta esce al bastion mentre il resto del desktop mantiene la connessione normale. Un’istanza locale di 3proxy vincolata all’indirizzo del tunnel fa lo stesso per una porta senza SSH, e stunnel copre una singola porta avvolta in TLS. I limiti sono pratici: un proxy trasporta solo i protocolli che sanno usarlo, UDP richiede un’associazione UDP SOCKS5 che l’impostazione «SOCKS5» di molti client non attiva, e il DNS deve essere risolto dal proxy (socks5h in curl), altrimenti il nome sfugge al resolver locale.
Tieni il listener su loopback a meno che un’altra macchina non ne abbia bisogno.
Verifica il percorso con una cattura, non con un messaggio di stato
Le finestre di stato dell’applicazione descrivono l’impostazione, non il pacchetto. Cattura su entrambe le interfacce e confronta:
tcpdump -i any -n port 443
ss -tnp | grep 443Se la connessione compare sull’interfaccia fisica senza una voce corrispondente sul tunnel, il selettore non è in vigore, qualunque cosa riporti il client. Controlla IPv6 e il resolver separatamente, e controlla se esiste un secondo percorso come un processo ausiliario o un proxy di sistema ereditato.
Poi scegli il confine che corrisponde al requisito: un proxy basta per un sito, una regola UID basta per un account, un profilo del firewall basta quando la connessione deve fallire chiusa, e un namespace è l’unica opzione che nasconde il tunnel a tutto ciò che sta fuori.