แอปของคุณเรียก 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

บทความที่เกี่ยวข้อง