Eén programma aan een VPN koppelen op Linux is vooral een routeringsprobleem, en de firewallregel beslist alleen wat er gebeurt nadat de route is gekozen. Als je de UID van het programma in OUTPUT matcht voordat de tunnel de standaardroute bezit, laat de regel juist het verkeer vallen dat je wilde beschermen. Dezelfde regel ziet ook het resolverproces niet dat de DNS-query's van het programma beantwoordt. Zet de volgorde goed en één account zonder rechten wordt een betrouwbare kiezer voor de gesplitste tunnel.

Geef het account een route vóór een oordeel

De eigenaar-match beantwoordt één vraag: heeft een lokale socket die bij deze UID hoort het pakket gebouwd? Hij kiest geen interface en kan verkeer niet verplaatsen dat de hoofdtabel al via de fysieke NIC verstuurt. Daarom blokkeert een UID-regel zonder bijpassende route het programma in plaats van het om te leiden.

Stuur het account eerst door zijn eigen tabel en laat de firewall daarna het resultaat bevestigen. Dit werkt op een host waarvan iproute2 uidrange begrijpt, wat ip rule help in één regel meldt. Tabel 100 houdt een standaardroute door de tunnel, en de regel stuurt alleen UID 1000 naar die tabel. Al het andere blijft op de hoofdtabel en blijft de fysieke verbinding gebruiken.

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

Een tweede account dat een andere tunnel nodig heeft, krijgt tabel 101 en zijn eigen regel, omdat twee profielen niet beide de standaardroute in dezelfde tabel kunnen bezitten. NetworkManager-profielen zijn systeembreed, dus dit deel blijft handmatig, en de regels verdwijnen bij een herstart tenzij iets ze herstelt. Schrijf de tabellen in een opstartscript naast de tunneldefinitie, zodat een herverbinding één account niet op het verkeerde pad achterlaat.

Waarom de loopback-uitzondering de tunnel doodt

De meeste gekopieerde versies van de regel laten elk pakket vallen dat niet via loopback naar buiten gaat. De tunnel is geen loopback, dus die vorm laat ook het tunnelverkeer vallen, en het resultaat lijkt een dode VPN in plaats van een firewallfout.

Wanneer OUTPUT draait, heeft de kernel al een uitgaande interface gekozen, dus -o wordt vergeleken met het antwoord van de route en niet met de pakketkop. De eigenaar-match wordt alleen in OUTPUT en POSTROUTING geraadpleegd, en doorgestuurde pakketten dragen geen socket die hij kan lezen. Die combinatie betekent dat de uitzondering de tunnelinterface moet noemen en die match moet ontkennen, in plaats van loopback te ontkennen.

Loopback verdient nog steeds zijn eigen ACCEPT boven de DROP, omdat het eerste DNS-pakket van het programma via lo naar de lokale stub gaat. Zet de twee regels in deze volgorde, en het account bereikt de stub, dan de tunnel, en niets anders.

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 is een aparte regelset, en een paar alleen voor v4 laat ip6tables onaangeroerd. Een programma dat AAAA-records verkiest, zou dan via de fysieke verbinding naar buiten gaan zonder ooit de DROP te bereiken. Spiegel beide regels in ip6tables, of schakel IPv6 op de host uit wanneer de tunnel geen v6-adres draagt. Regels die met de hand zijn toegevoegd verdwijnen ook bij een herstart, dus sla ze op met netfilter-persistent save of bouw dezelfde logica opnieuw als een nftables-tabel die door de opstarteenheid wordt geladen.

DNS-antwoorden komen van een ander proces

De query van het programma naar 127.0.0.53 passeert de loopback-ACCEPT, en daar stopt de UID-match. De stub-resolver stuurt omhoog door vanuit zijn eigen serviceaccount, dus een regel die aan vpnapp is gekoppeld ziet de tweede query nooit. Als de resolver een LAN-server voor de verbinding kiest, komen de antwoorden terug via de fysieke NIC terwijl de pakketten van het programma zelf in de tunnel blijven. Het zichtbare resultaat is een werkende tunnel met kapotte namen, of namen die worden beantwoord door een server die je niet hebt gekozen.

Repareer de naamresolutie op de tunnelverbinding, niet in de toepassing. Geef het tunnelprofiel een DNS-server en een routeringsdomein, en bevestig dan met resolvectl query en resolvectl status tun0 dat de resolver die gebruikt voor de namen die ertoe doen. Binnen een netwerknaamruimte verdwijnt het probleem, omdat de resolver van de ruimte alleen de tunnel kent.

Controleer de regel vanaf het account zelf

Vraag de kernel welke route een UID krijgt, doe dan een verzoek als dat account en vergelijk het uitgaande adres met dat van je eigen account. Als beide hetzelfde adres melden, matcht de regel iets anders dan het verkeer dat je hebt getest. De eerste opdracht hieronder toont de routeopzoeking zonder een pakket te sturen, wat een routeringsfout van een firewallfout scheidt.

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

Tellers tonen of een regel ooit is afgegaan. Voer iptables -vnL OUTPUT uit, lees het pakketaantal naast elke regel, en behandel een nul naast de DROP na echt verkeer als een matchingfout.

Doorgestuurd verkeer valt buiten dit ontwerp. Een container of VM achter een brug heeft geen lokale socket die eigendom is van vpnapp, dus de eigenaar-match gaat nooit af en die pakketten volgen in plaats daarvan het FORWARD-beleid. Als die werklast moet worden gedekt, test die apart en schrijf een beleid dat aan de brug of het containeradres is gekoppeld. Het ontwerp houdt stand wanneer de tabel, de interface-uitzondering, de resolver en de v6-regels allemaal dezelfde kant op wijzen; laat een van hen weg en het account lekt of verliest de verbinding.

Gerelateerde artikelen