Android VpnService: pembacaan TUN yang macet versus galat penulisan ENOBUFS
Pembacaan yang tidak pernah kembali dan penulisan yang gagal dengan ENOBUFS punya penyebab berlawanan. Ukur jendela yang diumumkan dan panjang paket untuk memilih perbaikan.
Dua gejala muncul di loop VpnService yang sama dan disalahkan pada hal yang sama: pembaca yang tidak pernah kembali, dan penulisan yang gagal dengan ENOBUFS. Keduanya bukan variasi dari satu bug. Pembacaan yang macet adalah ketidaksepakatan soal kontrol aliran dengan peer; penulisan yang gagal adalah paket yang tidak akan diterima antarmuka. Jika Anda memperlakukannya sebagai satu masalah, Anda akan menghabiskan sehari menyetel buffer yang tidak pernah terlibat. Pembagian di bawah ini adalah cara saya memutuskan separuh loop mana yang diinstrumentasi lebih dulu.
Satu loop, dua titik kegagalan independen
Layanan Anda membaca paket IP dari deskriptor berkas TUN, memeriksanya, dan menulis balasan kembali ke deskriptor yang sama. Semua yang rusak di jalur itu muncul di salah satu dari dua panggilan tersebut, itulah sebabnya keduanya dilaporkan sebagai terowongan rusak. Kedua panggilan punya kontrak berbeda. Pembacaan menunggu kernel menyerahkan paket kepada Anda, dan penulisan menyerahkan paket yang boleh ditolak kernel.
Pembacaan adalah separuh yang mudah untuk dipikirkan. Pembacaan yang tidak mengembalikan apa pun entah menunggu lalu lintas yang tidak pernah datang atau menunggu karena peer berhenti mengirim.
Pembacaan yang macet adalah ketidaksepakatan kontrol aliran
Membaca perangkat TUN memblokir sampai ada paket tersedia. Itu perilaku yang dirancang, dan itulah sebabnya utas yang terparkir di dalam read tampak normal pada tautan yang menganggur. Itu menjadi macet ketika tumpukan TCP Anda sendiri terus menerima data dari aplikasi lokal tetapi tidak pernah mengumumkan jendela yang cukup bagi peer jarak jauh untuk mengirim lebih banyak. Peer mengisi jendela yang Anda umumkan, menunggu ACK dan pembaruan jendela, lalu berhenti. Pembaca Anda kemudian tidak punya apa pun untuk dikembalikan, dan aplikasi melihat koneksi yang terbuka dan senyap.
Penyebab umumnya adalah tumpukan yang menulis ke soket tanpa melacak jendela yang diumumkan, dan yang lupa mengirim pembaruan jendela setelah mengosongkan buffer-nya. Dari luar keduanya tampak identik: bita keluar, tidak ada yang kembali, tidak ada galat yang dimunculkan. Memblokir utas pembaca pada penguraian, resolusi DNS, atau I/O disk menghasilkan bentuk gejala yang sama karena alasan tak terkait, jadi pastikan utas benar-benar di dalam read sebelum menyalahkan kontrol aliran.
Periksa antrean kirim peer dan jendela yang terakhir dilihatnya. Jika peer menahan data karena jendela Anda nol, perbaikannya ada di jalur ACK Anda, bukan di pembacaan TUN.
ENOBUFS adalah masalah ukuran, bukan kekurangan buffer
Penulisan yang mengembalikan ENOBUFS pada antarmuka TUN biasanya menolak panjang paket. Antarmuka punya MTU, paket yang lebih besar tidak dapat dimasukkan ke antrean, dan mencoba ulang paket yang sama gagal dengan cara yang sama setiap kali. Determinisme itu adalah petunjuknya. Kekurangan buffer yang sebenarnya hilang saat tekanan turun, sedangkan penolakan ukuran tidak peduli berapa lama Anda menunggu.
TCP punya mekanisme tepat untuk ini, dan inilah langkah yang orang lewati: clamp MSS. Saat Anda menyintesis segmen di ruang pengguna, Anda memilih ukuran muatan, dan mengambilnya dari MTU antarmuka lokal alih-alih MTU terowongan memancarkan segmen yang tidak dapat dibawa terowongan. Batasi ke yang lebih kecil dari keduanya. Clamp hanya mencakup TCP, jadi datagram UDP yang terlalu besar harus difragmentasi atau dibuang oleh kode Anda.
Catat panjang setiap paket yang Anda tulis saat men-debug. Panjang yang gagal adalah jawabannya, dan itu tidak akan berubah di antara percobaan.
Tiga pengukuran yang memisahkan kasus
Anda tidak perlu debugger untuk salah satu dari dua kegagalan itu. Tiga pengamatan cukup untuk mengarahkan Anda ke bagian yang tepat, dan itu memakan waktu lebih singkat daripada membangun ulang.
| 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 |
Jika baris ketiga cocok, loop itu sendiri baik-baik saja dan masalahnya ada di tempat terowongan tidak dapat melihatnya: soket transport tanpa pelindung yang membawa lalu lintas VPN Anda sendiri kembali ke terowongan, yang biasanya muncul sebagai koneksi yang berhasil lalu tidak mengirimkan apa pun.
Perbaiki saluran yang menjadi sandaran kedua jalur
Tidak ada gejala yang layak dikejar sampai antarmuka dibangun dengan benar. Panggilan builder yang penting adalah alamat, rute, MTU, dan persetujuan, dalam urutan itu:
Builder()
.addAddress(10.8.0.2, 32)
.addRoute(0.0.0.0, 0)
.setMtu(1400)
.establish()Setel MTU ke nilai sebenarnya terowongan alih-alih membiarkannya pada nilai yang dipakai contoh. Semua yang Anda tulis nanti dibatasi oleh angka itu, dan ketidakcocokan antara MTU yang Anda setel dan MSS yang Anda batasi adalah cara paling umum untuk memunculkan kembali kegagalan penulisan setelah perbaikan. Lalu lindungi soket yang membawa lalu lintas ke server VPN itu sendiri. Tanpa protect, paket itu dirutekan kembali ke terowongan Anda sendiri dan koneksi tidak pernah selesai, yang tampak seperti pembacaan macet tetapi sebenarnya loop perutean.
Persetujuan tidak bisa dilewati. Layanan yang dimulai tanpa prepare mengembalikan null tidak mendapat antarmuka, dan establish mengembalikan null alih-alih melempar, jadi periksa deskriptor sebelum memulai loop. Referensi VpnService adalah tempat untuk memastikan kontrak builder alih-alih menyalin contoh.
Verifikasi satu arah sekaligus
Kirim gema UDP melalui terowongan dan amati panjang paket sebelum menyentuh TCP. Lalu buka handshake dan pastikan segmen tetap di bawah MSS yang Anda hitung. Hitung paket, bukan koneksi, agar kemacetan muncul sebagai celah di penghitung alih-alih soket menggantung. Jika pembacaan berhenti setelah ledakan penulisan, jendelalah tersangkanya; jika penulisan gagal seketika, panjangnya.
Lalu reboot perangkat dan ulangi sekali, karena VpnService yang hanya berfungsi sampai perubahan jaringan pertama adalah masalah perutean, bukan kontrol aliran.