Android VpnService ansluter men inga paket når nätverket
Tunneln är uppe och sidorna misslyckas fortfarande: kontrollera läsmod, socket-skydd, TCP-stöd och ruttlistan innan du skyller på VPN-servern.
Tunneln kommer upp, nyckelikonen visas och varje begäran i webbläsaren tar fortfarande slut på tid. I en VpnService-app betyder den kombinationen oftast att läslöpan lever och utloppshalvan är trasig, inte att servern avvisade handskakningen. Android har redan skapat gränssnittet, flyttat appens trafik in i det och gett din process en filbeskrivning. Allt efter den beskrivningen är din kod, och ett enda saknat anrop räcker för att svälja hela strömmen.
Vad systemet ger dig och vad det inte ger
establish() returnerar en ParcelFileDescriptor för ett tun-gränssnitt. Systemet skriver utgående IP-paket i det och förväntar sig att din process läser dem, avgör vart de ska och skickar dem genom en vanlig socket. Under finns ingen kernelforwardering, så när ingen läser släpps paketen och gränssnittets räknare rör sig aldrig.
Börja med att räkna i stället för att gissa. Öka en räknare vid varje lyckad läsning och logga destination och protokoll för de första paketen. Om räknaren står kvar på noll medan trafik genereras ligger felet före din vidarebefordringskod, vilket begränsar sökningen till builder-konfigurationen och läsläget. Om räknaren rör sig och webbläsaren ändå misslyckas ligger felet i sändvägen eller på vägen tillbaka.
Blockerande läsning avgör om löpan rör sig
setBlocking(true) sätter beskrivningen i blockerande läge och har funnits sedan API 21. På äldre versioner är lösningen en liten JNI-hjälpare som rensar O_NONBLOCK med fcntl på den rå beskrivningen. Läget spelar roll eftersom det avgör vad en läsning utan väntande paket får göra.
I icke-blockerande läge återvänder den läsningen direkt med ett negativt värde, och en löpa som ignorerar resultatet snurrar en kärna utan att flytta någon trafik. I blockerande läge väntar läsningen, vilket en vidarebefordringstråd vill, men tråden kan då inte längre avbrytas. Att stänga beskrivningen är det stödda sättet att få läsningen att misslyckas så att löpan kan avslutas. Att döda processen eller avbryta tråden kan i stället lämna nästa start med en beskrivning som inte längre passar gränssnittet.
Läs ett paket per anrop i en buffert av gränssnittets MTU-storlek, och behandla noll eller ett negativt resultat som ett fel, inte som ett tomt paket.
protect() före den första sändningen, inte efter
Varje socket som bär ett inkapslat paket måste skyddas först. En oskyddad socket följer samma ruttregler som du nyss installerade, så paketen går tillbaka in i tunneln och din egen läslöpa plockar upp dem igen. Tunneln vidarebefordrar då sin egen utdata i oändlighet, och utifrån ser det ut som en anslutning som öppnas och aldrig svarar.
Ordningen är protect, sedan connect eller bind, sedan send. Att anropa protect efter connect kan lämna det tidigare ruttbeslutet kvar, så socketen fortsätter använda fel väg trots att anropet returnerade true. Kontrollera booleanen den returnerar och misslyckas högljutt när den är false, i stället för att gå in i en sändning som inte kan fungera.
Ett behörighetsfel från sendto på en socket som aldrig rört protect() är den vanliga signaturen för det här felet.
UDP-exemplet bär inte HTTPS
Vidarebefordringsexemplet som de flesta guider kopierar hanterar UDP och inget annat. TCP behöver handskakning, sekvensnummer, bekräftelser och omsändning, och en löpa som kopierar nyttolast mellan en tun-beskrivning och en socket ger inget av det. Därför kan en UDP-DNS-fråga gå igenom medan varje HTTPS-begäran fastnar. Kör antingen en TCP-stack i användarutrymmet eller lämna anslutningarna till en lokal proxy som avslutar TCP på enheten och vidarebefordrar strömmen.
Testa uppdelningen innan du skriver om något: skicka en UDP-fråga och öppna en TCP-anslutning till samma värd. Om UDP-vägen fungerar och TCP-vägen hänger vid första SYN är vidarebefordringskoden problemet och inte servern. Den enda jämförelsen sparar mycket tid som annars läggs på att läsa serverloggar utan något användbart innehåll.
Ruttlistan och filter per app begränsar vad du ser
addAddress, addRoute och addDns definierar vad systemet skickar in i tunneln. En destination utanför ruttlistan når aldrig din beskrivning, så ingen mängd vidarebefordringslogik rättar det. Logga builder-anropen du gör och jämför dem med de adresser appen faktiskt försöker nå.
Val per app har sin egen nedre gräns: addAllowedApplication och addDisallowedApplication kräver API 21, och på äldre versioner saknas metoderna helt enkelt. En design som filtrerar på paket kan inte köras där alls.
Återupprättande lämnar inaktuella beskrivningar
Ett nytt anrop till establish() returnerar en ny beskrivning och stänger föregående gränssnitt. Den gamla beskrivningen är nu ogiltig, så en tråd som fortfarande blockerar på den får ett fel, och sockets skapade för den gamla vägen kan behöva bytas ut. onRevoke är återanropet för en tunnel som användaren eller systemet har tagit bort. Att hålla livscykeln hos en ägare, som stoppar läsaren före ny upprättning, förhindrar de flesta ansluten-men-död-tillstånd.
Läs varje fel efter det steg det hör till. En återställning av anslutningen från recvfrom betyder att fjärrenden stängde medan din kod fortfarande läste. Ett fel om dålig adress pekar på header-parsning eller en offset-bugg, inte på nätverket.
Arbetsordning: blockerande läge på, ett paket per läsning, header parsad, socket skyddad, nyttolast skickad, ren stängning vid överlämning och en ruttlista som täcker destinationen.