Android VpnService: заглављено читање TUN наспрам ENOBUFS грешака при писању
Читање које се никада не враћа и писање које пада са ENOBUFS имају супротне узроке. Измерите оглашени прозор и дужину пакета да одаберете поправку.
Два симптома се појављују у истој VpnService петљи и оба се приписују истој ствари: читач који се никада не враћа и писање које пада са ENOBUFS. То нису варијанте једне грешке. Заглављено читање је неслагање о контроли тока са peer-ом; неуспело писање је пакет који интерфејс неће прихватити. Ако их третирате као један проблем, потрошићете дан на подешавање бафера који никада нису били укључени. Подела испод је како одлучујем коју половину петље прво инструментирати.
Једна петља, две независне тачке отказа
Ваш сервис чита IP пакет из TUN дескриптора датотеке, прегледа га и пише одговор назад у исти дескриптор. Све што се поквари на том путу појављује се на једном од та два позива, зато се оба пријављују као покварен тунел. Позиви имају различите уговоре. Читање чека да вам кернел преда пакет, а писање предаје кернелу пакет који он сме да одбије.
Читања су лакша половина за размишљање. Читање које не враћа ништа или чека саобраћај који никада не долази или чека јер је peer престао да шаље.
Заглављено читање је неслагање о контроли тока
Читање TUN уређаја блокира док пакет не буде доступан. То је пројектовано понашање и зато нит паркирана у read изгледа нормално на празном линку. То постаје заглављивање када ваш сопствени TCP стек настави да прима податке од локалне апликације али никада не огласи довољан прозор да удаљени peer пошаље још. Peer попуни прозор који сте огласили, чека ACK и ажурирање прозора, и стане. Тада ваш читач нема шта да врати, а апликација види везу која је отворена и тиха.
Чести узроци су стек који пише у сокет без праћења оглашеног прозора и онај који заборави да пошаље ажурирање прозора након што испразни свој бафер. Спoља изгледају идентично: бајтови излазе, ништа се не враћа, никаква грешка се не подиже. Блокирање читачке нити на парсирању, DNS резолуцији или дисковном I/O производи исти облик симптома из неповезаног разлога, зато потврдите да је нит заиста унутар read пре него што окривите контролу тока.
Проверите peer-ов ред за слање и прозор који је последњи видео. Ако peer држи податке јер је ваш прозор нула, поправка је у вашој ACK путањи, не у TUN читању.
ENOBUFS је проблем величине, не недостатак бафера
Писање које враћа ENOBUFS на TUN интерфејсу обично одбија дужину пакета. Интерфејс има MTU, већи пакет не може се ставити у ред, а поновни покушај истог пакета пада исто сваки пут. Та детерминистичност је знак. Прави недостатак бафера се чисти како притисак опада, док одбијање величине не занима колико дуго чекате.
TCP има механизам управо за ово, и то је корак који људи прескачу: MSS clamp. Када синтетишете сегменте у корисничком простору бирате величину payload-а, а бирање из MTU локалног интерфејса уместо из MTU тунела емитује сегменте које тунел не може да носи. Ограничите на мањи од два. Clamp покрива само TCP, па се превелики UDP датаграм мора фрагментирати или одбацити вашим кодом.
Логујте дужину сваког пакета који пишете док дебагујете. Дужина која пада је одговор и неће се променити између покушаја.
Три мерења која раздвајају случајеве
Не треба вам дебагер ни за један од два отказа. Три запажања су довољна да се усмерите на прави одељак и трају краће од поновне изградње.
| 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 |
Ако се трећи ред поклапа, сама петља је у реду и проблем је тамо где га тунел не види: незаштићени транспортни сокет који носи ваш сопствени VPN саобраћај назад у тунел, што се обично појављује као веза која успе и затим не испоручи ништа.
Поправите водовод од ког зависе оба пута
Ниједан симптом није вредан јурњаве док интерфејс није исправно изграђен. Builder позиви који су важни су адреса, рута, MTU и сагласност, тим редом:
Builder()
.addAddress(10.8.0.2, 32)
.addRoute(0.0.0.0, 0)
.setMtu(1400)
.establish()Поставите MTU на стварну вредност тунела уместо да га оставите на ономе што је пример користио. Све што касније пишете ограничено је тим бројем, а неслагање између MTU-а који постављате и MSS-а на који ограничавате најчешћи је начин да поново увесете грешку писања након поправке. Затим заштитите сокет који носи саобраћај до самог VPN сервера. Без protect ти пакети се рутирају назад у ваш сопствени тунел и веза се никада не заврши, што изгледа као заглављено читање али је routing loop.
Сагласност се не може заобићи. Сервис покренут без тога да prepare врати null не добија интерфејс, а establish враћа null уместо да баци грешку, зато проверите дескриптор пре него што покренете петљу. VpnService референца је место да потврдите builder уговор уместо да копирате пример.
Верификујте један смер по један
Пошаљите UDP echo кроз тунел и гледајте дужине пакета пре него што дирнете TCP. Затим отворите handshake и потврдите да сегменти остају испод MSS-а који сте израчунали. Бројте пакете, а не везе, да би се застој појавио као празнина у бројачу уместо као висећи сокет. Ако се читања зауставе након рафала писања, прозор је осумњичени; ако писања падају одмах, дужина је.
Затим рестартујте уређај и поновите једном, јер VpnService који ради само до прве промене мреже је routing проблем, а не проблем контроле тока.