Android VpnService: TUN-læsning der går i stå kontra ENOBUFS-skrivefejl
En læsning der aldrig vender tilbage, og en skrivning der fejler med ENOBUFS, har modsatte årsager. Mål det annoncerede vindue og pakkelængden for at vælge rettelsen.
To symptomer dukker op i samme VpnService-løkke og får skylden for det samme: en læser der aldrig vender tilbage, og en skrivning der fejler med ENOBUFS. De er ikke varianter af samme fejl. En blokeret læsning er uenighed om flow control med peer'en; en mislykket skrivning er en pakke, som grænsefladen ikke vil acceptere. Behandler du dem som ét problem, bruger du en dag på at tune buffere, der aldrig var involveret. Opdelingen nedenfor er, hvordan jeg beslutter, hvilken halvdel af løkken jeg instrumenterer først.
Én løkke, to uafhængige fejlpunkt
Din service læser en IP-pakke fra TUN-filbeskrivelsen, undersøger den og skriver et svar tilbage til samme beskrivelse. Alt, der brækker på den vej, dukker op ved et af de to kald, og derfor bliver begge rapporteret som en ødelagt tunnel. Kaldene har forskellige kontrakter. En læsning venter på, at kernen giver dig en pakke, og en skrivning giver kernen en pakke, som den har lov til at afvise.
Læsninger er den lette halvdel at tænke over. En læsning, der ikke returnerer noget, venter enten på trafik, der aldrig kommer, eller fordi peer'en er stoppet med at sende.
En blokeret læsning er uenighed om flow control
Læsning af en TUN-enhed blokerer, indtil en pakke er tilgængelig. Det er designet adfærd, og derfor ser en tråd parkeret i read normal ud på et ledigt link. Det bliver et hang, når din egen TCP-stak fortsætter med at tage imod data fra den lokale app, men aldrig annoncerer et vindue, der er stort nok til, at den fjerneste peer kan sende mere. Peer'en fylder det vindue, du annoncerede, venter på en ACK og en vinduesopdatering og stopper. Så har din læser intet at returnere, og appen ser en forbindelse, der er åben og tavs.
De almindelige årsager er en stak, der skriver i socket'en uden at følge det annoncerede vindue, og en, der glemmer at sende en vinduesopdatering, efter den har tømt sin buffer. Udefra ser de identiske ud: bytes går ud, intet kommer tilbage, ingen fejl rejses. Blokering af læsetråden på parsing, DNS-opslag eller disk-I/O giver samme form for symptom af en urelateret årsag, så bekræft, at tråden faktisk er inde i read, før du giver flow control skylden.
Tjek peer'ens send-kø og det vindue, den sidst så. Hvis peer'en holder data, fordi dit vindue er nul, ligger rettelsen i din ACK-vej, ikke i TUN-læsningen.
ENOBUFS er et størrelsesproblem, ikke buffermangel
En skrivning, der returnerer ENOBUFS på en TUN-grænseflade, afviser normalt pakkelængden. Grænsefladen har en MTU, en pakke større end den kan ikke sættes i kø, og gentagne forsøg med samme pakke fejler på samme måde hver gang. Den determinisme er tegnet. En ægte buffermangel klares, når trykket falder, mens en størrelsesafvisning er ligeglad med, hvor længe du venter.
TCP har en mekanisme til netop dette, og det er det trin, folk springer over: MSS-clamp. Når du syntetiserer segmenter i brugerrummet, vælger du payload-størrelsen, og tager du den fra den lokale grænseflades MTU i stedet for tunnelens MTU, udsender du segmenter, som tunnelen ikke kan bære. Clamp til den mindste af de to. Clampet dækker kun TCP, så et for stort UDP-datagram skal fragmenteres eller droppes af din kode.
Log længden af hver pakke, du skriver, mens du fejlretter. Den længde, der fejler, er svaret, og den ændrer sig ikke mellem forsøg.
Tre målinger, der skiller tilfældene
Du har ikke brug for en debugger til nogen af de to fejl. Tre observationer er nok til at lede dig til det rigtige afsnit, og de tager mindre tid end en genopbygning.
| 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 række passer, er løkken selv fin, og problemet sidder, hvor tunnelen ikke kan se det: en ubeskyttet transport-socket, der bærer din egen VPN-trafik tilbage i tunnelen, hvilket normalt viser sig som en forbindelse, der lykkes og derefter ikke leverer noget.
Ret det VVS, som begge veje afhænger af
Ingen af symptomerne er værd at jagte, før grænsefladen er bygget korrekt. De builder-kald, der betyder noget, er adressen, ruten, MTU'en og samtykket, i den rækkefølge:
Builder()
.addAddress(10.8.0.2, 32)
.addRoute(0.0.0.0, 0)
.setMtu(1400)
.establish()Sæt MTU'en til tunnelens reelle værdi i stedet for at lade den stå på det, eksemplet brugte. Alt, du senere skriver, er begrænset af det tal, og uoverensstemmelse mellem den MTU, du sætter, og den MSS, du clamper til, er den mest almindelige måde at genindføre skrivefejlen efter en rettelse. Beskyt derefter den socket, der bærer trafik til selve VPN-serveren. Uden protect routes disse pakker tilbage i din egen tunnel, og forbindelsen fuldføres aldrig, hvilket ligner en blokeret læsning, men er en routing-løkke.
Samtykke kan ikke omgås. En service startet uden at prepare returnerer null får ingen grænseflade, og establish returnerer null i stedet for at kaste, så tjek beskrivelsen, før du starter løkken. VpnService-referencen er stedet at bekræfte builder-kontrakten i stedet for at kopiere et eksempel.
Verificér én retning ad gangen
Send et UDP-ekko gennem tunnelen, og hold øje med pakkelængder, før du rører TCP. Åbn derefter et handshake og bekræft, at segmenterne bliver under den MSS, du beregnede. Tæl pakker frem for forbindelser, så en blokering vises som et hul i tælleren i stedet for en fastlåst socket. Hvis læsninger stopper efter en byge af skrivninger, er vinduet den mistænkte; hvis skrivninger fejler med det samme, er det længden.
Genstart derefter enheden og gentag én gang, fordi en VpnService, der kun virker indtil den første netværksændring, er et routingproblem snarere end et flow control-problem.