Il container non raggiunge l’API riservata alla VPN? Fai un proxy sull’host
Il tunnel vive sull’host, quindi il container non ha una rotta verso l’indirizzo privato. Pubblica un proxy inverso stretto e chiamalo come host.docker.internal.
Il tuo servizio Compose va in timeout contro un’API che il browser sulla stessa macchina apre senza lamentarsi. Questa differenza è l’indizio: la richiesta che funziona parte da un host con un tunnel VPN, e quella che si blocca parte da un container con la propria pila di rete. Nulla dentro il container conosce le rotte del tunnel, quindi la destinazione privata resta irraggiungibile comunque sia scritto il nome. La soluzione è un salto deliberato attraverso l’host, non una tabella di routing più astuta.
Parti dal perché i due percorsi differiscono, poi decidi dove deve vivere il tunnel.
Perché l’host funziona e il container no
Alzare il tunnel dà all’host un’interfaccia, un insieme di rotte e, di solito, una voce di resolver fornita dalla VPN. Un container non eredita nulla di tutto ciò: parte con un’interfaccia sul bridge del motore e una rotta predefinita che punta a quel bridge. Le rotte installate sull’host sono invisibili a tutto ciò che gira dentro quello spazio dei nomi.
Per questo i sintomi sembrano incoerenti e non semplicemente rotti. L’host risolve il nome privato e si connette, perché sia il nome sia la rotta esistono lì. Il container invia il suo pacchetto a un indirizzo privato che nulla sul percorso del bridge può raggiungere, e la richiesta va in timeout. Aggiungere il nome al file hosts del container non cambia nulla, perché l’indirizzo stesso è irraggiungibile da dove si trova il container.
Raggiungi l’host dall’interno del container
Un processo nel container che compone localhost sta componendo il container. Docker Desktop pubblica host.docker.internal come un nome che punta alla macchina che esegue il motore; i motori Linux lo lasciano indefinito a meno che il servizio lo richieda, ed è ciò che fa il mapping qui sotto. Con questo, l’applicazione punta a un nome e a una porta invece che all’indirizzo privato.
services:
app:
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
INTERNAL_API_HOST: "host.docker.internal:8666"Il tunnel resta sul lato host del salto, e la porta in quella variabile è quella su cui il proxy resterà in ascolto. Un container che perde il tunnel ora fallisce a un solo salto noto invece di tentare una rotta che non ha mai avuto.
Scegli dove vive il tunnel
Tre forme sono possibili per un servizio Compose che ha bisogno di un’API privata, e si distinguono per quale processo possiede il tunnel. Un client VPN può girare nell’immagine del container, il che richiede di esporre un dispositivo di tunnel e di concedere la capacità di amministrazione di rete che lo accompagna. L’applicazione può condividere lo spazio dei nomi di rete di un container di tunnel separato, così entrambi i processi vedono una pila e una tabella di routing. Oppure il tunnel può restare sull’host, con un ristretto inoltro dall’host al container.
Le prime due trascinano file di profilo, comportamento di riconnessione e politica del resolver dentro il progetto, e rendono ogni servizio del file dipendente dalla salute del tunnel. È troppo accoppiamento per una sola chiamata API. Per una pila di sviluppo che legge un solo endpoint interno, l’inoltro lato host è la modifica più piccola che comunque tiene le credenziali dove le gestisce il team di piattaforma.
Pubblica un solo percorso stretto sull’host
Parti da ciò che eredita l’inoltro: eseguilo con l’account che possiede il tunnel e i suoi socket in uscita prenderanno le rotte dell’host e il resolver dell’host. Un blocco location di nginx copre un percorso.
server {
listen 8666;
location /search/ {
proxy_pass http://10.20.30.40:8090/;
proxy_set_header Host internal-api.example;
proxy_read_timeout 60s;
}
}Tratta l’indirizzo e la porta come segnaposto e inserisci la coppia su cui l’API ascolta davvero. Ogni upstream aggiuntivo merita il proprio blocco location, e ogni blocco dovrebbe ammettere solo i percorsi chiamati da questo servizio, così che il proxy non diventi una porta generale verso la rete privata. Lega l’ascoltatore all’indirizzo del bridge per i client che arrivano via host-gateway, al loopback solo quando il client condivide lo spazio dei nomi di rete dell’host, e mai a 0.0.0.0. Scegli anche una porta ancora libera: un numero già occupato lascia il proxy senza ascolto mentre il container continua ad andare in timeout, cosa indistinguibile dal guasto di rete con cui hai iniziato.
Porte, nomi e la decisione TLS
La risoluzione dei nomi si divide come il routing. Le ricerche pubbliche del container passano dal resolver del motore, quindi un nome privato che si risolve sull’host non si risolverà dentro il container. Il proxy è il componente che risolve il nome privato, perché eredita il resolver dell’host.
TLS è la decisione che viene rinviata e poi spiega il guasto successivo. Se il proxy inoltra i byte intatti, il container vede il certificato proprio dell’API e ha bisogno della CA interna nel proprio archivio di fiducia. Se il proxy termina TLS, quella fiducia passa all’host e l’applicazione inizia a costruire link dall’intestazione host che riceve. Scegli un solo assetto e applicalo a ogni risorsa, perché un reindirizzamento emesso dall’altro punta a un nome host che il container non può risolvere.
Verifica dall’interno del servizio
Invia la richiesta dal container, non dall’host: un semplice curl sul percorso del proxy, poi il controllo di salute dell’applicazione stessa. Quando il proxy risponde ma l’applicazione fallisce ancora, confronta tre cose: l’URL di base che costruisce, qualsiasi variabile proxy nel suo ambiente e la CA di cui si fida. Tutte stanno nell’ambiente e nei registri del container, e correggerne una lascia la VPN in pace.
Scrivi l’assetto scelto nel README del progetto finché è fresco. Altrimenti la prossima persona che incontrerà questo timeout passerà lo stesso pomeriggio a riscoprire che il tunnel vive sull’host.