การตรวจจับ VPN บน Android: ทำไม tun0 และ ping จึงทำให้เข้าใจผิด
ชื่ออินเทอร์เฟซเป็นเพียงธรรมเนียมปฏิบัติ ping พิสูจน์ว่าโฮสต์หนึ่งเข้าถึงได้ ถาม NetworkCapabilities ว่าเครือข่ายประกาศอะไร แล้วตอบสนองต่อ callback
การตรวจสอบที่แอป Android ส่วนใหญ่ใช้ตรวจจับ VPN คือชื่ออินเทอร์เฟซ และมันไม่เคยเป็นการตรวจสอบ VPN เลย มันรายงานว่ามีอินเทอร์เฟซบางอย่างที่มีสตริงที่นักพัฒนาคาดหวัง ซึ่งบนอุปกรณ์หนึ่งหมายถึง OpenVPN และอีกเครื่องหนึ่งหมายถึงสิ่งที่เกี่ยวข้องกันโดยสิ้นเชิง ping ก็เป็นหลักฐานชนิดเดียวกัน: มันพิสูจน์ว่าปลายทางหนึ่งตอบกลับ ไม่ได้พิสูจน์ว่าอุโมงค์ได้ขนส่งแพ็กเก็ต สิ่งที่แพลตฟอร์มเปิดเผยจริง ๆ คือชุดเครือข่ายและทรานสปอร์ตที่แต่ละเครือข่ายประกาศ และนั่นเป็นอีกคำถามที่มีอีกคำตอบ
ความเชื่อผิด: ถ้ามี tun0 แสดงว่า VPN ทำงานอยู่
แอป VPN บน Android คือการนำ VpnService ไปใช้ซึ่งส่งตัวอธิบายไฟล์ tun ให้ระบบ ชื่ออินเทอร์เฟซมาจากผู้ที่สร้างมัน: OpenVPN มักสร้าง tun0, PPTP และ L2TP สร้าง ppp0, WireGuard ตั้งชื่ออินเทอร์เฟซตามอุโมงค์ และอุโมงค์ IPsec อาจไม่แสดงชื่อแบบธรรมดาเลย ดังนั้นการอ่าน /proc/net/dev หรือการนับอ็อบเจกต์ NetworkInterface จึงค้นหาธรรมเนียมปฏิบัติ ไม่ใช่ข้อเท็จจริง
ทิศทางของข้อผิดพลาดทั้งสองด้านยังมีชีวิตอยู่ อุปกรณ์อาจมี tun0 สำหรับอุโมงค์ภายในที่ไม่เกี่ยวข้องกับ VPN ของผู้ใช้ และอุโมงค์ WireGuard ที่สมบูรณ์ดีอาจมองไม่เห็นสำหรับการตรวจสอบที่รู้จักเพียงสตริง tun0 ผลบวกลวงและผลลบลวงในฟังก์ชันเดียวกันทำให้ใช้เป็นเครื่องมือวินิจฉัยไม่ได้ เพราะตั๋วสนับสนุนที่บอกว่า «ไม่พบ VPN» ไม่น่าเชื่อถือในทิศทางใดเลย
การเข้าถึงก็แคบลงด้วย Android เวอร์ชันใหม่จำกัดสิ่งที่แอปธรรมดาอ่านได้จาก /proc ดังนั้นการตรวจสอบตามชื่อที่เคยทำงานบน Android 8 อาจคืนรายการว่างบนอุปกรณ์รุ่นหลังโดยไม่มีการเปลี่ยนโค้ดเลย เมื่อการตรวจสอบล้มเหลวอย่างเงียบ ๆ ไปทางด้านเปิด แอปก็สรุปว่าไม่มี VPN บนฮาร์ดแวร์ที่อุโมงค์ทำงานปกติ
ความเชื่อผิด: ping ที่สำเร็จหมายถึงอุโมงค์กำลังขนส่งทราฟฟิก
ping ไปยังโฮสต์ภายในจะสำเร็จทุกครั้งที่มีเส้นทางไปยังโฮสต์นั้น ด้วยอุโมงค์แบบแบ่ง โฮสต์อาจเข้าถึงได้ผ่าน Wi-Fi ขณะที่อุโมงค์ทำงานอยู่และกำลังทำอย่างอื่นโดยสิ้นเชิง ด้วย VPN ต่อแอป แอปที่ทำ ping อาจถูกตัดออกจากอุโมงค์ ทำให้เสียงสะท้อน ICMP ออกไปข้างนอกและกลับมา ซึ่งพิสูจน์สิ่งที่ตรงกันข้ามกับที่การตรวจสอบสันนิษฐาน
ICMP และ TCP ก็ถูกกรองแยกกัน อุโมงค์ที่อนุญาตคำขอเสียงสะท้อนแต่ทิ้งพอร์ต TCP ที่บริการต้องการจะผ่านการทดสอบ ping และล้มเหลวในแอป และนั่นเป็นกฎไฟร์วอลล์ที่อีกฝั่ง ไม่ใช่คุณสมบัติของอุปกรณ์ การอ่าน ping ที่สำเร็จอย่างซื่อตรงมีเพียงอย่างเดียวคือปลายทางนี้ตอบโปรโตคอลนี้ในขณะนี้
สิ่งที่แพลตฟอร์มรายงานจริง ๆ
ตั้งแต่ API 21 เป็นต้นมา ConnectivityManager.getAllNetworks() คืนทุกเครือข่ายที่ระบบรู้จัก และ getNetworkCapabilities(network) รายงานว่าเครือข่ายนั้นประกาศอะไร เครือข่าย VPN มี TRANSPORT_VPN การวนซ้ำทุกเครือข่ายตอบคำถามหนึ่งข้อ: มี VPN ใดทำงานอยู่บนอุปกรณ์นี้ตอนนี้หรือไม่
คำถามที่สองที่แคบกว่าต้องการการเรียกอื่น getActiveNetwork() คืนเครือข่ายที่ทราฟฟิกของแอปจะใช้ตอนนี้ และการตรวจสอบความสามารถเดียวกันบนเครือข่ายนั้นตอบว่า: ทราฟฟิกของฉันจะผ่าน VPN หรือไม่? ทั้งสองอาจไม่ตรงกัน และความไม่ตรงกันนั้นให้ข้อมูล VPN ที่ทำงานอยู่แต่ไม่ใช่ปัจจุบันชี้ไปที่การกำหนดเส้นทางต่อแอปหรือการกำหนดค่าอุโมงค์แบบแบ่ง ไม่ใช่การขาดอุโมงค์
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
ปกป้องการเรียกทั้งสอง ความสามารถอาจเป็น null สำหรับเครือข่ายที่หายไประหว่างการลงรายการกับการสอบถาม และเครือข่ายที่ใช้งานอยู่อาจเป็น null ระหว่างการส่งต่อ NET_CAPABILITY_NOT_VPN มีประโยชน์ในทิศทางตรงกันข้าม เพื่อยืนยันว่าเครือข่ายไม่ใช่อุโมงค์อย่างแน่นอน ไม่ใช่แค่ขาดแฟล็กทรานสปอร์ต VPN
NetworkInfo เป็นความเชื่อผิดเดียวกันพร้อม API
ConnectivityManager.TYPE_VPN กับ NetworkInfo เป็นรุ่นที่สองของการตรวจสอบนี้ และมันล้าสมัยด้วยเหตุผล ในเวอร์ชันสมัยใหม่อ็อบเจกต์ที่คืนสำหรับ VPN อาจเป็น null ขณะที่อุโมงค์เชื่อมต่ออยู่ ดังนั้นโค้ดที่ถือว่า null คือ «ไม่มี VPN» จะรายงานผลลบลวงอย่างเงียบ ๆ บนอุปกรณ์ที่คำตอบสำคัญที่สุด
การออกอากาศที่มักมาด้วยก็ไม่ดีกว่า CONNECTIVITY_CHANGE มีประวัติที่บันทึกไว้เกี่ยวกับการพลาดการเปลี่ยนผ่านของ VPN รวมถึงบิลด์ที่มันทำงานเฉพาะเมื่อ VPN เชื่อมต่อหรือตัดการเชื่อมต่อ แทนที่จะทำงานทุกครั้งที่เครือข่ายเปลี่ยน ตัวรับฟังที่สร้างบนมันจะสร้างตัวแปรสถานะที่ค่อย ๆ ออกห่างจากมุมมองของแพลตฟอร์มจนถึงการเชื่อมต่อใหม่เต็มรูปแบบครั้งถัดไป
ตอบสนองต่อ callback แทนการสำรวจเป็นระยะ
ลงทะเบียน NetworkRequest ที่ต้องการ TRANSPORT_VPN และลบ NET_CAPABILITY_NOT_VPN แล้วเรียก registerNetworkCallback ด้วยคำขอนั้น จากนั้น onAvailable และ onLost จะบอกคุณว่าเมื่อใดการประกาศเปลี่ยน และนั่นคือเหตุการณ์ที่คุณสนใจจริง ๆ ยกเลิกการลงทะเบียน callback เมื่อคอมโพเนนต์หยุด มิฉะนั้นแอปจะถือการอ้างอิงถึงหน้าจอที่ตายแล้วและ callback จะยังคงทำงานต่อไป
NetworkRequest req = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(req, callback);
สิ่งนี้ต้องใช้ ACCESS_NETWORK_STATE ซึ่งเป็นสิทธิ์ปกติ: ไม่ต้อง root ไม่มีกล่องโต้ตอบความยินยอมของผู้ใช้ ไม่ต้องมีสิทธิ์ VPN การสำรวจเป็นระยะด้วยตัวจับเวลาคือทางเลือกที่ผู้คนหันไปหา และมันกินแบตเตอรี่ขณะที่ยังพลาดการเปลี่ยนผ่านที่สั้นกว่าช่วงเวลา ซึ่งเป็นการเปลี่ยนผ่านที่ผู้ใช้รายงานนั่นเอง
สิ่งที่คำตอบยังบอกคุณไม่ได้
API เหล่านี้ไม่มีตัวใดรายงานว่าอุโมงค์แข็งแรงหรือไม่ ยุติที่เซิร์ฟเวอร์ใด หรือทราฟฟิกของแอปใดแอปหนึ่งอยู่ข้างในหรือไม่ เครือข่าย VPN อาจมีอยู่โดยไม่มีเส้นทางที่ใช้ได้ และอุโมงค์ต่อแอปอาจตัดแอปที่ถามคำถามออก
ดังนั้นบันทึกสิ่งที่การตรวจสอบเห็น ไม่ใช่แค่ค่าบูลีน บันทึกตัวระบุเครือข่าย ทรานสปอร์ตที่ประกาศ และเครือข่ายนั้นใช้งานอยู่หรือไม่ รายงานที่บอกว่า «ไม่พบ VPN» ใช้ไม่ได้ ขณะที่รายงานที่บอกว่าเครือข่ายประกาศ TRANSPORT_VPN แต่ getActiveNetwork() คืน Wi-Fi ชี้ตรงไปที่การกำหนดเส้นทางหรือรายการยกเว้น และนั่นคือการแก้ไขที่ใครบางคนลงมือทำได้