De ce tunelul tău Android VpnService nu mai rutează traficul
Patru convingeri despre VpnService care se strică în producție: un singur tunel o dată, aria rutelor, liste pe aplicație și ordinea opririi, cu o verificare pentru fiecare.
Patru presupuneri despre Android VpnService m-au costat odată cea mai mare parte a unei săptămâni și niciuna nu era despre protocolul de rețea. Aplicația spunea conectat, pictograma cheii stătea în bara de stare, iar traficul ieșea în continuare pe ruta normală. Continuam să presupun că tunelul este al meu până îl opresc, că rutele pe care le adaug sunt rutele pe care le primesc și că un apel la stopSelf va coborî interfața cu el. Serviciul se comporta exact cum este documentat. Partea stricată era modelul meu mental.
Mitul 1: tunelul stă ridicat până îl oprești
Android ține o singură interfață VPN o dată. Stabilește una a doua și platforma nu o pune în coadă: o dezactivează pe a ta și apelează onRevoke() în serviciul tău. Dacă codul tău demontează totul doar la apăsarea unui buton, o preluare lasă în urmă un descriptor și un fir de citire, iar următoarea pornire le moștenește pe amândouă.
Referința VpnService spune explicit că prepare() returnează null când aplicația are deja consimțământ și un Intent când utilizatorul mai trebuie să aprobe. Acel consimțământ ajunge doar dacă trimiți intenția prin startActivityForResult(). Sari peste predare și establish() returnează null, ceea ce se citește ca un eșec tăcut și nu ca o problemă de permisiuni.
Always-on VPN adaugă un proprietar pe care nu îl controlezi. Sistemul poate readuce tunelul după o pornire sau după o cădere și trece înaintea unei aplicații VPN pornite manual de utilizator. Construiește onRevoke() astfel încât să închidă aceleași resurse ca oprirea ta normală și tratează-l ca pe o cale principală de oprire, nu ca pe un caz marginal. Utilizatorii schimbă aplicații VPN; codul tău trebuie să supraviețuiască.
Mitul 2: addRoute captează exact ce ai listat
Rutele decid ce pachete ajung la descriptorul tău, iar o eroare de arie produce cazul înnebunitor în care o parte din trafic trece prin tunel și o parte nu. addRoute("0.0.0.0", 0) ia tot IPv4, ceea ce înseamnă că fiecare pachet ajunge acum la tine și trebuie fie forwardat, fie aruncat. Un /32 pentru un singur host nu ia aproape nimic, iar restul se comportă ca și cum VPN-ul tău nu ar fi fost niciodată instalat.
Familiile se numără separat. O rută doar IPv4 lasă IPv6 pe rețeaua de bază, așa că un host cu două stive poate încă rezolva și conecta în afara tunelului, deși în interior pare perfect conectat.
DNS urmează aceeași logică. Serverele adăugate cu addDnsServer() ajung la aplicațiile care folosesc resolverul tunelului, în timp ce o aplicație cu propriul endpoint DNS criptat sau cu Private DNS setat pe un nume de host le ignoră complet. Când numele eșuează pentru o aplicație și funcționează pentru alta, verifică traseul resolverului înainte de a atinge măcar o rută.
Mitul 3: regulile pe aplicație sunt un comutator pe care îl poți întoarce în orice direcție
addDisallowedApplication() și addAllowedApplication() sunt două moduri, nu două filtre. Constructorul acceptă un model pe tunel, așa că o aplicație care vrea totul în afară de trei pachete folosește lista interzisă și lasă lista permisă goală. Cele două se exclud reciproc, iar oamenii află asta la rulare după ce ecranul de setări a oferit deja ambele comutatoare.
Vizibilitatea pachetelor mușcă și ea. Pe Android 11 și mai sus poți numi doar pachete pe care le vezi, așa că o listă de excluderi construită dintr-o interogare pe care aplicația ta nu are voie să o ruleze va rata intrări în liniște. Compară lista cu ce returnează managerul de pachete în realitate înainte să te bazezi pe ea.
Profilurile de lucru împart din nou publicul. Un tunel pornit în profil acoperă acel profil și aplicațiile lui, în timp ce partea personală își păstrează propria conectivitate. Este o decizie de arie luată o singură dată și îi este locul în interfață, nu într-un tichet de suport.
Mitul 4: stopSelf încheie tunelul
Nu îl încheie. Interfața trăiește în descriptorul returnat de establish(), iar obiectul serviciului poate muri în timp ce descriptorul rămâne deschis. Închide mai întâi ParcelFileDescriptor, renunță la referință, alătură-te firelor de citire și scriere sau întrerupe-le, apoi apelează stopForeground(true) și stopSelf() în această ordine.
Un descriptor uitat este motivul obișnuit pentru care un serviciu pare să supraviețuiască propriei cereri de oprire și explică o rută învechită care trăiește mai mult decât aplicația care a adăugat-o.
Greșeala inversă este la fel de obișnuită: închiderea descriptorului în timp ce un fir de citire este la mijlocul citirii, apoi lăsarea acelui fir să se ocupe de o excepție pentru care nimeni nu a scris cod. Ține secvența de oprire într-un singur loc și lasă atât acțiunea utilizatorului, cât și onRevoke() să apeleze acolo.
Ce să verifici când tunelul pare mort
Protejează socketurile de lungă durată cu protect() înainte ca tunelul să urce. Fără asta, transportul tunelului însuși este rutat înapoi în tunel și conexiunea se blochează într-un mod care arată exact ca o defecțiune de server. Testează împotriva unei adrese care există doar în spatele rețelei la distanță, nu împotriva unui site public care răspunde din cache sau de pe traseul de bază.
Înregistrează rezultatul lui establish(), rutele, pachetele excluse și motivul opririi. Acele patru linii separă o problemă de consimțământ de o problemă de arie și de una de ciclu de viață fără captură de pachete.