Android VpnService: zaseknuté čtení TUN versus chyby zápisu ENOBUFS
Čtení, které se nikdy nevrátí, a zápis, který selže s ENOBUFS, mají opačné příčiny. Změřte inzerované okno a délku paketu, abyste zvolili opravu.
Dva příznaky se objevují ve stejné smyčce VpnService a svaluje se vina na totéž: čtečka, která se nikdy nevrátí, a zápis, který selže s ENOBUFS. Nejsou to variace jedné chyby. Zaseknuté čtení je nesouhlas o řízení toku s protějškem; neúspěšný zápis je paket, který rozhraní nepřijme. Pokud je budete řešit jako jeden problém, strávíte den laděním bufferů, které v tom nikdy nebyly. Rozdělení níže je způsob, jakým se rozhoduji, kterou polovinu smyčky instrumentovat první.
Jedna smyčka, dva nezávislé body selhání
Vaše služba čte IP paket z deskriptoru souboru TUN, prohlédne ho a zapíše odpověď zpět do stejného deskriptoru. Všechno, co se na této cestě pokazí, se projeví u jednoho z těchto dvou volání, a proto se obojí hlásí jako rozbitý tunel. Volání mají různé kontrakty. Čtení čeká, až vám jádro předá paket, a zápis předává jádru paket, který smí odmítnout.
Čtení je ta snadnější polovina na uvažování. Čtení, které nic nevrací, buď čeká na provoz, který nikdy nedorazí, nebo čeká, protože protějšek přestal posílat.
Zaseknuté čtení je nesouhlas o řízení toku
Čtení zařízení TUN blokuje, dokud není paket k dispozici. To je zamýšlené chování, a proto vlákno zaparkované v read vypadá na nečinném spoji normálně. Zaseknutím se to stane, když váš vlastní TCP stack dál přijímá data z místní aplikace, ale nikdy neinzeruje dostatečné okno, aby vzdálený protějšek mohl poslat víc. Protějšek zaplní okno, které jste inzerovali, počká na ACK a aktualizaci okna a zastaví se. Vaše čtečka pak nemá co vrátit a aplikace vidí spojení, které je otevřené a tiché.
Časté příčiny jsou stack, který zapisuje do socketu bez sledování inzerovaného okna, a takový, který zapomene po vyprázdnění bufferu poslat aktualizaci okna. Zvenku vypadají stejně: bajty odcházejí, nic se nevrací, žádná chyba se nevyhodí. Zablokování čtecího vlákna na parsování, DNS rozlišení nebo diskovém I/O vytváří stejný tvar příznaku z nesouvisejícího důvodu, takže než obviníte řízení toku, ověřte, že vlákno je skutečně uvnitř read.
Zkontrolujte odesílací frontu protějšku a okno, které naposledy viděl. Pokud protějšek drží data, protože vaše okno je nula, oprava je ve vaší cestě ACK, ne ve čtení TUN.
ENOBUFS je problém velikosti, ne nedostatek bufferu
Zápis, který na rozhraní TUN vrací ENOBUFS, obvykle odmítá délku paketu. Rozhraní má MTU, paket větší než ona nemůže být zařazen do fronty a opakování stejného paketu selhává pokaždé stejně. Ta determinovanost je vodítko. Skutečný nedostatek bufferu se vyčistí, jak tlak klesá, zatímco odmítnutí velikosti nezajímá, jak dlouho čekáte.
TCP má mechanismus přesně pro tohle, a to je krok, který lidé přeskakují: MSS clamp. Když syntetizujete segmenty v uživatelském prostoru, vybíráte velikost payloadu, a výběr z MTU místního rozhraní místo z MTU tunelu vydává segmenty, které tunel neunese. Omezte se na menší z obou. Clamp pokrývá jen TCP, takže příliš velký UDP datagram musí váš kód fragmentovat nebo zahodit.
Při ladění logujte délku každého paketu, který zapíšete. Délka, která selže, je odpověď, a mezi pokusy se nezmění.
Tři měření, která oddělují případy
Pro ani jedno z těchto selhání nepotřebujete debugger. Tři pozorování stačí k nasměrování do správné sekce a zaberou méně času než rebuild.
| What you see | Where to look |
|---|---|
| Reader parked, peer send queue empty, last advertised window zero | Window updates in your ACK path |
| Write fails on one length and succeeds on a smaller one | MSS clamp and tunnel MTU |
| Both directions look fine, app still sees no data | Protected socket and routing loop |
Pokud sedí třetí řádek, smyčka sama je v pořádku a problém je tam, kde ho tunel nevidí: nechráněný transportní socket, který nese váš vlastní VPN provoz zpět do tunelu, což se obvykle projeví jako spojení, které uspěje a pak nic nedoručí.
Opravte potrubí, na kterém závisí obě cesty
Žádný příznak nemá cenu honit, dokud není rozhraní správně postavené. Volání builderu, na kterých záleží, jsou adresa, trasa, MTU a souhlas, v tomto pořadí:
Builder()
.addAddress(10.8.0.2, 32)
.addRoute(0.0.0.0, 0)
.setMtu(1400)
.establish()Nastavte MTU na skutečnou hodnotu tunelu, místo abyste ho nechali na tom, co použil příklad. Všechno, co později zapíšete, je omezeno tímto číslem, a nesoulad mezi MTU, které nastavíte, a MSS, na které omezujete, je nejběžnější způsob, jak po opravě znovu zavést chybu zápisu. Pak chraňte socket, který nese provoz k samotnému VPN serveru. Bez protect se tyto pakety směrují zpět do vašeho vlastního tunelu a spojení se nikdy nedokončí, což vypadá jako zaseknuté čtení, ale je to smyčka směrování.
Souhlas nelze obejít. Služba spuštěná bez toho, aby prepare vrátil null, nedostane rozhraní, a establish vrací null místo toho, aby vyhodil výjimku, takže před spuštěním smyčky zkontrolujte deskriptor. Referenční příručka VpnService je místo, kde potvrdit kontrakt builderu místo kopírování příkladu.
Ověřujte jeden směr po druhém
Pošlete tunelem UDP echo a sledujte délky paketů, než se dotknete TCP. Pak otevřete handshake a potvrďte, že segmenty zůstávají pod MSS, které jste spočítali. Počítejte pakety, ne spojení, aby se zaseknutí projevilo jako mezera v čítači místo zaseknutého socketu. Pokud se čtení po dávce zápisů zastaví, podezřelé je okno; pokud zápisy selhávají okamžitě, je to délka.
Potom zařízení restartujte a jednou opakujte, protože VpnService, který funguje jen do první změny sítě, je problém směrování, ne řízení toku.