มีสองวิธีที่จะกันไม่ให้แพ็กเก็ตของทันเนลเองกลับเข้าไปในทันเนล: ปกป้องซ็อกเก็ตที่ใช้ขนส่ง หรือนำทั้งแอปออกจาก VPN ทั้งสองวิธีแก้ปัญหาที่ต่างกัน และการเลือกวิธีที่กว้างกว่าเพียงเพราะต่อสายได้ง่ายกว่านั้นคือสาเหตุที่แอปสูญเสียการเข้าถึงเครือข่ายท้องถิ่นและ DNS แบบแยกโดยไม่รู้ตัว เริ่มจากสิ่งที่ต้องอยู่ภายนอกทันเนล แล้วเลือกขอบเขตที่เล็กที่สุดที่ทำได้

สิ่งที่ protect() นำออกจากทันเนล

VpnService.protect() เป็นการเรียกใช้ต่อซ็อกเก็ต คุณส่งซ็อกเก็ตหรือตัวระบุที่พาทันเนลให้มัน แล้ว Android จะทำเครื่องหมายซ็อกเก็ตนั้นให้การรับส่งข้อมูลใช้เครือข่ายพื้นฐานแทนอินเทอร์เฟซ VPN ทุกอย่างอื่นที่แอปส่งยังคงไปตามเส้นทาง VPN ปกติ

ขอบเขตที่แคบนั้นคือประเด็น ซ็อกเก็ตที่ได้รับการปกป้องไม่ได้เปลี่ยนวิธีที่ไคลเอนต์ HTTP ของคุณแปลงชื่อ ซึ่งอินเทอร์เฟซที่การเรียกวิเคราะห์ของคุณใช้ หรือสิ่งที่เกิดขึ้นกับการเชื่อมต่อที่สองที่เปิดภายหลัง

สามเงื่อนไขที่กำหนดขอบเขต

เงื่อนไขแรกคือมีเพียงการขนส่งของทันเนลเท่านั้นที่ต้องออกไป ถ้าการรับส่งอื่นของแอปยังควรผ่าน VPN การปกป้องซ็อกเก็ตคือขอบเขตที่ถูกต้อง เงื่อนไขที่สองคือคุณควบคุมการสร้างซ็อกเก็ตหรือไม่ ไลบรารีที่เปิดซ็อกเก็ตของตัวเองลึกอยู่ภายในไม่เปิดที่ให้คุณเรียก protect() เว้นแต่จะเปิดฮุกหรือรับซ็อกเก็ตที่สร้างไว้แล้ว เงื่อนไขที่สามคือแอปต้องเข้าถึงเครือข่ายท้องถิ่นหรือพอร์ทัลที่ดักไว้โดยตรงหรือไม่ ซึ่งเป็นข้อกำหนดที่ต่างจากการทำให้ทันเนลยังมีชีวิต

การยกเว้นแอปด้วย addDisallowedApplication() คือเครื่องมือที่ทื่อ มันนำทุกการเชื่อมต่อที่แอปทำออกจาก VPN รวมถึงรายการที่คุณต้องการตรวจสอบ และบนอุปกรณ์บางรุ่นยังเปลี่ยนพฤติกรรมการค้นพบในเครือข่ายท้องถิ่นด้วย

ทางเลือกที่สามคือผูกการขนส่งเข้ากับเครือข่ายที่เลือกด้วย bindProcessToNetwork() หรือตัวจัดการเครือข่ายระดับซ็อกเก็ต ซึ่งมีประโยชน์เมื่ออุปกรณ์มี Wi-Fi และเซลลูลาร์พร้อมกันและคุณต้องการให้ทันเนลวิ่งผ่านลิงก์อัปเฉพาะ

เรียกมันขณะสร้างการขนส่ง

เรียก protect() ในบล็อกเดียวกับที่สร้างการขนส่ง ก่อน connect() หรือ send() ถ้าการเรียกคืนค่า false ให้ปิดซ็อกเก็ตและให้การตั้งค่าล้มเหลว การทำต่อจะทำให้คุณเหลือการขนส่งที่แพ็กเก็ตอาจกลับเข้าสู่อินเทอร์เฟซของคุณเอง

