Android VpnService: blockierendes TUN-Lesen gegenüber ENOBUFS-Schreibfehlern
Ein Lesevorgang, der nie zurückkehrt, und ein Schreibvorgang, der mit ENOBUFS scheitert, haben entgegengesetzte Ursachen. Messen Sie das angekündigte Fenster und die Paketlänge, um die richtige Korrektur zu wählen.
Zwei Symptome treten in derselben VpnService-Schleife auf und werden demselben Umstand angelastet: ein Leser, der nie zurückkehrt, und ein Schreibvorgang, der mit ENOBUFS scheitert. Sie sind keine Varianten eines Fehlers. Ein blockiertes Lesen ist eine Uneinigkeit über die Flusskontrolle mit dem Peer; ein gescheiterter Schreibvorgang ist ein Paket, das die Schnittstelle nicht annimmt. Behandelt man beides als ein Problem, verbringt man einen Tag damit, Puffer zu justieren, die nie beteiligt waren. Die folgende Aufteilung ist, wie ich entscheide, welche Hälfte der Schleife ich zuerst instrumentiere.
Eine Schleife, zwei unabhängige Fehlerpunkte
Ihr Dienst liest ein IP-Paket aus dem TUN-Dateideskriptor, prüft es und schreibt eine Antwort in denselben Deskriptor zurück. Alles, was auf diesem Weg bricht, zeigt sich an einem dieser beiden Aufrufe, weshalb beides als defekter Tunnel gemeldet wird. Die Aufrufe haben unterschiedliche Verträge. Ein Lesen wartet darauf, dass der Kernel Ihnen ein Paket übergibt, und ein Schreiben übergibt dem Kernel ein Paket, das er ablehnen darf.
Lesen ist die leichte Hälfte zum Nachdenken. Ein Lesen, das nichts zurückgibt, wartet entweder auf Datenverkehr, der nie ankommt, oder weil der Peer aufgehört hat zu senden.
Ein blockiertes Lesen ist eine Uneinigkeit über die Flusskontrolle
Das Lesen eines TUN-Geräts blockiert, bis ein Paket verfügbar ist. Das ist beabsichtigtes Verhalten, und deshalb sieht ein Thread, der in read parkt, auf einer untätigen Verbindung normal aus. Zum Hänger wird es, wenn Ihr eigener TCP-Stack weiterhin Daten von der lokalen App annimmt, aber nie genug Fenster ankündigt, damit der entfernte Peer mehr senden kann. Der Peer füllt das angekündigte Fenster, wartet auf ein ACK und eine Fensteraktualisierung und hört auf. Dann hat Ihr Leser nichts zurückzugeben, und die App sieht eine Verbindung, die offen und still ist.
Die häufigen Ursachen sind ein Stack, der in den Socket schreibt, ohne das angekündigte Fenster zu verfolgen, und einer, der vergisst, nach dem Leeren seines Puffers eine Fensteraktualisierung zu senden. Von außen sehen sie identisch aus: Bytes gehen hinaus, nichts kommt zurück, kein Fehler wird ausgelöst. Das Blockieren des Leser-Threads bei Parsing, DNS-Auflösung oder Festplatten-I/O erzeugt dieselbe Symptomform aus einem unabhängigen Grund, also bestätigen Sie, dass der Thread tatsächlich in read steckt, bevor Sie die Flusskontrolle beschuldigen.
Prüfen Sie die Sendewarteschlange des Peers und das Fenster, das er zuletzt gesehen hat. Hält der Peer Daten zurück, weil Ihr Fenster null ist, liegt die Korrektur in Ihrem ACK-Pfad, nicht im TUN-Lesen.
ENOBUFS ist ein Größenproblem, kein Puffermangel
Ein Schreiben, das auf einer TUN-Schnittstelle ENOBUFS zurückgibt, lehnt meist die Paketlänge ab. Die Schnittstelle hat eine MTU, ein größeres Paket kann nicht eingereiht werden, und ein erneuter Versuch mit demselben Paket scheitert jedes Mal gleich. Diese Determinismus ist der Hinweis. Ein echter Puffermangel löst sich, wenn der Druck nachlässt, während eine Größenablehnung nicht kümmert, wie lange Sie warten.
TCP hat einen Mechanismus genau dafür, und es ist der Schritt, den Leute überspringen: der MSS-Clamp. Wenn Sie Segmente im User Space synthetisieren, wählen Sie die Nutzlastgröße, und wenn Sie sie aus der MTU der lokalen Schnittstelle statt aus der MTU des Tunnels nehmen, erzeugen Sie Segmente, die der Tunnel nicht tragen kann. Begrenzen Sie auf die kleinere der beiden. Der Clamp deckt nur TCP ab, ein übergroßes UDP-Datagramm muss also von Ihrem Code fragmentiert oder verworfen werden.
Protokollieren Sie beim Debuggen die Länge jedes Pakets, das Sie schreiben. Die Länge, die scheitert, ist die Antwort, und sie ändert sich zwischen Versuchen nicht.
Drei Messungen, die die Fälle trennen
Sie brauchen für keinen der beiden Fehler einen Debugger. Drei Beobachtungen reichen, um sich in den richtigen Abschnitt zu leiten, und sie dauern kürzer als ein 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 |
Passt die dritte Zeile, ist die Schleife selbst in Ordnung, und das Problem sitzt dort, wo der Tunnel es nicht sehen kann: ein ungeschützter Transport-Socket, der Ihren eigenen VPN-Verkehr zurück in den Tunnel trägt, was sich meist als Verbindung zeigt, die erfolgreich ist und dann nichts liefert.
Reparieren Sie die Leitungen, von denen beide Pfade abhängen
Keines der Symptome ist es wert, verfolgt zu werden, bevor die Schnittstelle korrekt gebaut ist. Die entscheidenden Builder-Aufrufe sind die Adresse, die Route, die MTU und die Zustimmung, in dieser Reihenfolge:
Builder()
.addAddress(10.8.0.2, 32)
.addRoute(0.0.0.0, 0)
.setMtu(1400)
.establish()Setzen Sie die MTU auf den realen Wert des Tunnels, statt sie auf dem zu belassen, was das Beispiel verwendet hat. Alles, was Sie später schreiben, ist durch diese Zahl begrenzt, und eine Diskrepanz zwischen der gesetzten MTU und der MSS, auf die Sie begrenzen, ist der häufigste Weg, den Schreibfehler nach einer Korrektur wieder einzuführen. Schützen Sie dann den Socket, der Verkehr zum VPN-Server selbst trägt. Ohne protect werden diese Pakete zurück in Ihren eigenen Tunnel geroutet, und die Verbindung kommt nie zustande, was wie ein blockiertes Lesen aussieht, aber eine Routing-Schleife ist.
Zustimmung lässt sich nicht umgehen. Ein Dienst, der gestartet wird, ohne dass prepare null zurückgibt, bekommt keine Schnittstelle, und establish gibt null zurück, statt zu werfen, prüfen Sie also den Deskriptor, bevor Sie die Schleife starten. Die VpnService-Referenz ist der Ort, um den Builder-Vertrag zu bestätigen, statt ein Beispiel zu kopieren.
Prüfen Sie jeweils eine Richtung
Senden Sie ein UDP-Echo durch den Tunnel und beobachten Sie die Paketlängen, bevor Sie TCP anfassen. Öffnen Sie dann einen Handshake und bestätigen Sie, dass die Segmente unter der berechneten MSS bleiben. Zählen Sie Pakete statt Verbindungen, damit ein Stillstand als Lücke im Zähler erscheint statt als hängender Socket. Stocken Lesen nach einem Schub von Schreibvorgängen, ist das Fenster verdächtig; scheitern Schreibvorgänge sofort, ist es die Länge.
Starten Sie dann das Gerät neu und wiederholen Sie es einmal, denn ein VpnService, der nur bis zur ersten Netzwerkänderung funktioniert, ist ein Routingproblem und kein Problem der Flusskontrolle.