Waarom Android VpnService een deel van het verkeer opvangt en niet alles
Routes, DNS-entries en de reikwijdte per app bepalen welke pakketten het tun-apparaat bereiken, waarom lokale apparaten verdwijnen en wat de bouwer weigert.
Niets in VpnService onderschept verkeer uit zichzelf. Het vraagt het systeem om een tun-interface, geeft die interface een reeks adressen en routes, en vanaf dat moment kopieert de kernel elk pakket dat bij die routes past naar een bestandsdescriptor die de app kan lezen. De code van de app ziet nooit de pakketten die de routes niet hebben geselecteerd, dus de reikwijdte van een tunnel is een vraag over de routetabel voordat het een programmeervraag is. Daarom kan dezelfde service-implementatie op de ene telefoon elke stroom opslokken en op de andere slechts een deel, en daarom repareert het toevoegen van een route soms een tunnel die kapot leek. Wat je krijgt is wat je hebt gedeclareerd, min alles wat de bouwer stilzwijgend weigerde.
Routes bepalen welke pakketten aankomen
Builder.addRoute(prefix, prefixLength) installeert een prefix in de routetabel van het VPN-netwerk. Het systeem behandelt dat netwerk als standaard voor de apps binnen de reikwijdte, dus de entries worden geraadpleegd vóór de standaardroute van het fysieke netwerk en een passende prefix volstaat om een stroom om te leiden. Daarom neemt een route voor 0.0.0.0/0 IPv4 volledig over, inclusief de printer, het cast-doel en de beheerpagina van de router.
Clients die die dekking willen zonder de fysieke standaardentry te verwijderen, gebruiken in plaats daarvan het paar 0.0.0.0/1 en 128.0.0.0/1. Twee specifiekere routes verslaan een standaardroute, samen beslaan ze de hele adresruimte, en de eigen standaardentry van de telefoon blijft in de tabel voor de sockets die nog aan het fysieke netwerk gebonden zijn.
Adressen en DNS-entries komen van dezelfde bouwer en beantwoorden een andere vraag. addAddress geeft de interface een adres met een prefixlengte, en addDnsServer installeert een resolver die apps binnen de reikwijdte via de interface zullen bevragen. Beide families kunnen op één interface zitten, dus een set routes met alleen IPv4 naast een IPv6-adres is een configuratie die correct leest en toch lekt. Als je een familie routeert waarin de interface geen adres heeft, heeft de kernel geen bronadres voor die pakketten en ze verschijnen nooit bij de descriptor.
Wat establish() teruggeeft
establish() geeft een ParcelFileDescriptor terug, of null wanneer de app niet gemachtigd is. Null is een toestemmingstoestand en geen netwerkfout: VpnService.prepare() moet al null hebben teruggegeven, of de gebruiker moet de systeemdialoog vóór de aanroep hebben goedgekeurd. Toestemming kan worden ingetrokken terwijl de service draait, en de interface sluit onder je voeten zonder enige callback naar je leeslus.
De descriptor is niet-blokkerend tenzij je anders zegt, dus een leesactie komt meteen terug zonder gegevens in plaats van de thread te parkeren. Een lus die voor een blokkerende socket is geschreven, brandt een core, en een lus die een lege leesactie als fataal behandelt, scheurt de tunnel in een idle seconde. Builder.setBlocking(true) verandert dat gedrag, en de prijs is een thread die binnen de leesactie wacht.
Route- en adresargumenten worden gevalideerd als LinkAddress-waarden, en multicast-bestemmingen zoals 224.0.0.0/4 worden botweg geweigerd. Een punt-naar-punt tun-interface heeft ook geen broadcast-domein, dus multicast-ontdekking eroverheen vraagt een ander ontwerp in plaats van nog een route. De VpnService.Builder-referentie is de lijst die je moet controleren voordat je concludeert dat een aanroep is genegeerd.
De reikwijdte per app vernauwt de set, niet de tunnel
Vanaf API 21 accepteert de bouwer addAllowedApplication en addDisallowedApplication, en de twee lijsten sluiten elkaar uit binnen één bouwer. Een toelatingslijst laat de tunnel bestaan voor die pakketten terwijl al het andere het fysieke netwerk houdt; een blokkeerlijst vangt alles behalve de genoemde pakketten, wat een client als gesplitst tunnelen aanbiedt. Vóór API 21 is er helemaal geen platformmechanisme, en gedrag per app kwam van externe clients die op UID filterden binnen hun eigen doorstuurder. Het verschil is belangrijk bij het debuggen, want een tunnel per app met een toelatingslijst van één pakket levert bijna geen verkeer op en dat is correct gedrag, geen kapotte route.
Wat de reikwijdte ook is, de socket die je app naar de VPN-server opent, mag niet door de tunnel reizen die hij bouwt. VpnService.protect(socket) bindt die socket aan het onderliggende netwerk zodat zijn pakketten het tun-apparaat overslaan. Als je de aanroep overslaat, wordt de handshake opgeslokt door de interface die hem had moeten opzetten.
VpnService.setUnderlyingNetworks sluit de lus door het systeem te vertellen welk fysiek netwerk de tunnel draagt. Die waarde bepaalt hoe de VPN connectiviteit aan andere apps meldt, en een null- of verouderde waarde is een veelvoorkomende oorzaak van een verbonden tunnel die de rest van de telefoon als offline behandelt. Werk hem bij wanneer het onderliggende netwerk verandert, niet alleen bij het opstarten.
Checksums en MTU: waar een correcte route toch faalt
Zodra pakketten aankomen, bepalen twee details op pakketniveau of iets van begin tot eind werkt. Het eerste is het bronadres: bij meerdere interfaceadressen kan de kernel pakketten sturen die het eerste dragen, dus een doorstuurder die headers herschrijft, moet het adres en de checksum die het dekt corrigeren.
Het tweede is de reikwijdte van de checksum. Wanneer de app zelf TCP- of UDP-pakketten bouwt, dekt de checksum een pseudo-header met het bronadres, het bestemmingsadres, het protocolnummer en de transportlengte, en een daarvan weglaten levert pakketten op die aankomen en worden weggegooid. Die fout lijkt op een time-out na een antwoord dat nooit komt, wat de meesten terug naar de routes stuurt. setMtu legt de MTU van de interface vast, en elke leesactie geeft precies één volledig IP-pakket terug, dus een buffer die voor een bytestroom is geschreven, fragmenteert je parsing en niet het pakket.
Een verificatieronde die de lagen scheidt
Om te vinden waar de reikwijdte misging, test je de lagen op volgorde. Vraag het apparaat welke routes het VPN-netwerk werkelijk heeft, controleer dan of een stroom naar een bekend adres als een leesactie bij de descriptor verschijnt, en controleer dan of het pakket dat je doorstuurder uitzendt goed gevormd is. Een routeprobleem toont niets bij de descriptor, terwijl een checksumprobleem een pakket toont dat aankomt en verdwijnt.
Als niets de descriptor bereikt, is de routset de verdachte en niet de lezer: bevestig de prefixes, bevestig dat er een adres in dezelfde familie is, en bevestig dat de app binnen de reikwijdte valt die je hebt ingesteld. Als pakketten de descriptor bereiken maar er niets terugkomt, inspecteer dan protect() en de waarde van het onderliggende netwerk voordat je weer een route aanraakt. Als de verre kant melding maakt van misvormde pakketten, zijn de checksum en de MTU de plekken om te kijken. Eén sonde per laag bespaart een middag gissen.