รูปแบบเล็กพอที่จะเก็บไว้ที่เดียว:

DatagramSocket transport = new DatagramSocket();
if (!protect(transport)) {
    transport.close();
    throw new IOException("transport not protected");
}
transport.connect(serverAddress);

มีสามรายละเอียดที่กัดตรงนี้ ข้อแรก ค่าบูลีนคือสัญญาณเดียวที่คุณได้รับ ดังนั้นบันทึกความล้มเหลวพร้อมบริบทพอที่จะแยกความยินยอมที่ถูกเพิกถอนออกจากการเรียกที่ถูกปฏิเสธ ข้อสอง protect() ครอบคลุมซ็อกเก็ต ไม่ใช่หมายเลขตัวระบุ: ถ้าคุณปิดซ็อกเก็ตแล้วภายหลังนำจำนวนเต็มเดียวกันไปใช้กับซ็อกเก็ตอื่น ซ็อกเก็ตใหม่จะไม่ได้รับการปกป้อง ข้อสาม เมื่อไลบรารีเนทีฟเปิดการขนส่ง ให้ปกป้องตัวระบุก่อนที่โค้ดเนทีฟจะเชื่อมต่อ ไม่อย่างนั้นเส้นทางอาจถูกเลือกไปแล้ว เอกสารอ้างอิง VpnService คือที่ที่คุณยืนยันโอเวอร์โหลดที่ระดับ API เป้าหมายของคุณมีให้จริง

ทดสอบเส้นทางที่ถูกบล็อก ไม่ใช่แค่เส้นทางที่ราบรื่น

ตรวจสอบขณะทันเนลทำงาน เพราะเป็นสถานะเดียวที่สองเส้นทางจะต่างกันได้ ส่งคำขอหนึ่งผ่านการขนส่งที่ได้รับการปกป้องและอีกหนึ่งจากเส้นทางโค้ดที่ไม่เคยเรียก protect() แล้วเปรียบเทียบสิ่งที่ปลายทางไกลเห็น: การเชื่อมต่อที่ได้รับการปกป้องควรมาถึงโดยมีเครือข่ายพื้นฐานเป็นต้นทาง ขณะที่อันที่ไม่ได้รับการปกป้องแสดงที่อยู่ของทันเนลเองและขยับตัวนับไบต์ของทันเนล บนอุปกรณ์ ss หรือ netstat แสดงการแบ่งเดียวกันก่อนมีสิ่งใดออกจากวิทยุ

ทดสอบการเพิกถอนแยกต่างหาก เพราะการเปลี่ยนความยินยอมรื้ออินเทอร์เฟซโดยไม่แตะโค้ดซ็อกเก็ตของคุณ เพิกถอนความยินยอม VPN หรือหยุดบริการ สร้างการขนส่งใหม่ และยืนยันว่า protect() ยังคืนค่า true และการขนส่งเชื่อมต่อใหม่เมื่อให้ความยินยอมอีกครั้ง

ถ้าคำขอที่ได้รับการปกป้องยังคงตามทันเนลอยู่ การเรียกปกป้องอาจเกิดขึ้นหลังจากซ็อกเก็ตถูกจัดเส้นทางไปแล้ว ย้ายมันให้เร็วขึ้นแล้วทดสอบอีกครั้ง

เขียนการตัดสินใจไว้ข้างการตั้งค่าการขนส่ง: ซ็อกเก็ตใดได้รับการปกป้อง การรับส่งใดของแอปถูกยกเว้น และแอปทำอะไรเมื่อการปกป้องล้มเหลว บรรทัดนั้นคือสิ่งที่ป้องกันไม่ให้การรีแฟกเตอร์ภายหลังขยายการยกเว้นไปทั้งแอปเพราะการทดสอบเดียวล้มเหลว มันยังให้นักพัฒนาคนถัดไปตรวจสอบเจตนาได้โดยไม่ต้องย้อนวิศวกรรม manifest

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