Eén app via een VPN routeren zonder de hele machine
Geen onbeheerde desktop biedt een VPN-schakelaar per proces. Kies de laag die verkeer selecteert: een gebonden socket, een UID-regel, een firewallprofiel of een SOCKS-hop.
De VPN-client verbindt, de browser werkt, en het ene commando dat je echt belangrijk vindt gaat nog steeds via je thuisverbinding naar buiten. Of het tegenovergestelde gebeurt: de tunnel grijpt de standaardroute en de lokale fileserver verdwijnt van de desktop. Ik kwam dit tegen bij een build-agent die een interne artefactspiegel moest bereiken terwijl de rest van de host op het kantoor-LAN bleef, en de client bood precies twee keuzes, al het verkeer of niets. De vraag is in beide richtingen dezelfde, dit programma en niet de hele machine, en de meeste clients hebben daarvoor geen selectievakje, omdat de routetabel nooit heeft geweten wat een programma is.
Waarom de routetabel je programma niet kan zien
Wanneer een proces een pakket verstuurt, zoekt de stack het bestemmingsadres op in de routetabel. Noch de naam van het uitvoerbare bestand noch de poort komt die opzoeking in. Domeinnamen falen om dezelfde reden: een tunnel draagt verkeer naar een adres, en wanneer een naam naar een adres is omgezet, is routering de enige hefboom die de kernel nog heeft. Daarom betekent “VPN alleen voor deze site” uiteindelijk een proxy, en “VPN alleen voor dit programma” een van een kleine reeks mechanismen.
| 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 |
Geen van deze is een route per proces. Elk verplaatst de beslissing naar een laag die het programma, de gebruiker of de eigen configuratie van de app kan zien.
Eén bestemming is nog steeds een route, geen programmaregel. Op macOS is het gebruikelijke recept het vinkje bij “Al het verkeer via VPN verzenden” uit te zetten en een hostroute toe te voegen: sudo route add -host internal-tuner.example -interface ppp0, weer verwijderd met sudo route delete -host internal-tuner.example -interface ppp0. Lees eerst de interfacenaam uit ifconfig, want ppp0 is alleen een voorbeeld. Jokertekens werken niet in een hostroute, dus een dienst die met meerdere namen spreekt heeft één vermelding per naam nodig, en de regel dekt elk proces op de machine, niet degene die je in gedachten had.
Bind de socket aan het tunneladres
Als je de client beheerst, is binding de goedkoopste selector. Een socket die aan het lokale adres van de tunnel is gebonden gaat via die interface naar buiten zolang daar een route voor de bestemming bestaat, en alles anders op de machine houdt zijn normale pad. De subtiliteit is dat de binding het bronadres kiest terwijl de routetabel nog steeds de uitgang kiest. Een binding alleen verplaatst geen verkeer dat geen route in de tunnel heeft, en dat is de fout achter de meeste meldingen “ik heb het gebonden en het gaat nog steeds de verkeerde kant uit”.
import socket
s = socket.socket()
s.bind(("10.8.0.2", 0))
s.connect(("build.internal.example", 443))Hogere tools tonen hetzelfde idee: curl --interface 10.8.0.2, PHP's CURLOPT_INTERFACE, en het source_address-argument in Pythons HTTP-clients. Het adres moet bij de tunnel horen, en een typefout hier faalt als een gewone verbindingsfout, niet als een routeringsklacht. Aan een apparaatnaam binden in plaats van een adres is een Linux-voorrecht en gaat niet over naar Windows of macOS, daarom geven draagbare scripts het adres mee.
Linux: koppel aan de eigenaar, niet de programmanaam
De kernel kan de eigenaar-gebruiker van een socket matchen, dus een toegewezen account wordt de selector.
ip rule add uidrange 1001-1001 lookup 100
ip route add default dev tun0 table 100Dit werkt alleen als de tunnel niet al de standaardroute is. Start de client met --route-nopull, of filter de opgeduwde omleiding met --pull-filter ignore "redirect-gateway" op OpenVPN 2.4 en later, en voeg daarna zelf de tabelvermelding toe. Anders bestaan beide standaarden en de lagere prioriteit wint. De bewuste versie daarvan is de standaardroute naar de tunnelgateway te richten, of dat vanuit een disconnect-script te doen: het houdt verkeer van internet af wanneer de tunnel valt, maar dat is een kill switch, geen selector, en die blijft tot iemand hem verwijdert.
De sterkere versie is een netwerknaamruimte. Maak die aan, voer de client daarbinnen uit, en start de toepassing met ip netns exec. Niets buiten de naamruimte ziet de tunnelinterface, dus de toepassing kan niet per ongeluk terugvallen op de hostroute. De prijs is echt: de naamruimte heeft een veth-paar nodig om het fysieke netwerk te bereiken en zijn eigen resolverbestand, omdat de mount-naamruimte de resolutie-instellingen van de host niet deelt.
Twee faalmodi verklaren de meeste lekken. Naamresolutie gebeurt nog steeds in de resolver van de host, dus de query gaat buiten de tunnel naar buiten zelfs wanneer de TCP-verbinding naar binnen gaat. En IPv6 heeft zijn eigen ip -6 rule nodig, omdat een v4-regel die niet dekt.
Windows en macOS: beperk de firewall of geef een proxy door
Windows heeft ook geen routeselector per toepassing, maar zijn firewallregels kunnen tot een profiel worden beperkt, en een VPN-adapter kan als Openbaar worden geclassificeerd terwijl het thuis-LAN Privé blijft. Maak uitgaande blokkeerregels voor het programma op de profielen Privé en Domein, laat Openbaar open, en de toepassing kan het netwerk alleen via de tunnel bereiken. Stel eerst de netwerkcategorie van de verbinding in, bevestig daarna dat de regel overeenkwam in het controlevenster van de firewall. Het is een blokkade, geen route: het faalt gesloten wanneer de tunnel valt, en het kan een toepassing die de firewall negeert niet stoppen.
macOS volgt dezelfde logica met andere hulpmiddelen. Een SOCKS-luisteraar die via SSH bereikt wordt is de minst ingrijpende optie, want ssh -D 1080 user@host maakt één lokale poort en alleen toepassingen die erop zijn ingesteld nemen dat pad. Een pf-regel kan een programma per gebruiker of groep blokkeren, maar kan nog steeds niet de tunnel voor dat programma alleen kiezen. Tunnelblick kan helemaal niet per domein filteren, omdat routes op adressen werken; OpenConnect-gebruikers grijpen naar vpn-slice wanneer de server een te brede prefixlijst opduwt.
Twee omwegen dekken wat de platformhulpmiddelen missen. Browserextensies die per domein van proxy wisselen zijn het praktische antwoord voor “alleen deze site”, omdat ze op de toepassingslaag werken waar namen nog bestaan; ze dekken één browser en niets anders. De botte is de leverancierclient in een virtuele machine te draaien zodat de gast de routewijziging bezit terwijl de host zijn pad houdt, ten koste van twee netwerkstacks om te onderhouden.
Een SOCKS-hop voor tools zonder bindoptie
De meeste toepassingen kunnen naar een HTTP- of SOCKS-proxy worden gericht, zelfs als ze niets van interfaces weten. Een SSH-client levert er een zonder wijziging op de serverzijde:
ssh -N -D 127.0.0.1:9999 [email protected]Alles dat is ingesteld om die poort te gebruiken, gaat bij de bastion naar buiten terwijl de rest van de desktop zijn normale verbinding houdt. Een lokale 3proxy-instantie die aan het tunneladres is gebonden doet hetzelfde voor één poort zonder SSH, en stunnel dekt één enkele TLS-ingepakte poort. De grenzen zijn praktisch: een proxy draagt alleen protocollen die weten hoe ze hem gebruiken, UDP heeft SOCKS5 UDP-associatie nodig die de “SOCKS5”-instelling van veel clients niet activeert, en DNS moet door de proxy worden opgelost (socks5h in curl) of de naam lekt naar de lokale resolver.
Houd de luisteraar op loopback, tenzij een andere machine hem nodig heeft.
Controleer het pad met een capture, niet met een statusbericht
Statusvensters van toepassingen beschrijven de instelling, niet het pakket. Vang op beide interfaces en vergelijk:
tcpdump -i any -n port 443
ss -tnp | grep 443Als de verbinding op de fysieke interface verschijnt zonder bijpassende vermelding op de tunnel, is de selector niet actief, wat de client ook meldt. Controleer IPv6 en de resolver apart, en controleer een tweede pad zoals een hulpproces of een geërfde systeemproxy.
Kies dan de grens die bij de eis past: een proxy is genoeg voor één site, een UID-regel is genoeg voor één account, een firewallprofiel is genoeg wanneer de verbinding gesloten moet falen, en een naamruimte is de enige optie die de tunnel voor alles daarbuiten verbergt.