Come instradare solo il traffico dei container Docker tramite OpenVPN
Lasciate l'host sul suo uplink abituale e mettete una sola sottorete di container nel tunnel: ordine NAT, regole di origine, iroute di OpenVPN e controlli MSS.
Il pacchetto del container non raggiunge mai tun0, e capacità aggiuntive sul container non lo cambieranno. Tra l'ospite e il tunnel ci sono due ricerche di rotta: una nello spazio dei nomi di rete dell'ospite, una sull'host dopo che il frame arriva sul bridge, e la seconda consulta una tabella diversa con regole di corrispondenza diverse. La mia disposizione di riferimento è un host Linux con un OpenVPN client che detiene 10.8.0.10 su tun0, un bridge Docker su 172.18.0.0/16, e un indirizzo privato che solo il lato tunnel può raggiungere. L'obiettivo resta stretto: quella sottorete ospite esce dal tunnel mentre l'host conserva il suo uplink abituale.
Eseguire il client VPN dentro il container è un'altra progettazione, non una scorciatoia. Allora il container possiede il suo tunnel e le sue credenziali, e il lavoro si sposta in un Dockerfile invece di restare in iptables.
L'inoltro sceglie una rotta per destinazione, poi NAT riscrive l'origine
Con net.ipv4.ip_forward=1 impostato, un pacchetto inoltrato viene instradato solo per destinazione, nella tabella principale. L'origine non ha alcun ruolo in quella decisione. Solo dopo la scelta della rotta il kernel esegue POSTROUTING, ed è lì che si trova la regola propria di Docker: -s 172.18.0.0/16 ! -o br-appnet -j MASQUERADE. Tutto ciò che lascia il bridge verso un'altra interfaccia viene tradotto, e l'indirizzo in cui si trasforma dipende dall'interfaccia a cui la rotta lo ha inviato.
Quella regola spiega la maggior parte dei sintomi che mi vengono descritti. Se la tabella principale non ha una rotta per la destinazione privata, il pacchetto esce dall'uplink con l'indirizzo fisico dell'host. Il provider scarta la destinazione, o il sito remoto scarta un'origine RFC 1918 che non si aspettava, e il container va in timeout mentre il tunnel resta su e sano. Una cattura su tun0 non mostra nulla, e questo è l'indizio. Se la rotta punta davvero a tun0, MASQUERADE riscrive l'origine in 10.8.0.10 e lo stesso container funziona senza che tocchiate nulla.
Chiedete la ricerca che nomina l'origine dell'ospite. Senza from e iif l'host risponde per il proprio socket e segnala un percorso che il pacchetto inoltrato non prenderà mai:
ip route get 192.168.11.222 from 172.18.0.5 iif br-appnetSe quella risposta nomina l'uplink, il tunnel non è in gioco e nessuna impostazione del lato tunnel aiuterà.
Portate l'ospite sull'indirizzo proprio del tunnel
Il cambiamento minimo che funziona è una riscrittura esplicita dell'origine in uscita. Inseritela invece di aggiungerla: Docker siede già in testa a POSTROUTING, quindi una regola aggiunta con -A viene valutata dietro quella di Docker e non scatta mai.
iptables -t nat -I POSTROUTING 1 -s 172.18.0.0/16 -o tun0 -j SNAT --to-source 10.8.0.10Due condizioni decidono se questo tiene. L'indirizzo deve essere uno che il client possiede davvero, quindi fissatelo con una voce per client che contenga ifconfig-push 10.8.0.10 255.255.255.0; una regola che nomina un indirizzo mai assegnato produce scarti silenziosi, non un errore. E il lato remoto deve accettare quell'origine e instradare le risposte indietro, per questo una lista di autorizzati lì nomina l'indirizzo del tunnel e non la sottorete ospite. Quando l'indirizzo del client è dinamico, -j MASQUERADE con -o tun0 segue ciò che l'interfaccia possiede, al costo di una politica remota contro cui non potete scrivere un valore fisso.
Il filtraggio è l'altra metà. Docker imposta la politica FORWARD su drop e svuota le catene che possiede a ogni riavvio del demone, quindi mettete la politica locale in DOCKER-USER, che sopravvive:
iptables -I DOCKER-USER -i br-appnet -o tun0 -j ACCEPT
iptables -I DOCKER-USER -i tun0 -o br-appnet -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTLa regola di ritorno conta: una risposta che arriva su tun0 per una connessione aperta dal bridge non viene accettata solo perché la direzione in uscita lo è stata.
Tenete l'host diretto con una regola di origine
Quando l'intero bridge deve usare il tunnel ma nient'altro sull'host deve farlo, saltate NAT e date a quell'origine la sua tabella. Gli indirizzi ospite restano allora visibili da un capo all'altro.
echo "200 vpnbox" >> /etc/iproute2/rt_tables
ip rule add from 172.18.0.0/16 lookup vpnbox pref 1000
ip route add default dev tun0 table vpnboxLe regole vengono consultate prima della tabella principale, e from corrisponde all'origine. I pacchetti esterni del tunnel portano l'origine fisica dell'host, quindi non corrispondono mai a from 172.18.0.0/16 e il tunnel non può instradare se stesso di ritorno in se stesso. È questa autoesclusione a rendere stabile la disposizione.
Il percorso di ritorno ora ha bisogno di aiuto. Il lato remoto vede un 172.18.0.x non tradotto e non ha motivo di sapere dove vive. Sul server, un file per client necessita di iroute 172.18.0.0 255.255.0.0 perché la tabella interna del server consegni quel prefisso a questo client, e di un route 172.18.0.0 255.255.0.0 spinto perché gli altri client sappiano che esiste.
Due controlli prima di fidarvi. Eseguite ip route show default: con redirect-gateway def1 la tabella contiene 0.0.0.0/1 e 128.0.0.0/1 via tun0, e l'host non è affatto diretto, perché quei mezzi predefiniti vincono senza sostituire default. Le regole aggiunte con ip rule add spariscono anche al riavvio, quindi portatele in uno script up.
La dimensione dei pacchetti è il secondo guasto che incontrate
Un'interfaccia bridge a 1500 byte dà al tunnel più di quanto possa portare una volta aggiunte le intestazioni UDP e IP esterne. Gli scambi piccoli passano, quindi DNS risolve e l'handshake TLS si completa, poi un caricamento più grande si blocca senza alcun errore da nessuna parte. Il mittente continua a ripetere la stessa dimensione di segmento perché il messaggio ICMP di necessità di frammentazione, che gli direbbe di ridurre, non arriva mai.
Correggetelo sull'interfaccia, sull'MSS o su entrambi. Il serraggio non richiede alcuna collaborazione dell'ospite:
ip link set dev tun0 mtu 1400
iptables -t mangle -A FORWARD -o tun0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtuIl serraggio riscrive solo TCP. UDP, QUIC incluso, ha bisogno invece di un'interfaccia ospite più bassa, con --opt com.docker.network.driver.mtu=1400 alla creazione della rete o la chiave compose corrispondente.
DNS e i controlli che vale la pena ripetere
La risoluzione dei nomi segue lo stesso percorso. Il resolver integrato di Docker su 127.0.0.11 inoltra ai server configurati dell'host, quindi un container eredita ciò che usa l'host. Quando la zona privata è risolta solo da un resolver raggiungibile tramite il tunnel, puntate l'ospite a quell'indirizzo, e provate con getent hosts dentro il container invece che sull'host.
- Eseguite la richiesta che fallisce dentro l'ospite; un test sul lato host usa un altro indirizzo di origine e nasconde il problema.
- Leggete i contatori delle regole NAT e di filtraggio che avete aggiunto. Una regola senza corrispondenze è un errore di instradamento, non di politica.
- Confermate che l'indirizzo del tunnel nella vostra regola SNAT sia uno che il client possiede davvero.
- Abbassate l'MTU dell'ospite o serrate l'MSS prima di dare la colpa al server per uno stallo.
Percorrete quella lista e fermatevi al primo guasto. È quello lo strato da correggere; qualunque cosa aggiunta più in basso nasconde solo il confine che si rompe.