ค่ามาถึง VpnService.Builder.addDnsServer เป็นสตริงและ builder ปฏิเสธที่นั่นก่อนจะมีสิ่งใดถูกสร้าง การปฏิเสธนี้เป็นภายใน: ไม่มี resolver ใดถูกติดต่อ ไม่มี tunnel และสิ่งที่ล้มเหลวคือการตรวจรูปแบบ ไม่ใช่ DNS

เรื่องนี้สำคัญเพราะแอป VPN ทั่วไปมีอินพุต DNS สามอย่างที่คล้ายกัน: ค่าในโปรไฟล์ ค่าที่เซิร์ฟเวอร์ส่งมา และ Private DNS ในระดับระบบ มีเพียงสองอย่างแรกที่มาถึงการเรียกนี้

ออบเจ็กต์เตรียมการตรวจอาร์กิวเมนต์ทันที

VpnService.Builder ไม่ใช่ tunnel มันคือ ออบเจ็กต์เตรียมการที่รวบรวมที่อยู่ เส้นทาง และเซิร์ฟเวอร์ และมีเพียง establish() ที่เปลี่ยนรายการเหล่านั้นเป็น ParcelFileDescriptor ที่มีชีวิตและอินเทอร์เฟซ รายการ DNS ถูกตรวจเมื่อเรียก addDnsServer

ผลลัพธ์สองอย่าง: stack trace สั้นและจงใจไม่มีข้อมูล และ catch ที่ไม่ระวังทำให้เหลือ tunnel ที่ไม่มี resolver

builder เก็บทุกรายการที่ให้ไว้ การเพิ่มเซิร์ฟเวอร์ที่ผ่านการตรวจพิสูจน์เพียงว่าสตริงถูกวิเคราะห์เป็นที่อยู่ลิเทอรัลตอนสร้าง

อาร์กิวเมนต์เป็นอะไรได้

พารามิเตอร์คือที่อยู่ ไม่ใช่ endpoint เอกสาร Builder รับลิเทอรัล IPv4 หรือ IPv6 และการวิเคราะห์เป็นตัวเลขเท่านั้น: android.net.InetAddresses.parseNumericAddress คือหน้าสาธารณะของ parser นั้นตั้งแต่ API 29

การแมปในทางปฏิบัติมีลักษณะดังนี้

InputResultReason
10.8.0.1acceptedIPv4 literal
2606:4700:4700::1111acceptedIPv6 literal
dns.example.comrejectedhostname; numeric parsing does not resolve it
10.8.0.1:53rejectedport belongs to your own config model
https://dns.example.com/dns-queryrejectedDoH URL, a different configuration path
127.0.0.1rejectedloopback is not a remote resolver
0.0.0.0 or ::accepted by the builderparses fine, but names no destination; only your own validator rejects it

เวอร์ชันมีผลต่อวิธีที่คุณตรวจ ไม่ใช่สิ่งที่ builder รับ InetAddresses ใช้ได้ตั้งแต่ API 29 ดังนั้นบน Android 10 การวิเคราะห์คือการเรียกครั้งเดียว addDnsServer(InetAddress) คือรูปแบบเดิม String คือส่วนเพิ่มใหม่ ต่ำกว่า API 29 InetAddress.getByName จะแก้ชื่อโฮสต์ด้วยการค้นหาเครือข่ายที่บล็อก

loopback ถูกปฏิเสธที่นี่ wildcard ไม่

ไคลเอนต์ VPN บนเดสก์ท็อปมักส่ง stub resolver ไปที่ 127.0.0.1:53 การคัดลอกสิ่งนั้นลงในโปรไฟล์ Android จะนำค่าที่อธิบายเครื่องภายในเข้ามา และที่นี่ไม่มีเครื่องภายใน builder ปฏิเสธเฉพาะกรณีนี้: ที่อยู่ null หรือ loopback จะโยน IllegalArgumentException

wildcard เป็นกรณีตรงข้าม และนี่คือที่การตีความส่วนใหญ่ผิด 0.0.0.0 และ :: ไม่ใช่ loopback ดังนั้น builder จึงรับไว้ ไม่มีอะไรถูกโยนตอนสร้าง อินเทอร์เฟซจะขึ้นพร้อมรายการที่ไม่ระบุปลายทาง การจับสิ่งนั้นเป็นงานของ validator: ถือว่า isAnyLocalAddress เป็นอินพุตที่ไม่ถูกต้อง

วิเคราะห์ครั้งเดียวที่ขอบของการกำหนดค่า

วิธีแก้ราคาถูกคือตรวจตรงที่สตริงเข้าสู่โมเดลของคุณ

import android.net.InetAddresses
import java.net.InetAddress

private fun dnsLiteralOrNull(raw: String?): InetAddress? {
    val text = raw?.trim().orEmpty()
    if (text.isEmpty()) return null
    val address = try {
        InetAddresses.parseNumericAddress(text)
    } catch (e: IllegalArgumentException) {
        return null
    }
    return address.takeUnless { it.isAnyLocalAddress || it.isLoopbackAddress }
}

ผลลัพธ์ null กลายเป็นข้อผิดพลาดระดับฟิลด์ที่พาข้อความผิด และเป็นรูปแบบเดียวที่ฝ่ายสนับสนุนจัดการได้ บนเส้นทางสำเร็จ ออบเจ็กต์เข้าสู่ builder.addDnsServer(address) โดยตรง และจับ IllegalArgumentException อย่างเฉพาะเจาะจง ไม่ใช่ Exception

อย่าแทนที่ด้วย resolver สาธารณะเมื่อค่าที่ต้องการถูกปฏิเสธ โปรไฟล์ที่เปลี่ยนไปใช้ DNS สาธารณะอย่างเงียบ ๆ ยังเชื่อมต่อได้ แต่ชื่อภายในจะแก้ไม่ได้

ยืนยันการเข้าถึงเมื่อมีอินเทอร์เฟซแล้ว

การยอมรับของ builder คือจุดตรวจแรก ไม่ใช่จุดสุดท้าย เมื่ออินเทอร์เฟซขึ้น LinkProperties.getDnsServers() รายงานสิ่งที่เครือข่ายได้รับจริง และ adb shell dumpsys connectivity พิมพ์คุณสมบัติเดียวกันจากภายนอก

Private DNS เป็นการควบคุมแยกและไม่ผ่านอาร์กิวเมนต์นี้ ชื่อโฮสต์ของผู้ให้บริการ DoT เป็นของการตั้งค่าระบบ ไม่ใช่ฟิลด์ IP

ก่อนเปลี่ยน parser ของโปรไฟล์อีกครั้ง ให้บันทึกสตริงดิบและแหล่งที่มา ยืนยันว่าเป็นลิเทอรัลไม่ใช่ชื่อโฮสต์ ไม่ใช่ loopback หรือ wildcard และส่งออบเจ็กต์ที่วิเคราะห์แล้วต่อ

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