Android VpnService: citiri TUN blocate versus erori de scriere ENOBUFS
O citire care nu se întoarce niciodată și o scriere care eșuează cu ENOBUFS au cauze opuse. Măsurați fereastra anunțată și lungimea pachetului pentru a alege corectura.
Două simptome apar în aceeași buclă VpnService și amândouă sunt puse pe seama aceluiași lucru: un cititor care nu se întoarce niciodată și o scriere care eșuează cu ENOBUFS. Nu sunt variante ale aceleiași erori. O citire blocată este o neînțelegere privind controlul fluxului cu peer-ul; o scriere eșuată este un pachet pe care interfața nu îl va accepta. Dacă le tratați ca pe o singură problemă, veți petrece o zi reglând buffere care nu au fost niciodată implicate. Împărțirea de mai jos este modul în care decid ce jumătate a buclei instrumentez prima.
O buclă, două puncte de eșec independente
Serviciul dumneavoastră citește un pachet IP din descriptorul de fișier TUN, îl inspectează și scrie un răspuns înapoi în același descriptor. Tot ce se strică pe acea cale apare la una dintre cele două apeluri, motiv pentru care ambele sunt raportate ca tunel stricat. Apelurile au contracte diferite. O citire așteaptă ca nucleul să vă dea un pachet, iar o scriere dă nucleului un pachet pe care acesta are dreptul să îl refuze.
Citirile sunt jumătatea ușoară de analizat. O citire care nu returnează nimic fie așteaptă trafic care nu sosește niciodată, fie așteaptă pentru că peer-ul a încetat să trimită.
O citire blocată este o neînțelegere a controlului fluxului
Citirea unui dispozitiv TUN se blochează până când un pachet este disponibil. Acesta este comportamentul proiectat și de aceea un fir oprit în read arată normal pe un link inactiv. Devine o blocare când propriul dumneavoastră stivă TCP continuă să accepte date de la aplicația locală dar nu anunță niciodată o fereastră suficientă pentru ca peer-ul îndepărtat să trimită mai mult. Peer-ul umple fereastra pe care ați anunțat-o, așteaptă un ACK și o actualizare de fereastră, și se oprește. Atunci cititorul dumneavoastră nu are ce returna, iar aplicația vede o conexiune deschisă și tăcută.
Cauzele obișnuite sunt o stivă care scrie în socket fără a urmări fereastra anunțată și una care uită să trimită o actualizare de fereastră după ce își golește bufferul. Din exterior arată identic: bytes ies, nimic nu se întoarce, nicio eroare nu este ridicată. Blocarea firului de citire pe parsare, rezolvare DNS sau I/O de disc produce aceeași formă de simptom dintr-un motiv nelegat, așa că confirmați că firul este într-adevăr în read înainte de a da vina pe controlul fluxului.
Verificați coada de trimitere a peer-ului și fereastra pe care a văzut-o ultima dată. Dacă peer-ul ține date pentru că fereastra dumneavoastră este zero, corectura este în calea ACK, nu în citirea TUN.
ENOBUFS este o problemă de dimensiune, nu o lipsă de buffer
O scriere care returnează ENOBUFS pe o interfață TUN respinge de obicei lungimea pachetului. Interfața are o MTU, un pachet mai mare nu poate fi pus în coadă, iar reîncercarea aceluiași pachet eșuează la fel de fiecare dată. Acest determinism este indiciul. O lipsă reală de buffer dispare când presiunea scade, în timp ce o respingere de dimensiune nu îi pasă cât așteptați.
TCP are un mecanism exact pentru asta, și este pasul pe care oamenii îl sar: clamp-ul MSS. Când sintetizați segmente în spațiul utilizatorului alegeți dimensiunea payload-ului, iar alegerea ei din MTU-ul interfeței locale în loc de MTU-ul tunelului emite segmente pe care tunelul nu le poate transporta. Limitați la cea mai mică dintre cele două. Clamp-ul acoperă doar TCP, așa că un datagram UDP prea mare trebuie fragmentat sau aruncat de codul dumneavoastră.
Înregistrați lungimea fiecărui pachet pe care îl scrieți în timpul depanării. Lungimea care eșuează este răspunsul și nu se va schimba între încercări.
Trei măsurători care separă cazurile
Nu aveți nevoie de un debugger pentru niciuna dintre cele două eșecuri. Trei observații sunt suficiente pentru a vă îndrepta spre secțiunea corectă și durează mai puțin decât o recompilare.
| 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 |
Dacă al treilea rând se potrivește, bucla în sine este în regulă, iar problema este acolo unde tunelul nu o poate vedea: un socket de transport neprotejat care duce propriul dumneavoastră trafic VPN înapoi în tunel, ceea ce apare de obicei ca o conexiune care reușește și apoi nu livrează nimic.
Reparați instalația de care depind ambele căi
Niciun simptom nu merită urmărit până când interfața nu este construită corect. Apelurile builder care contează sunt adresa, ruta, MTU-ul și consimțământul, în această ordine:
Builder()
.addAddress(10.8.0.2, 32)
.addRoute(0.0.0.0, 0)
.setMtu(1400)
.establish()Setați MTU-ul la valoarea reală a tunelului în loc să îl lăsați la cea folosită de exemplu. Tot ce scrieți mai târziu este limitat de acel număr, iar o discrepanță între MTU-ul pe care îl setați și MSS-ul la care limitați este cea mai comună modalitate de a reintroduce eroarea de scriere după o corectură. Apoi protejați socket-ul care transportă trafic către serverul VPN însuși. Fără protect, acele pachete sunt rutate înapoi în propriul tunel și conexiunea nu se finalizează niciodată, ceea ce arată ca o citire blocată dar este o buclă de rutare.
Consimțământul nu poate fi ocolit. Un serviciu pornit fără ca prepare să returneze null nu obține interfață, iar establish returnează null în loc să arunce, așa că verificați descriptorul înainte de a porni bucla. Referința VpnService este locul pentru a confirma contractul builder în loc să copiați un exemplu.
Verificați o singură direcție odată
Trimiteți un ecou UDP prin tunel și urmăriți lungimile pachetelor înainte de a atinge TCP. Apoi deschideți un handshake și confirmați că segmentele rămân sub MSS-ul pe care l-ați calculat. Numărați pachete, nu conexiuni, astfel încât o blocare să apară ca o lacună în contor în loc de un socket blocat. Dacă citirile se opresc după o rafală de scrieri, fereastra este suspectul; dacă scrierile eșuează imediat, este lungimea.
Apoi reporniți dispozitivul și repetați o dată, pentru că un VpnService care funcționează doar până la prima schimbare de rețea este o problemă de rutare și nu de control al fluxului.