De telefoon start opnieuw, always-on staat aan, «verbindingen zonder VPN blokkeren» staat aan, en de tunnel komt nooit op. Niets is verkeerd geconfigureerd: de blokkade doet zijn werk, en de poging van de client zelf om zijn endpoint te bereiken is er ook aan onderworpen. De endpointnaam moet worden opgelost voordat de tunnel bestaat, door een client die met het netwerk mag praten terwijl alle andere apps dat niet mogen. Begrijpen welk onderdeel dat doet, en in welke volgorde, is wat dit mysterie in een configuratievraag verandert.

Wat de VPN-builder feitelijk vraagt

Een Android VPN-app geeft het platform geen serveradres. Hij bouwt een lokale interface: adressen en routes voor de tunnel, de resolvers die apps erbinnen moeten gebruiken, en dan geeft establish() een bestandsdescriptor terug die de app leest en schrijft als ruwe IP-pakketten. Het externe endpoint komt in die builder helemaal niet voor.

Dat is het belangrijkste structurele feit. Het platform maakt een tun-interface en handhaaft beleid daarop; de app implementeert het transport in gebruikersruimte, wat betekent dat de app zelf de endpointnaam oplost en de versleutelde verbinding opent. Een endpoint als hostnaam is dus mogelijk — niet omdat het platform namen ondersteunt, maar omdat het platform het endpoint nooit ziet.

Eén veld in de builder neemt wel adressen aan: de DNS-servers. Die moeten numerieke resolveradressen zijn, meestal degene die de operator via de tunnel wil laten bevragen. Dat veld beschrijft waar apps hun query's naartoe sturen nadat de tunnel op is. Het zegt niets over hoe de client het endpoint vindt, en de twee als dezelfde instelling behandelen is waar de verwarring begint.

Twee DNS-vragen, niet één

De eerste vraag is: waar sturen apps hun query's naartoe als de tunnel eenmaal op is? Het antwoord is een resolveradres binnen de tunnel, bereikbaar via de tun-interface, en per constructie numeriek.

De tweede vraag is: hoe vindt de client zijn eigen endpoint voordat die tunnel bestaat? Die opzoeking gebeurt buiten de tunnel, op het onderliggende netwerk, in het proces van de client. Hij mag een naam gebruiken, en hij moet een resolver gebruiken die het apparaat zonder VPN kan bereiken.

StageWho resolvesPathFailure looks like
Endpoint lookupThe VPN appUnderlying network, protected socketNever connects; no tunnel interface traffic
Peer authenticationClient and serverTunnel transportConnects then drops; certificate or key error
Private name lookupApps via the tunnelResolver inside the tunTunnel up, internal sites do not resolve

Drie fasen, drie verschillende symptomen. Eén foutmelding die zegt «kan niet verbinden» plakt ze aan elkaar, en daarom gaan mensen het DNS-serverveld bewerken om een probleem op te lossen dat optreedt voordat dat veld ooit wordt geraadpleegd.

Waarom de bootstrap kan vastlopen

Always-on met de blokkade aan betekent dat verkeer dat niet in de VPN zit, wordt weggegooid, inclusief verkeer van de app die de VPN bezit. De ontsnapping is expliciet: de app roept protect() aan op een socket, wat die aan het onderliggende netwerk bindt zodat hij de tunnel omzeilt en naar buiten mag. Elke VPN-client moet dit voor zijn eigen transport doen, anders bereikt hij nooit een server.

Overweeg nu een uitrol waarin de enige resolver die de endpointnaam kent, zelf in de tunnel zit. De client heeft de tunnel nodig om de naam op te lossen, en de naam om de tunnel te bouwen. Dat is een echte cirkelafhankelijkheid, en geen enkele clientconfiguratie lost die op. Het is een ontwerpfout, geen bug.

Timing legt dezelfde afhankelijkheid in mildere vormen bloot. Bij het opstarten vóór de eerste ontgrendeling heeft het apparaat misschien nog geen bruikbaar netwerk; op een captive portal bestaat het netwerk wel, maar blokkeert het alles behalve het portaal; bij een overgang van Wi-Fi naar mobiel verandert het onderliggende netwerk van de beschermde socket en moet de client dat opmerken en opnieuw oplossen. Elk daarvan is een bootstrap-gebeurtenis, en elk is het waard om bewust te testen in plaats van om 3 uur 's nachts te ontdekken.

Waarom het antwoord vastpinnen geen oplossing is

De verleidelijke omweg is de naam één keer oplossen en het resulterende adres in een numeriek veld plakken, of het in een eigen build hardcoderen. Het lijkt te werken, en het verandert een naam in een momentopname. Wanneer de operator het endpoint verplaatst — een migratie, een anycast-wijziging, een nieuw regionaal adres — blijft de client het oude bellen en niets in de interface legt uit waarom.

DNS-records hebben een time to live juist zodat antwoorden kunnen veranderen; caching is gedefinieerd in termen van die levensduur, niet voor altijd. Een antwoord vastpinnen gooit het mechanisme weg dat een naam beter maakt dan een adres, terwijl de operationele kosten van het gebruik van een naam blijven.

Vastpinnen gaat vaak samen met een tweede slechte gewoonte: identiteitscontroles versoepelen omdat de verbinding nu anders faalt. Verificatie van de serveridentiteit hoort niet bij het bootstrap-probleem, en die verzwakken ruilt een DNS-ongemak in voor een afluisterrisico.

Ontwerpen die een herstart overleven

De duurzame opzet is weinig glamoureus. Gebruik een client die beschermde sockets correct implementeert en zijn endpoint op het onderliggende netwerk oplost, en geef hem een endpointnaam die publiek oplost, of via een resolver die vóór de tunnel bereikbaar is. Laat de blokkade aan; die helemaal uitschakelen om DNS te laten werken haalt de eigenschap weg waarvoor de uitrol bestaat.

Waar het endpoint privé moet blijven, bied een expliciet bootstrap-pad dat het platform kan gebruiken — een resolver die zonder de tunnel bereikbaar is, of een beheerverbinding die de operator onderhoudt — en houd die uitzondering smal en eigendom van beleid, niet van een gebruikersschakelaar. De Android VpnService-documentatie is de referentie voor wat het platform garandeert over bescherming en levenscyclus, en de VpnService.Builder-referentie bevestigt dat het DNS-veld numerieke adressen aanneemt.

Verifieer daarna de drie fasen, in volgorde, op een nieuwe verbinding na een herstart: de endpointnaam wordt opgelost, de tunnel komt tot stand en de peer authenticeert, en een privénaam wordt via de resolver van de tunnel opgelost. Als de eerste fase faalt, kan niets daarna werken, en de oplossing hoort in het bootstrap-pad — nooit in het resolverveld.

  • Het oplossen van het endpoint gebeurt in de app, buiten de tunnel, op een beschermde socket.
  • Het DNS-veld in de builder is voor apps binnen de tunnel, niet voor het eigen endpoint van de client.
  • Een resolver die alleen via de tunnel bereikbaar is, is een doodlopende weg, geen trage start.
  • Test endpoint, authenticatie en privé-opzoeking apart na een herstart.

Gerelateerde artikelen