Empat asumsi tentang Android VpnService pernah menghabiskan sebagian besar waktu seminggu saya, dan tak satu pun soal protokol jaringan. Aplikasi bilang terhubung, ikon kunci duduk di bilah status, dan trafik tetap keluar lewat rute normal. Saya terus menganggap tunnel itu milik saya sampai saya menghentikannya, bahwa rute yang saya tambahkan adalah rute yang saya dapat, dan bahwa memanggil stopSelf akan menurunkan antarmuka bersamanya. Layanannya berperilaku persis seperti yang didokumentasikan. Model mental sayalah bagian yang rusak.

Mitos 1: tunnel tetap hidup sampai Anda menghentikannya

Android memegang satu antarmuka VPN pada satu waktu. Pasang yang kedua dan platform tidak mengantrekannya: ia menonaktifkan milik Anda dan memanggil onRevoke() pada layanan Anda. Jika kode Anda hanya membongkar saat tombol ditekan, pengambilalihan meninggalkan deskriptor dan thread pembaca, dan mulai berikutnya mewarisi keduanya.

Referensi VpnService menyatakan jelas bahwa prepare() mengembalikan null ketika aplikasi sudah punya persetujuan, dan Intent ketika pengguna masih harus menyetujuinya. Persetujuan itu hanya sampai jika Anda mengirimkan intent melalui startActivityForResult(). Lewatkan penyerahan itu dan establish() mengembalikan null, yang terbaca sebagai kegagalan senyap, bukan masalah izin.

Always-on VPN menambahkan pemilik yang tidak Anda kendalikan. Sistem dapat menghidupkan lagi tunnel setelah boot atau setelah crash, dan ia mendahului aplikasi VPN yang dijalankan pengguna secara manual. Bangun onRevoke() agar menutup sumber daya yang sama seperti penutupan normal Anda, dan perlakukan itu sebagai jalur henti utama, bukan kasus sudut. Pengguna berganti aplikasi VPN; kode Anda harus bertahan dari itu.

Mitos 2: addRoute menangkap persis apa yang Anda daftarkan

Rute menentukan paket mana yang mencapai deskriptor Anda, dan kesalahan cakupan menghasilkan kasus menjengkelkan ketika sebagian trafik masuk tunnel dan sebagian tidak. addRoute("0.0.0.0", 0) mengambil seluruh IPv4, yang berarti setiap paket kini datang kepada Anda dan harus diteruskan atau dibuang. Sebuah /32 untuk satu host hampir tidak mengambil apa pun, dan semua yang lain berperilaku seolah VPN Anda tidak pernah terpasang.

Keluarga dihitung terpisah. Rute hanya-IPv4 membiarkan IPv6 di jaringan dasar, jadi host dua-tumpuk masih bisa menyelesaikan nama dan terhubung di luar tunnel sambil terlihat terhubung sempurna di dalamnya.

DNS mengikuti logika yang sama. Server yang ditambahkan dengan addDnsServer() masuk ke aplikasi yang memakai resolver tunnel, sedangkan aplikasi dengan endpoint DNS terenkripsi sendiri atau Private DNS yang disetel ke nama host mengabaikannya sepenuhnya. Ketika nama gagal untuk satu aplikasi dan berhasil untuk yang lain, periksa jalur resolver sebelum menyentuh satu rute pun.

Mitos 3: aturan per aplikasi adalah sakelar yang bisa Anda balik ke dua arah

addDisallowedApplication() dan addAllowedApplication() adalah dua mode, bukan dua filter. Builder menerima satu model per tunnel, jadi aplikasi yang ingin semua kecuali tiga paket memakai daftar terlarang dan membiarkan daftar yang diizinkan kosong. Keduanya saling eksklusif, dan orang mengetahuinya saat runtime setelah layar pengaturan sudah menawarkan kedua sakelar.

Visibilitas paket juga menggigit. Di Android 11 dan setelahnya Anda hanya bisa menyebut paket yang Anda lihat, jadi daftar pengecualian yang dibangun dari kueri yang tidak boleh dijalankan aplikasi Anda akan diam-diam melewatkan entri. Bandingkan daftar itu dengan apa yang benar-benar dikembalikan manajer paket sebelum memercayainya.

Profil kerja membagi audiens lagi. Tunnel yang dimulai di dalam profil mencakup profil itu dan aplikasinya, sedangkan sisi pribadi mempertahankan konektivitasnya sendiri. Itu keputusan cakupan yang diambil sekali, dan tempatnya di antarmuka, bukan di tiket dukungan.

Mitos 4: stopSelf mengakhiri tunnel

Tidak. Antarmuka hidup di deskriptor yang dikembalikan establish(), dan objek layanan bisa mati sementara deskriptor itu tetap terbuka. Tutup ParcelFileDescriptor lebih dulu, lepaskan referensinya, gabungkan atau hentikan thread pembaca dan penulis, lalu panggil stopForeground(true) dan stopSelf() dalam urutan itu.

Deskriptor yang terlupakan adalah alasan biasa sebuah layanan tampak bertahan dari permintaan hentinya sendiri, dan itu menjelaskan rute usang yang hidup lebih lama daripada aplikasi yang menambahkannya.

Kesalahan sebaliknya sama umumnya: menutup deskriptor saat thread pembaca di tengah pembacaan, lalu membiarkan thread itu menangani pengecualian yang tidak ditulis siapa pun. Simpan urutan penutupan di satu tempat dan biarkan tindakan pengguna maupun onRevoke() memanggilnya.

Apa yang harus diperiksa ketika tunnel tampak mati

Lindungi soket berumur panjang dengan protect() sebelum tunnel naik. Tanpa itu, transport tunnel itu sendiri dirutekan kembali ke dalam tunnel dan koneksi macet dengan cara yang terlihat persis seperti kesalahan server. Uji terhadap alamat yang hanya ada di balik jaringan jauh, bukan situs publik yang menjawab dari cache atau dari jalur dasar.

Catat hasil establish(), rutenya, paket yang dikecualikan, dan alasan penutupannya. Empat baris itu memisahkan masalah persetujuan dari masalah cakupan dan dari masalah siklus hidup tanpa menangkap paket.

Artikel terkait