Android VpnService: blokkert TUN-lesing mot ENOBUFS-skrivefeil
En lesing som aldri kommer tilbake, og en skriving som feiler med ENOBUFS, har motsatte årsaker. Mål det annonserte vinduet og pakkelengden for å velge rettelse.
To symptomer dukker opp i samme VpnService-løkke og får skylden for det samme: en leser som aldri kommer tilbake, og en skriving som feiler med ENOBUFS. De er ikke varianter av én feil. En blokkert lesing er uenighet om flytkontroll med peer-en; en mislykket skriving er en pakke grensesnittet ikke vil godta. Behandler du dem som ett problem, bruker du en dag på å justere buffere som aldri var involvert. Oppdelingen nedenfor er hvordan jeg bestemmer hvilken halvdel av løkken jeg instrumenterer først.
Én løkke, to uavhengige feilpunkter
Tjenesten din leser en IP-pakke fra TUN-filbeskrivelsen, undersøker den og skriver et svar tilbake til samme beskrivelse. Alt som bryter på den veien dukker opp ved én av de to kallene, og derfor rapporteres begge som en ødelagt tunnel. Kallene har ulike kontrakter. En lesing venter på at kjernen gir deg en pakke, og en skriving gir kjernen en pakke den har lov til å avvise.
Lesinger er den enkle halvdelen å tenke på. En lesing som ikke returnerer noe, venter enten på trafikk som aldri kommer, eller venter fordi peer-en sluttet å sende.
En blokkert lesing er uenighet om flytkontroll
Lesing av en TUN-enhet blokkerer til en pakke er tilgjengelig. Det er designet atferd, og derfor ser en tråd parkert i read normal ut på en ledig lenke. Det blir en henging når din egen TCP-stack fortsetter å ta imot data fra den lokale appen men aldri annonserer et vindu stort nok til at den eksterne peer-en kan sende mer. Peer-en fyller vinduet du annonserte, venter på en ACK og en vindusoppdatering, og stopper. Da har leseren din ingenting å returnere, og appen ser en tilkobling som er åpen og stille.
De vanlige årsakene er en stack som skriver til socketen uten å følge det annonserte vinduet, og en som glemmer å sende en vindusoppdatering etter å ha tømt bufferen. Utenfra ser de identiske ut: byte går ut, ingenting kommer tilbake, ingen feil reises. Blokkering av lesetråden på parsing, DNS-oppslag eller disk-I/O gir samme form for symptom av en urelatert årsak, så bekreft at tråden faktisk er inne i read før du gir flytkontrollen skylden.
Sjekk peer-ens sendekø og vinduet den så sist. Hvis peer-en holder data fordi vinduet ditt er null, ligger rettelsen i ACK-veien din, ikke i TUN-lesingen.
ENOBUFS er et størrelsesproblem, ikke buffermangel
En skriving som returnerer ENOBUFS på et TUN-grensesnitt avviser vanligvis pakkelengden. Grensesnittet har en MTU, en større pakke kan ikke settes i kø, og gjentatte forsøk med samme pakke feiler på samme måte hver gang. Den determinismen er tegnet. En ekte buffermangel klarner når trykket synker, mens en størrelsesavvisning ikke bryr seg om hvor lenge du venter.
TCP har en mekanisme nettopp for dette, og det er trinnet folk hopper over: MSS-clamp. Når du syntetiserer segmenter i brukerrommet, velger du nyttelaststørrelsen, og å ta den fra det lokale grensesnittets MTU i stedet for tunnelens MTU sender ut segmenter tunnelen ikke kan bære. Begrens til den minste av de to. Clampen dekker bare TCP, så et for stort UDP-datagram må fragmenteres eller droppes av koden din.
Logg lengden på hver pakke du skriver under feilsøking. Lengden som feiler er svaret, og den endrer seg ikke mellom forsøk.
Tre målinger som skiller tilfellene
Du trenger ikke en debugger for noen av de to feilene. Tre observasjoner er nok til å lede deg til riktig avsnitt, og de tar kortere tid enn en ombygging.
| 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 |
Hvis den tredje raden passer, er løkken selv i orden og problemet sitter der tunnelen ikke kan se det: en ubeskyttet transport-socket som bærer din egen VPN-trafikk tilbake i tunnelen, noe som vanligvis viser seg som en tilkobling som lykkes og deretter ikke leverer noe.
Reparer rørleggerarbeidet begge veier avhenger av
Ingen symptom er verdt å jage før grensesnittet er bygget riktig. Builder-kallene som betyr noe er adressen, ruten, MTU-en og samtykket, i den rekkefølgen:
Builder()
.addAddress(10.8.0.2, 32)
.addRoute(0.0.0.0, 0)
.setMtu(1400)
.establish()Sett MTU-en til tunnelens reelle verdi i stedet for å la den stå på det eksemplet brukte. Alt du skriver senere er begrenset av det tallet, og avvik mellom MTU-en du setter og MSS-en du begrenser til er den vanligste måten å gjeninnføre skrivefeilen etter en rettelse. Beskytt deretter socketen som bærer trafikk til VPN-serveren selv. Uten protect rutes disse pakkene tilbake i din egen tunnel og tilkoblingen fullføres aldri, noe som ser ut som en blokkert lesing men er en ruting-løkke.
Samtykke kan ikke omgås. En tjeneste startet uten at prepare returnerer null får ingen grensesnitt, og establish returnerer null i stedet for å kaste, så sjekk beskrivelsen før du starter løkken. VpnService-referansen er stedet for å bekrefte builder-kontrakten i stedet for å kopiere et eksempel.
Verifiser én retning om gangen
Send et UDP-ekko gjennom tunnelen og følg med på pakkelengdene før du rører TCP. Åpne deretter et håndtrykk og bekreft at segmentene holder seg under MSS-en du beregnet. Tell pakker i stedet for tilkoblinger, så en blokkering vises som et hull i telleren i stedet for en fastlåst socket. Hvis lesinger stopper etter en byge med skrivinger, er vinduet den mistenkte; hvis skrivinger feiler med en gang, er det lengden.
Start deretter enheten på nytt og gjenta én gang, fordi en VpnService som bare virker til den første nettverksendringen er et rutingproblem og ikke et flytkontrollproblem.