Per què el teu túnel Android VpnService deixa d'encaminar el trànsit
Quatre creences sobre VpnService que es trenquen en producció: un túnel alhora, abast de les rutes, llistes per aplicació i ordre d'aturada, amb una comprovació per a cadascuna.
Quatre suposicions sobre Android VpnService em van costar una vegada bona part d'una setmana, i cap d'elles no era sobre el protocol de xarxa. L'aplicació deia connectat, la icona del clau seia a la barra d'estat, i el trànsit continuava sortint per la ruta normal. Jo seguia suposant que el túnel era meu fins que l'aturava, que les rutes que afegia eren les rutes que obtenia, i que cridar stopSelf s'enduria la interfície amb ell. El servei es comportava exactament com diu la documentació. La part trencada era el meu model mental.
Mite 1: el túnel es manté fins que l'atures
Android manté una interfície VPN alhora. Estableix-ne una segona i la plataforma no la posa en cua: desactiva la teva i crida onRevoke() al teu servei. Si el teu codi només desmunta en prémer un botó, una presa de control deixa un descriptor i un fil de lectura enrere, i l'arrencada següent hereta tots dos.
La referència de VpnService diu explícitament que prepare() retorna null quan l'aplicació ja té consentiment i un Intent quan l'usuari encara l'ha d'aprovar. Aquest consentiment només arriba si lliures la intenció mitjançant startActivityForResult(). Salta el lliurament i establish() retorna null, cosa que es llegeix com un fracàs silenciós i no com un problema de permisos.
Always-on VPN afegeix un propietari que no controles. El sistema pot tornar a aixecar el túnel després d'un reinici o d'una caiguda, i s'avança a una aplicació VPN que l'usuari hagi iniciat a mà. Construeix onRevoke() perquè tanqui els mateixos recursos que la teva aturada normal, i tracta'l com un camí d'aturada principal i no com un cas de cantonada. Els usuaris canvien d'aplicacions VPN; el teu codi ha de sobreviure-hi.
Mite 2: addRoute captura exactament el que has llistat
Les rutes decideixen quins paquets arriben al teu descriptor, i un error d'abast produeix el cas exasperant en què part del trànsit va pel túnel i part no. addRoute("0.0.0.0", 0) pren tot l'IPv4, cosa que significa que cada paquet ara arriba a tu i s'ha de reenviar o descartar. Un /32 per a un sol host gairebé no pren res, i tota la resta es comporta com si el teu VPN mai s'hagués instal·lat.
Les famílies es compten per separat. Una ruta només IPv4 deixa IPv6 a la xarxa subjacent, així que un host de doble pila encara pot resoldre i connectar fora del túnel mentre sembla perfectament connectat a dins.
El DNS segueix la mateixa lògica. Els servidors afegits amb addDnsServer() van a les aplicacions que fan servir el resolutor del túnel, mentre que una aplicació amb el seu propi punt final DNS xifrat o amb Private DNS fixat a un nom d'amfitrió els ignora del tot. Quan els noms fallen per a una aplicació i funcionen per a una altra, revisa el camí del resolutor abans de tocar ni una sola ruta.
Mite 3: les regles per aplicació són un interruptor que pots girar en qualsevol sentit
addDisallowedApplication() i addAllowedApplication() són dos modes, no dos filtres. El constructor accepta un model per túnel, així que una aplicació que vol tot excepte tres paquets fa servir la llista de prohibits i deixa buida la de permesos. Els dos són mútuament excloents, i la gent ho descobreix en temps d'execució després que la pantalla d'ajustos ja hagi ofert tots dos commutadors.
La visibilitat dels paquets també mossega. A Android 11 i posteriors només pots nomenar paquets que puguis veure, així que una llista d'exclusions construïda a partir d'una consulta que la teva aplicació no pot executar perdrà entrades en silenci. Compara la llista amb el que el gestor de paquets retorna realment abans de confiar-hi.
Els perfils de treball tornen a dividir l'audiència. Un túnel iniciat dins del perfil cobreix aquell perfil i les seves aplicacions, mentre que el costat personal conserva la seva pròpia connectivitat. És una decisió d'abast presa una vegada, i pertoca a la interfície, no a una incidència de suport.
Mite 4: stopSelf acaba el túnel
No l'acaba. La interfície viu al descriptor que retorna establish(), i l'objecte del servei pot morir mentre aquest descriptor continua obert. Tanca primer el ParcelFileDescriptor, deixa anar la referència, uneix o interromp els fils de lectura i escriptura, i després crida stopForeground(true) i stopSelf() en aquest ordre.
Un descriptor oblidat és la raó habitual per la qual un servei sembla sobreviure a la seva pròpia petició d'aturada, i explica una ruta obsoleta que viu més que l'aplicació que la va afegir.
L'error invers és igual de comú: tancar el descriptor mentre un fil de lectura és a mig llegir, i després deixar que aquell fil gestioni una excepció per a la qual ningú no ha escrit res. Mantén la seqüència d'aturada en un sol lloc i fes que tant l'acció de l'usuari com onRevoke() hi cridin.
Què cal comprovar quan el túnel sembla mort
Protegeix els sockets de llarga vida amb protect() abans que el túnel s'aixequi. Sense això, el transport del propi túnel es torna a encaminar cap al túnel i la connexió s'encalla d'una manera que sembla exactament una fallada del servidor. Prova contra una adreça que només existeixi darrere de la xarxa remota, no contra un lloc públic que respongui de la memòria cau o del camí subjacent.
Registra el resultat de establish(), les rutes, els paquets exclosos i el motiu de l'aturada. Aquestes quatre línies separen un problema de consentiment d'un problema d'abast i d'un problema de cicle de vida sense capturar paquets.