مقدار به صورت رشته به VpnService.Builder.addDnsServer رسید و builder همان‌جا آن را رد کرد، پیش از آنکه چیزی برپا شود. این رد شدن محلی است: با هیچ resolver تماسی گرفته نشد، تونلی وجود ندارد و آنچه شکست خورد بررسی قالب است نه قطعی DNS.

این مهم است چون یک برنامه VPN معمولا سه ورودی DNS شبیه دارد: مقداری که در پروفایل تایپ یا وارد می‌شود، مقداری که سرور در پیکربندی می‌فرستد و Private DNS در سطح سیستم. فقط دو مورد اول به این فراخوانی می‌رسند.

یک شیء آماده‌سازی همان‌جا آرگومان را بررسی می‌کند

VpnService.Builder تونل نیست. شیء آماده‌سازی است که نشانی‌ها، مسیرها و سرورها را جمع می‌کند و فقط establish() آن فهرست‌ها را به ParcelFileDescriptor زنده و واسط تبدیل می‌کند. ورودی DNS هنگام فراخوانی addDnsServer بررسی می‌شود، نه هنگام برپایی.

دو پیامد دارد. ردپا کوتاه و عمدا بی‌فایده است: مقدار به صورت رشته ساده رسید. و یک catch بی‌دقت پیرامون کل ساخت، تونلی با نشانی و مسیر اما بدون resolver باقی می‌گذارد.

builder هر فهرستی که بدهید نگه می‌دارد. افزودن سروری که بررسی را بگذراند فقط ثابت می‌کند رشته هنگام ساخت به عنوان نشانی لفظی تجزیه شده است.

آرگومان چه می‌تواند باشد

پارامتر نشانی است نه endpoint. مرجع Builder نشانی لفظی IPv4 یا IPv6 می‌گیرد و تجزیه فقط عددی است: android.net.InetAddresses.parseNumericAddress از 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 آن‌ها را می‌پذیرد؛ هنگام ساخت چیزی پرتاب نمی‌شود. سپس واسط با ورودی‌ای بالا می‌آید که هیچ مقصدی را نام نمی‌برد. گرفتن این کار اعتبارسنج شماست: 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: گرفتن گسترده خطاهای اشاره‌گر null را هم می‌بلعد.

وقتی مقدار مورد نظر رد می‌شود آن را با resolver عمومی جایگزین نکنید. پروفیلی که بی‌صدا DNS عمومی می‌گذارد وصل می‌شود اما نام‌های داخلی حل نمی‌شوند.

پس از ایجاد واسط، دسترس‌پذیری را تأیید کنید

پذیرش توسط builder نخستین نقطه کنترل است نه آخرین. با بالا آمدن واسط، LinkProperties.getDnsServers() نشان می‌دهد شبکه واقعا چه دریافت کرده. adb shell dumpsys connectivity همان ویژگی‌ها را از بیرون چاپ می‌کند.

Private DNS کنترل جداگانه است و از این آرگومان نمی‌گذرد. نام میزبان ارائه‌دهنده DoT به تنظیم سیستم تعلق دارد نه به فیلد IP.

پیش از تغییر دوباره تجزیه‌گر پروفایل، این ترتیب را کار کنید: رشته خام و منبعش را ثبت کنید، تأیید کنید لفظی است نه نام میزبان، loopback و wildcard نیست، و شیء تجزیه‌شده را بفرستید.

مقالات مرتبط