De tunnel komt op, het sleutelpictogram verschijnt, en elke aanvraag in de browser verloopt nog steeds. In een VpnService-app betekent die combinatie meestal dat de leeslus leeft en de uitgangshelft kapot is, niet dat de server de handshake heeft geweigerd. Android heeft de interface al aangemaakt, het app-verkeer erin gezet en uw proces een bestandsdescriptor gegeven. Alles na die descriptor is uw code, en één ontbrekende aanroep is genoeg om de hele stroom op te slokken.

Wat het systeem u geeft en wat niet

establish() retourneert een ParcelFileDescriptor voor een tun-interface. Het systeem schrijft uitgaande IP-pakketten erin en verwacht dat uw proces ze leest, beslist waar ze heen gaan en ze via een gewone socket verstuurt. Eronder zit geen kernel-doorsturing, dus wanneer niemand leest worden pakketten weggegooid en de tellers van de interface bewegen nooit.

Begin met tellen in plaats van gissen. Verhoog een teller bij elke geslaagde leesactie en log de bestemming en het protocol van de eerste pakketten. Als de teller op nul blijft terwijl er verkeer wordt gegenereerd, zit de fout boven uw doorstuurcode, wat het zoeken beperkt tot de builder-configuratie en de leesmodus. Als de teller beweegt en de browser toch faalt, zit de fout in het verzend- of het terugpad.

Blokkerend lezen bepaalt of de lus beweegt

setBlocking(true) zet de descriptor in blokkerende modus en bestaat sinds API 21. Op oudere releases is de oplossing een kleine JNI-helper die O_NONBLOCK wist met fcntl op de ruwe descriptor. De modus is belangrijk omdat hij bepaalt wat een leesactie zonder wachtend pakket mag doen.

In niet-blokkerende modus keert die leesactie onmiddellijk terug met een negatieve waarde, en een lus die het resultaat negeert draait een core zonder verkeer te verplaatsen. In blokkerende modus wacht het lezen, wat een doorstuurbundel wil, maar de thread kan dan niet meer worden onderbroken. De descriptor sluiten is de ondersteunde manier om het lezen te laten mislukken zodat de lus kan stoppen. Het proces doden of de thread onderbreken kan de volgende start achterlaten met een descriptor die niet meer bij de interface past.

Lees één pakket per aanroep in een buffer ter grootte van de MTU van de interface, en behandel nul of een negatieve retour als fout, niet als leeg pakket.

protect() vóór de eerste verzending, niet erna

Elke socket die een ingekapseld pakket draagt, moet eerst worden beschermd. Een onbeschermde socket volgt dezelfde routeringsregels die u net hebt ingesteld, dus de pakketten gaan terug de tunnel in en uw eigen leeslus pakt ze weer op. De tunnel stuurt dan zijn eigen uitvoer eindeloos door, en van buitenaf lijkt het op een verbinding die opent en nooit antwoordt.

De volgorde is protect, dan connect of bind, dan send. protect aanroepen na connect kan de eerdere routebeslissing laten staan, waardoor de socket het verkeerde pad blijft gebruiken ook al gaf de aanroep true terug. Controleer de geretourneerde boolean en faal luid wanneer die false is, in plaats van een verzending in te gaan die niet kan werken.

Een permissiefout van sendto op een socket die protect() nooit heeft aangeraakt, is de gebruikelijke signatuur van deze fout.

Het UDP-voorbeeld draagt geen HTTPS

Het doorstuurvoorbeeld dat de meeste tutorials kopiëren behandelt UDP en niets anders. TCP heeft een handshake, volgnummers, bevestigingen en herverzending nodig, en een lus die payloads kopieert tussen een tun-descriptor en een socket levert daar niets van. Daarom kan een UDP-DNS-query doorkomen terwijl elk HTTPS-verzoek vastloopt. Draai een TCP-stack in userspace of geef verbindingen aan een lokale proxy die TCP op het apparaat beëindigt en de stroom doorstuurt.

Test de splitsing voordat u iets herschrijft: stuur één UDP-query en open één TCP-verbinding naar dezelfde host. Als het UDP-pad werkt en het TCP-pad bij de eerste SYN blijft hangen, is de doorstuurcode het probleem en niet de server. Die ene vergelijking bespaart veel tijd die wordt besteed aan het lezen van serverlogs zonder nuttige inhoud.

De routelijst en app-filters beperken wat u ziet

addAddress, addRoute en addDns bepalen wat het systeem de tunnel in stuurt. Een bestemming buiten de routelijst bereikt uw descriptor nooit, dus geen enkele hoeveelheid doorstuurlopica lost dat op. Log de builder-aanroepen die u doet en vergelijk ze met de adressen die de app echt probeert te bereiken.

Selectie per app heeft zijn eigen ondergrens: addAllowedApplication en addDisallowedApplication vereisen API 21, en op oudere releases ontbreken die methoden simpelweg. Een ontwerp dat op pakket filtert kan daar helemaal niet draaien.

Opnieuw opzetten laat verouderde descriptors achter

Een nieuwe aanroep van establish() retourneert een nieuwe descriptor en sluit de vorige interface. De oude descriptor is nu ongeldig, dus een thread die daar nog op blokkeert krijgt een fout, en sockets voor het oude pad moeten mogelijk worden vervangen. onRevoke is de callback voor een tunnel die de gebruiker of het systeem heeft weggenomen. De levenscyclus bij één eigenaar houden, die de lezer stopt voordat opnieuw wordt opgezet, voorkomt de meeste verbonden-maar-dode toestanden.

Lees elke fout volgens de stap waarbij hij hoort. Een verbindingsreset van recvfrom betekent dat de externe kant sloot terwijl uw code nog las. Een fout over een slecht adres wijst op header-parsing of een offset-bug, niet op het netwerk.

Werkvolgorde: blokkerende modus aan, één pakket per lezing, header geparseerd, socket beschermd, payload verzonden, schoon sluiten bij overdracht en een routelijst die de bestemming dekt.

Gerelateerde artikelen