Due sintomi compaiono nello stesso ciclo di VpnService e vengono attribuiti alla stessa cosa: un lettore che non torna mai e una scrittura che fallisce con ENOBUFS. Non sono varianti di un solo bug. Una lettura bloccata è un disaccordo sul controllo di flusso con il peer; una scrittura fallita è un pacchetto che l'interfaccia non accetterà. Se li trattate come un solo problema, passerete una giornata a regolare buffer che non sono mai stati coinvolti. La divisione qui sotto è come decido quale metà del ciclo strumentare per prima.

Un ciclo, due punti di guasto indipendenti

Il vostro servizio legge un pacchetto IP dal descrittore di file TUN, lo ispeziona e scrive una risposta nello stesso descrittore. Tutto ciò che si rompe su quel percorso emerge a una di queste due chiamate, ed è per questo che entrambe vengono segnalate come tunnel rotto. Le chiamate hanno contratti diversi. Una lettura attende che il kernel vi consegni un pacchetto, e una scrittura consegna al kernel un pacchetto che ha il diritto di rifiutare.

Le letture sono la metà facile da ragionare. Una lettura che non restituisce nulla o aspetta traffico che non arriva mai o aspetta perché il peer ha smesso di inviare.

Una lettura bloccata è un disaccordo sul controllo di flusso

La lettura di un dispositivo TUN si blocca finché un pacchetto non è disponibile. È un comportamento voluto, ed è per questo che un thread fermo dentro read sembra normale su un collegamento inattivo. Diventa un blocco quando il vostro stack TCP continua ad accettare dati dall'app locale ma non annuncia mai una finestra sufficiente perché il peer remoto ne invii altri. Il peer riempie la finestra che avete annunciato, attende un ACK e un aggiornamento di finestra, e si ferma. Il vostro lettore non ha allora nulla da restituire, e l'app vede una connessione aperta e silenziosa.

Le cause comuni sono uno stack che scrive nella socket senza seguire la finestra annunciata, e uno che dimentica di inviare un aggiornamento di finestra dopo aver svuotato il proprio buffer. Dall'esterno sembrano identici: i byte escono, nulla torna, nessun errore viene sollevato. Bloccare il thread di lettura su parsing, risoluzione DNS o I/O su disco produce la stessa forma di sintomo per una ragione non correlata, quindi confermate che il thread sia davvero dentro read prima di accusare il controllo di flusso.

Controllate la coda di invio del peer e la finestra che ha visto per ultima. Se il peer trattiene dati perché la vostra finestra è zero, la correzione è nel vostro percorso ACK, non nella lettura TUN.

ENOBUFS è un problema di dimensione, non una mancanza di buffer

Una scrittura che restituisce ENOBUFS su un'interfaccia TUN di solito rifiuta la lunghezza del pacchetto. L'interfaccia ha una MTU, un pacchetto più grande non può essere accodato, e ritentare lo stesso pacchetto fallisce allo stesso modo ogni volta. Quel determinismo è l'indizio. Una vera mancanza di buffer si esaurisce quando la pressione cala, mentre un rifiuto per dimensione non si cura di quanto aspettate.

TCP ha un meccanismo proprio per questo, ed è il passo che le persone saltano: il clamp MSS. Quando sintetizzate segmenti nello spazio utente scegliete la dimensione del payload, e prenderla dalla MTU dell'interfaccia locale invece che dalla MTU del tunnel emette segmenti che il tunnel non può trasportare. Limitate alla minore delle due. Il clamp copre solo TCP, quindi un datagramma UDP troppo grande deve essere frammentato o scartato dal vostro codice.

Registrate la lunghezza di ogni pacchetto che scrivete mentre fate debug. La lunghezza che fallisce è la risposta, e non cambierà tra un tentativo e l'altro.

Tre misure che separano i casi

Non serve un debugger per nessuno dei due guasti. Tre osservazioni bastano per indirizzarvi alla sezione giusta, e richiedono meno tempo di una ricompilazione.

What you seeWhere to look
Reader parked, peer send queue empty, last advertised window zeroWindow updates in your ACK path
Write fails on one length and succeeds on a smaller oneMSS clamp and tunnel MTU
Both directions look fine, app still sees no dataProtected socket and routing loop

Se la terza riga corrisponde, il ciclo stesso è a posto e il problema sta dove il tunnel non può vederlo: una socket di trasporto non protetta che riporta il vostro traffico VPN dentro il tunnel, cosa che di solito si presenta come una connessione che riesce e poi non consegna nulla.

Riparate l'impianto da cui dipendono entrambi i percorsi

Nessun sintomo vale la pena di essere inseguito finché l'interfaccia non è costruita correttamente. Le chiamate del builder che contano sono l'indirizzo, la rotta, la MTU e il consenso, in quest'ordine:

Builder()
  .addAddress(10.8.0.2, 32)
  .addRoute(0.0.0.0, 0)
  .setMtu(1400)
  .establish()

Impostate la MTU al valore reale del tunnel invece di lasciarla su quello usato dall'esempio. Tutto ciò che scriverete dopo è limitato da quel numero, e una discrepanza tra la MTU che impostate e l'MSS a cui limitate è il modo più comune di reintrodurre il guasto di scrittura dopo una correzione. Poi proteggete la socket che trasporta il traffico verso il server VPN stesso. Senza protect quei pacchetti vengono instradati di nuovo nel vostro tunnel e la connessione non si completa mai, il che sembra una lettura bloccata ma è un ciclo di routing.

Il consenso non si può aggirare. Un servizio avviato senza che prepare restituisca null non ottiene alcuna interfaccia, e establish restituisce null invece di sollevare un'eccezione, quindi controllate il descrittore prima di avviare il ciclo. Il riferimento VpnService è il posto dove confermare il contratto del builder invece di copiare un esempio.

Verificate una direzione alla volta

Inviate un echo UDP attraverso il tunnel e osservate le lunghezze dei pacchetti prima di toccare TCP. Poi aprite un handshake e confermate che i segmenti restano sotto l'MSS che avete calcolato. Contate i pacchetti invece delle connessioni, così un blocco appare come un vuoto nel contatore invece che come una socket appesa. Se le letture si fermano dopo una raffica di scritture, la finestra è la sospetta; se le scritture falliscono subito, lo è la lunghezza.

Poi riavviate il dispositivo e ripetete una volta, perché un VpnService che funziona solo fino al primo cambio di rete è un problema di routing e non di controllo di flusso.

Articoli correlati