Fissare un solo programma a una VPN su Linux è soprattutto un problema di routing, e la regola del firewall decide solo cosa succede dopo che la rotta è stata scelta. Se abbini l'UID del programma in OUTPUT prima che il tunnel possieda la rotta predefinita, la regola scarta proprio il traffico che volevi proteggere. La stessa regola non vede nemmeno il processo del resolver che risponde alle query DNS del programma. Metti nell'ordine giusto e un solo account senza privilegi diventa un selettore affidabile del tunnel diviso.

Dai all'account una rotta prima del verdetto

La corrispondenza del proprietario risponde a una domanda: un socket locale appartenente a questo UID ha costruito il pacchetto? Non sceglie un'interfaccia e non può spostare il traffico che la tabella principale invia già attraverso la NIC fisica. Per questo una regola UID senza rotta corrispondente blocca il programma invece di reindirizzarlo.

Manda l'account prima attraverso la sua tabella, poi lascia che il firewall confermi il risultato. Funziona su un host il cui iproute2 capisce uidrange, che ip rule help segnala in una riga. La tabella 100 contiene una rotta predefinita attraverso il tunnel, e la regola invia solo l'UID 1000 a quella tabella. Tutto il resto resta sulla tabella principale e continua a usare il collegamento fisico.

ip rule add uidrange 1000-1000 table 100
ip route add default dev tun0 table 100

Un secondo account che ha bisogno di un altro tunnel riceve la tabella 101 e la propria regola, perché due profili non possono possedere entrambi la rotta predefinita nella stessa tabella. I profili di NetworkManager sono a livello di sistema, quindi questa parte resta manuale, e le regole spariscono al riavvio se qualcosa non le ripristina. Scrivi le tabelle in uno script di avvio accanto alla definizione del tunnel, così una riconnessione non possa lasciare un account sul percorso sbagliato.

Perché l'eccezione loopback uccide il tunnel

La maggior parte delle versioni copiate della regola scarta ogni pacchetto che non esce da loopback. Il tunnel non è loopback, quindi quella forma scarta anche il traffico del tunnel, e il risultato sembra una VPN morta più che un errore del firewall.

Quando OUTPUT viene eseguito, il kernel ha già scelto un'interfaccia di uscita, quindi -o viene confrontato con la risposta della rotta e non con l'intestazione del pacchetto. La corrispondenza del proprietario viene consultata solo in OUTPUT e POSTROUTING, e i pacchetti inoltrati non portano alcun socket da leggere. Questa combinazione significa che l'eccezione deve nominare l'interfaccia del tunnel e negare quella corrispondenza, invece di negare loopback.

Loopback merita ancora il suo ACCEPT sopra il DROP, perché il primo pacchetto DNS del programma va allo stub locale tramite lo. Metti le due regole in questo ordine e l'account raggiunge lo stub, poi il tunnel, e nient'altro.

iptables -I OUTPUT 1 -o lo -m owner --uid-owner vpnapp -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner vpnapp ! -o tun0 -j DROP

IPv6 è un insieme di regole separato, e una coppia solo per v4 lascia ip6tables intatto. Un programma che preferisce i record AAAA uscirebbe allora dal collegamento fisico senza mai raggiungere il DROP. Rispecchia entrambe le regole in ip6tables, oppure disattiva IPv6 sull'host quando il tunnel non porta alcun indirizzo v6. Anche le regole aggiunte a mano spariscono al riavvio, quindi salvale con netfilter-persistent save o ricostruisci la stessa logica come tabella nftables caricata dall'unità di avvio.

Le risposte DNS arrivano da un altro processo

La query del programma verso 127.0.0.53 passa l'ACCEPT di loopback, ed è lì che la corrispondenza UID si ferma. Il resolver stub inoltra verso l'alto dal proprio account di servizio, quindi una regola legata a vpnapp non vede mai la seconda query. Se il resolver sceglie un server LAN per il collegamento, le risposte tornano dalla NIC fisica mentre i pacchetti del programma restano nel tunnel. Il risultato visibile è un tunnel funzionante con nomi rotti, o nomi a cui risponde un server che non hai scelto.

Correggi la risoluzione dei nomi sul collegamento del tunnel, non nell'applicazione. Dai al profilo del tunnel un server DNS e un dominio di routing, poi conferma con resolvectl query e resolvectl status tun0 che il resolver lo usa per i nomi che contano. Dentro uno spazio dei nomi di rete il problema sparisce, perché il resolver dello spazio conosce solo il tunnel.

Controlla la regola dall'account stesso

Chiedi al kernel quale rotta riceve un UID, poi fai una richiesta come quell'account e confronta l'indirizzo di uscita con quello del tuo account. Se entrambi riportano lo stesso indirizzo, la regola corrisponde a qualcosa di diverso dal traffico che hai testato. Il primo comando qui sotto mostra la ricerca della rotta senza inviare un pacchetto, il che separa un errore di routing da un errore del firewall.

ip route get 1.1.1.1 uid 1000
sudo -u vpnapp curl -s https://ifconfig.co

I contatori mostrano se una regola è mai scattata. Esegui iptables -vnL OUTPUT, leggi il conteggio dei pacchetti accanto a ogni regola e tratta uno zero accanto al DROP dopo traffico reale come un errore di corrispondenza.

Il traffico inoltrato resta fuori da questo progetto. Un container o una VM dietro un bridge non ha alcun socket locale posseduto da vpnapp, quindi la corrispondenza del proprietario non scatta mai e quei pacchetti seguono invece la politica FORWARD. Se quel carico deve essere coperto, provalo separatamente e scrivi una politica legata al bridge o all'indirizzo del container. Il progetto tiene quando la tabella, l'eccezione dell'interfaccia, il resolver e le regole v6 puntano tutti nella stessa direzione; lascia fuori uno di essi e l'account perde traffico o perde la connettività.

Articoli correlati