Tunel naskočí, ikona klíče se objeví a každý požadavek v prohlížeči přesto vyprší. V aplikaci s VpnService tato kombinace obvykle znamená, že čtecí smyčka žije a výstupní polovina je rozbitá, ne že server odmítl handshake. Android už rozhraní vytvořil, přesunul do něj provoz aplikace a předal vašemu procesu deskriptor souboru. Všechno za tímto deskriptorem je váš kód a jedno chybějící volání stačí, aby spolklo celý proud.

Co vám systém dává a co ne

establish() vrací ParcelFileDescriptor pro rozhraní tun. Systém do něj zapisuje odchozí IP pakety a očekává, že je váš proces přečte, rozhodne, kam jdou, a odešle je přes obyčejný socket. Pod tím není žádné přeposílání v jádru, takže když nikdo nečte, pakety se zahazují a čítače rozhraní se nikdy nehnou.

Začněte počítáním místo hádání. Zvyšujte čítač při každém úspěšném čtení a zaznamenejte cíl a protokol prvních několika paketů. Pokud čítač zůstane na nule, zatímco se generuje provoz, závada je před vaším kódem pro přeposílání, což zužuje hledání na konfiguraci builderu a režim čtení. Pokud se čítač hýbe a prohlížeč přesto selhává, závada je v cestě odesílání nebo na cestě zpět.

Blokující čtení rozhoduje, zda se smyčka hýbe

setBlocking(true) přepne deskriptor do blokujícího režimu a existuje od API 21. Ve starších verzích je řešením malý pomocník JNI, který vymaže O_NONBLOCK pomocí fcntl na surovém deskriptoru. Režim je důležitý, protože určuje, co smí čtení bez čekajícího paketu dělat.

V neblokujícím režimu se takové čtení vrátí okamžitě se zápornou hodnotou a smyčka, která výsledek ignoruje, točí jádro, aniž by přenesla jediný bajt provozu. V blokujícím režimu čtení čeká, což vlákno pro přeposílání chce, ale vlákno pak už nelze přerušit. Zavření deskriptoru je podporovaný způsob, jak čtení nechat selhat, aby smyčka mohla skončit. Zabít proces nebo přerušit vlákno může naopak nechat příští start s deskriptorem, který už rozhraní neodpovídá.

Čtěte jeden paket na volání do vyrovnávací paměti velikosti MTU rozhraní a nulu nebo záporný výsledek berte jako chybu, ne jako prázdný paket.

protect() před prvním odesláním, ne po něm

Každý socket, který nese zapouzdřený paket, musí být nejdřív ochráněn. Nechráněný socket poslouchá stejná pravidla směrování, která jste právě nastavili, takže jeho pakety se vracejí do tunelu a vaše vlastní čtecí smyčka je znovu zachytí. Tunel pak přeposílá vlastní výstup donekonečna a zvenčí to vypadá jako spojení, které se otevře a nikdy neodpoví.

Pořadí je protect, potom connect nebo bind, potom send. Volání protect po connect může ponechat dřívější rozhodnutí o trase, takže socket dál používá špatnou cestu, i když volání vrátilo true. Zkontrolujte boolean, který vrací, a nahlas selžete, když je false, místo abyste vešli do odeslání, které nemůže fungovat.

Chyba oprávnění ze sendto na socketu, který se nikdy nedotkl protect(), je obvyklý podpis této chyby.

Ukázka s UDP nenese HTTPS

Ukázka přeposílání, kterou kopíruje většina návodů, zvládá UDP a nic jiného. TCP potřebuje handshake, pořadová čísla, potvrzení a opakované odesílání, a smyčka, která kopíruje data mezi deskriptorem tun a socketem, nenabízí nic z toho. Proto UDP dotaz DNS projde, zatímco každý požadavek HTTPS uvázne. Buď spusťte TCP stack v uživatelském prostoru, nebo předejte spojení místnímu proxy, které ukončí TCP na zařízení a přepošle proud.

Rozdíl otestujte, než cokoli přepíšete: pošlete jeden UDP dotaz a otevřete jedno TCP spojení ke stejnému hostiteli. Pokud cesta UDP funguje a cesta TCP visí na prvním SYN, problém je v kódu pro přeposílání, ne v serveru. Toto jediné srovnání ušetří spoustu času stráveného čtením serverových logů, které nic užitečného neobsahují.

Seznam tras a filtry podle aplikací omezují, co vidíte

addAddress, addRoute a addDns určují, co systém posílá do tunelu. Cíl mimo seznam tras se k vašemu deskriptoru nikdy nedostane, takže to nespraví žádné množství logiky přeposílání. Zaznamenávejte volání builderu a porovnávejte je s adresami, na které se aplikace skutečně snaží dosáhnout.

Výběr podle aplikací má svůj práh: addAllowedApplication a addDisallowedApplication vyžadují API 21 a ve starších verzích tyto metody prostě chybí. Návrh, který filtruje podle balíčku, tam vůbec nemůže běžet.

Opětovné navázání nechává zastaralé deskriptory

Další volání establish() vrátí nový deskriptor a zavře předchozí rozhraní. Starý deskriptor je teď neplatný, takže vlákno, které na něm ještě blokuje, dostane chybu, a sockety vytvořené pro starou cestu může být nutné nahradit. onRevoke je zpětné volání pro tunel, který uživatel nebo systém odebral. Držení životního cyklu u jednoho vlastníka, který před novým navázáním zastaví čtečku, zabrání většině stavů připojeno-ale-mrtvé.

Každou chybu čtěte podle kroku, ke kterému patří. Reset spojení z recvfrom znamená, že vzdálená strana zavřela, zatímco váš kód ještě četl. Chyba špatné adresy ukazuje na parsování hlavičky nebo chybu offsetu, ne na síť.

Pořadí práce: blokující režim zapnutý, jeden paket na čtení, hlavička zpracovaná, socket chráněný, data odeslaná, čisté zavření při předání a seznam tras, který pokrývá cíl.

Související články