De onjuiste aanname is dat een VpnService-app een systeemschakelaar omzet en dat de telefoon daarna beschermd is. Wat er werkelijk gebeurt, is smaller: de app vraagt toestemming, bouwt een interface, leest en schrijft pakketten via een bestandsdescriptor, en geeft de interface terug wanneer hij stopt. Er kan tegelijk maar één zo'n tunnel leven, en het systeem kan hem zonder vragen wegnemen. Het meeste verwarrende gedrag dat mensen het protocol verwijten, komt uit dat eigendomsmodel.

Behandel elke levenscyclusovergang als deel van het ontwerp en niet als een randgeval.

Mythe: de app zet de VPN van de telefoon aan

De stroom begint met VpnService.prepare(), dat een Intent teruggeeft die de gebruiker moet accepteren, en gaat verder met establish(), dat daarna alsnog kan mislukken om redenen die de app niet beheert. Een tweede tunnel opzetten deactiveert de eerste interface en vervangt de systeemmelding. Nergens in die reeks bezit de app een schakelaar voor het hele apparaat; hij bezit de descriptor die hem is gegeven en de pakketten die daarop landen. Zelfs de blijvende melding hoort bij het systeem en niet bij het ontwerp van de app.

Omdat de interface wordt gegeven en niet gegrepen, kan een andere client de plaats op elk moment innemen. De app leert dat via een callback, niet via een retourwaarde.

Dat de app geforceerd is gestopt, is om dezelfde reden zichtbaar in het meldingenpaneel: het systeem meldt wat het heeft gegeven, niet wat de app koos te tonen.

Mythe: toestemming wordt één keer gegeven

Wanneer het systeem de tunnel aan een andere client overdraagt, stuurt het een intrekkingsstransactie op de binder die de basisklasse uit onBind() teruggeeft. Een app die die binder door zijn eigen vervangt, moet de intrekkingsstransactie zelf doorsturen, anders wordt VpnService.onRevoke() nooit geactiveerd en blijft de dienst schrijven naar een descriptor die niet meer bestaat. De goedkeuring kan ook door de gebruiker worden ingetrokken, en het systeem kan de dienst stoppen tijdens een herstart of een altijd-aan-herverbinding.

Dat afhandelen is concreet werk en geen logregel: stop met het lezen van de descriptor, sluit hem, wis de verbonden-vlag en vertel de gebruiker in welke toestand hij verkeert. Beslis daarna of de app opnieuw moet opzetten, opnieuw moet vragen of moet wachten, want in een altijd-aan-configuratie kan het systeem de dienst zelf terugbrengen.

Een app die intrekking als een uitzondering behandelt en niet als een toestand, meldt verbonden terwijl elk pakket dat hij schrijft wordt weggegooid, en dat is de slechtst mogelijke faalmodus voor een privacyhulpmiddel.

Mythe: de interface filtert het verkeer voor u

Wat binnenkomt, is ruwe IP. Onderscheppen of filteren betekent IP-, TCP-, UDP- en IPv6-headers parsen, stromen volgen en antwoorden terugschrijven naar de descriptor, wat een onderscheppingsapp tot een kleine netwerkstack maakt en niet tot een systeemvoorziening. protect() bestaat zodat de eigen transportsocket van de app zijn tunnel omzeilt in plaats van zichzelf te onderscheppen, en het geeft een boolean terug die een controle verdient op elke socket die wordt gemaakt nadat de tunnel staat.

De lijsten met toegestane en verboden apps van de builder bepalen wiens verkeer de interface binnenkomt. Ze geven geen toegang tot payloads voorbij de IP-pakketten die de interface draagt, en geen enkele API legt de tunnel van een andere client bloot of somt de VPN-profielen van het systeem op. Netwerkmogelijkheden kunnen zeggen dat een netwerk VPN-transport is, maar niet van wie de tunnel is.

Daarom krijgen statusvragen over andere apps tegenstrijdige antwoorden: de informatie wordt simpelweg niet aan uw proces blootgelegd.

Wat de levenscyclus werkelijk vereist

Volg de toestanden die u beheert: voorbereid, opgezet, ingetrokken, gestopt. Herverbinden zou moeten zijn: afbreken en daarna opzetten, met één eigenaar voor de descriptor en de transportsocket, want twee componenten die om het opzetten wedijveren, vechten om de enige beschikbare plaats. Achtergrondstarts zijn beperkt op modern Android, dus een boot-broadcast is geen garantie; gebruik een voorgronddienst met zijn melding waar het API-niveau het toelaat.

Log de overgang en niet alleen de eindtoestand, en houd de toestandsmachine klein genoeg om elk pad te testen. Een toestand die u in een test niet kunt bereiken, is een toestand die u in het veld zonder log tegenkomt.

Test de levenscyclus op een apparaat

Emulators kunnen connectiviteit nabootsen maar niet overtuigend de toestemmings- en intrekkingsroute, dus voer deze uit op een echt apparaat met een tweede VPN-client geïnstalleerd om de plaats te stelen.

adb shell dumpsys connectivity | grep -i -A3 vpn
adb shell dumpsys activity services | grep -i -A2 vpnservice
adb logcat -s VpnService:V AndroidRuntime:E
adb shell am force-stop com.example.vpnapp

Start de tunnel, bevestig de melding en de interface, verbind dan de andere client en kijk of de intrekkingscallback loopt en de toestand wordt gewist. Stop de app geforceerd en controleer of iets nog een levende descriptor meldt.

Herhaal de reeks na een altijd-aan-herstart, want dat pad verschilt van een door de gebruiker gestarte verbinding. Wat u zoekt is geen groen pictogram maar bewijs dat de app stopte met het gebruik van een descriptor die hij niet meer bezat.

Gerelateerde artikelen