การป้องกัน VpnService ล้มเหลวเมื่อมี VPN อื่นทำงานอยู่แล้ว
Android อนุญาตอินเทอร์เฟซ VPN หนึ่งรายการต่อผู้ใช้ ให้รันการตรวจสอบ prepare, establish, protect และความเป็นเจ้าของตามลำดับ แล้วเลือกการเพิกถอนหรือการยกเว้นรายแอป
แอปของคุณเรียก VpnService.prepare ได้รับ null สร้างอุโมงค์ และ descriptor ดูเหมือนถูกต้อง แต่ไม่มีอะไรไปถึงเซิร์ฟเวอร์ หรือ prepare คืนค่า Intent ผู้ใช้อนุมัติ และ establish ก็ยังล้มเหลว ผลลัพธ์ทั้งสองมักหมายถึงสิ่งเดียวกัน คือแอปอื่นเป็นเจ้าของอินเทอร์เฟซ VPN ของผู้ใช้รายนี้อยู่แล้ว และการเรียก protect ของคุณกำลังทำเครื่องหมายซ็อกเก็ตที่อุโมงค์ของคุณใช้ไม่ได้ ให้ทำการตรวจสอบด้านล่างตามลำดับและหยุดที่ข้อแรกที่ล้มเหลว เพราะขั้นตอนถัดไปไม่สามารถซ่อมขั้นตอนก่อนหน้าได้
ตรวจสอบช่อง VPN ก่อนที่จะแตะ protect
Android ให้ผู้ใช้แต่ละราย อินเทอร์เฟซ VPN ที่ใช้งานได้เพียงหนึ่งเดียว นี่เป็นกฎของแพลตฟอร์ม ไม่ใช่สิทธิ์ที่แอปของคุณจะขอได้ ดังนั้นไคลเอนต์สองตัวที่ต่างก็อยากเป็นอุโมงค์จึงอยู่ร่วมกันไม่ได้
VPN ที่เปิดตลอดเวลาซึ่งตั้งไว้สำหรับแอปอื่นเป็นกรณีที่ยากที่สุด เพราะระบบคงการเชื่อมต่อของแอปนั้นไว้และปฏิเสธการยึดครอง ตัวกรองทราฟฟิกอย่าง AdGuard ไคลเอนต์องค์กร หรือ VPN สำหรับผู้บริโภคอาจครองช่องนั้นอยู่ขณะที่ผู้ใช้ลืมไปแล้วว่าติดตั้งไว้ ขอให้ผู้ใช้เปิดการตั้งค่าและอ่านรายการที่แสดงใต้ VPN ชื่อที่อยู่ตรงนั้นคือเจ้าของที่คุณกำลังแข่งขันด้วย จนกว่ารายการนั้นจะถูกตัดการเชื่อมต่อหรือการตั้งค่าเปิดตลอดเวลาจะถูกล้าง VpnService ของคุณก็เป็นอินเทอร์เฟซไม่ได้
ขั้นที่ 1: อ่านว่า prepare คืนอะไร
VpnService.prepare ตอบคำถามเดียวว่า แอปนี้เป็น VPN ได้หรือไม่ มันคืนค่า null เมื่อมีความยินยอมอยู่แล้ว และคืนค่า Intent เมื่อผู้ใช้ยังต้องอนุมัติ ซึ่งคุณต้องเปิดด้วย startActivityForResult ให้ถือว่าการคืนค่า null เป็นสิทธิ์ในการดำเนินการต่อ ไม่ใช่หลักฐานว่า establish จะสำเร็จ
Intent intent = VpnService.prepare(context);
if (intent != null) {
startActivityForResult(intent, REQUEST_VPN);
}ถ้าเส้นทาง Intent กลับมาและผู้ใช้ปฏิเสธ prepare จะยังคืนค่า Intent ในการพยายามครั้งถัดไป บันทึกรหัสผลลัพธ์แทนการลองซ้ำในลูป เพราะความยินยอมที่ถูกปฏิเสธเป็นการตัดสินใจของผู้ใช้ ไม่ใช่ข้อผิดพลาดชั่วคราว
ขั้นที่ 2: ยืนยันว่าอินเทอร์เฟซเป็นของแอปคุณ
หลัง establish แล้ว ParcelFileDescriptor ที่คุณได้รับคือหลักฐานความเป็นเจ้าของ และควรตรวจสอบก่อนทำงานกับซ็อกเก็ตใด ๆ descriptor ที่เป็น null หรือถูกปิดไปแล้วหมายความว่าอินเทอร์เฟซไม่เคยถูกสร้าง แม้จะไม่มีข้อยกเว้นใดโผล่ขึ้นมา เปรียบเทียบชื่ออินเทอร์เฟซจาก builder กับชื่อที่ระบบรายงานสำหรับ VPN ที่ใช้งานอยู่ โดยใช้ dumpsys บนอุปกรณ์ทดสอบ ถ้าแตกต่างกัน อุโมงค์ที่คุณสร้างไม่ใช่อุโมงค์ที่ขนส่งทราฟฟิก
adb shell dumpsys connectivityความไม่ตรงกันแบบเดียวกันนี้อธิบายไอคอนที่เชื่อมต่อแต่ทราฟฟิกตาย ระบบแสดงอุโมงค์ของแอปอื่น ขณะที่โค้ดของคุณถือ descriptor ของอินเทอร์เฟซที่มันไม่ได้เป็นเจ้าของ
ขั้นที่ 3: เรียก protect ก่อน connect
เมื่ออินเทอร์เฟซมีอยู่ ซ็อกเก็ตที่แอปของคุณเปิดจะถูกส่งเข้าอุโมงค์โดยค่าเริ่มต้น การขนส่งภายนอกทำตามกฎนั้นไม่ได้ ไม่อย่างนั้นแพ็กเก็ตที่ห่อหุ้มจะวนกลับเข้าอินเทอร์เฟซ ดังนั้น VpnService protect จะทำเครื่องหมายซ็อกเก็ตให้ข้าม VPN และใช้เครือข่ายที่อยู่ ด้านล่าง การเรียกคืนค่า boolean และการเพิกเฉยต่อมันคือทางที่ทำให้คุณได้ซ็อกเก็ตที่ดูแข็งแรงแต่ไม่ได้ขนส่งอะไรเลย
เรียก protect ก่อน connect หรือ bind ไม่ใช่หลังจากนั้น การป้องกันต้องอยู่ก่อนที่ซ็อกเก็ตจะถูกเชื่อมต่อหรือผูก เพราะซ็อกเก็ตที่เชื่อมต่อแล้วได้เลือกเส้นทางของมันแล้ว และ protect ที่ตามมาทีหลังไม่ย้ายมันอย่างเชื่อถือได้ ถ้า protect คืนค่า false ให้หยุดและตรวจสอบความเป็นเจ้าของใหม่ เพราะมันล้มเหลวเมื่ออินเทอร์เฟซไม่ใช่ของแอปคุณ ซึ่งเป็นสภาพที่ VPN อื่นทิ้งไว้พอดี การคืนค่า false เป็นสัญญาณให้กลับไปขั้นที่ 1 ไม่ใช่ให้ลองซ็อกเก็ตเดิมอีกครั้ง
DatagramSocket socket = new DatagramSocket();
if (!vpnService.protect(socket)) {
return;
}
socket.connect(serverAddress, serverPort);ขั้นที่ 4: ผูกโปรเซสเมื่อซ็อกเก็ตไม่สะดวก
การขนส่งบางอย่างไม่ใช่ซ็อกเก็ตเดียว ไลบรารีอาจเปิดหลายตัว หรือคุณอาจต้องการให้ทุกการเชื่อมต่อในโปรเซสช่วยใช้ทางออกเดียวกัน ConnectivityManager.bindProcessToNetwork ผูกโปรเซสกับเครือข่ายที่เลือก และ setProcessDefaultNetwork ตัวเก่าก็ทำแบบเดียวกันก่อนจะเลิกใช้ ขอการเชื่อมต่อที่อยู่ ด้านล่าง ด้วย NET_CAPABILITY_NOT_VPN เพื่อให้การผูกเลือกเครือข่ายใต้อุโมงค์ของคุณ ไม่ใช่ตัวอุโมงค์เอง
NetworkRequest request = new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();การผูกโปรเซสไม่ใช่ VPN ตัวที่สอง มันเลือกทางออกให้โปรเซสนั้น ซึ่งมีประโยชน์กับการขนส่งของคุณและไร้ประโยชน์กับการรันสองอุโมงค์พร้อมกัน
ซ่อมอุปกรณ์ของผู้ใช้ ไม่ใช่เส้นทางของโค้ด
ไม่มี API ใดให้สองแอปแชร์ช่อง VPN ได้ ดังนั้นการแก้ไขจึงขึ้นกับสถานะของอุปกรณ์ เสนอทางที่ชัดเจนสองทางให้ผู้ใช้ คือตัดการเชื่อมต่อหรือล้างการตั้งค่าเปิดตลอดเวลาของแอปอื่นเพื่อให้ prepare ของคุณเข้าแทนได้ หรือเปิดการตั้งค่ารายแอปของแอปนั้นแล้วยกเว้นแอปคุณจากอุโมงค์ของมัน การยกเว้นจะทำให้ตัวกรองยังทำงานกับอย่างอื่นทั้งหมดและให้แอปคุณใช้การเชื่อมต่อปกติ ซึ่งมักเป็นผลลัพธ์ที่ผู้ใช้ต้องการจริง ๆ
เขียนทั้งสองทางลงในเอกสารสนับสนุนของคุณ เพราะผู้ใช้ไม่ค่อยรู้ว่าแอปใดเป็นเจ้าของ VPN ขอให้พวกเขาส่งชื่อ VPN ที่ใช้งานอยู่จากการตั้งค่าและผลลัพธ์ของ prepare, establish และ protect จากบันทึกของคุณ แล้วคุณจะบอกได้ภายในหนึ่งนาทีว่าขั้นตอนใดล้มเหลว จากนั้นตรวจสอบด้วยทราฟฟิกจริง คือการค้นหาชื่อ การเชื่อมต่อ TCP และคำขอหนึ่งรายการผ่านอุโมงค์ ไม่ใช่แค่ไอคอนที่เชื่อมต่อ descriptor ที่ถูกต้องและตัวบ่งชี้สีเขียวพิสูจน์ความเป็นเจ้าของได้อย่างดีที่สุด ไม่เคยพิสูจน์ว่าการขนส่งรอดพ้นกฎการกำหนดเส้นทาง
ถ้าการตรวจสอบทั้งสี่ผ่านและทราฟฟิกยังตาย ความเป็นเจ้าของไม่ใช่ปัญหาของคุณอีกต่อไป ไปที่การกำหนดเส้นทางและการเข้าถึงเซิร์ฟเวอร์ และทดสอบข้อมูลรับรองเดิมจากอุปกรณ์ที่ไม่ได้ติดตั้งแอป VPN