Значэнне дайшло да VpnService.Builder.addDnsServer як радок, і builder адхіліў яго там, перш чым нешта было ўсталявана. Гэтая адмова лакальная: ні адзін resolver не быў выкліканы, тунеля няма, а збаявала праверка фармату, а не DNS.

Гэта важна, бо тыповы VPN-дадатак мае тры падобныя ўваходы DNS: значэнне, уведзенае ці імпартаванае ў профіль, значэнне, якое сервер перадае падчас канфігурацыі, і Private DNS на ўзроўні сістэмы. Да гэтага выкліку могуць дайсці толькі першыя два.

Аб’ект падрыхтоўкі правярае аргумент адразу

VpnService.Builder — не тунель. Гэта аб’ект падрыхтоўкі, які збірае адрасы, маршруты і серверы, і толькі establish() ператварае гэтыя спісы ў жывы ParcelFileDescriptor і інтэрфейс. Запіс DNS правяраецца пры выкліку addDnsServer, а не пры ўсталяванні.

Вынікаюць дзве рэчы. Траса стэка кароткая і наўмысна неінфарматыўная: значэнне прыйшло звычайным радком, і фрэймворк не ведае, ці гэта імпарт QR, ці нагрузка сервера. А неахайны 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 }
}

Пусты вынік становісца памылкай узроўню поля з вінаватым тэкстам, і гэта адзіная форма, з якой можа працаваць падтрымка. На шляху поспеху аб’ект ідзе ў builder.addDnsServer(address). Лавіце IllegalArgumentException менавіта, а не Exception: шырокі перахоп глытае і памылкі нулявога паказальніка.

Не падмяняйце публічны resolver, калі патрэбнае значэнне адхілена. Профіль, які моўчкі падстаўляе публічны DNS, злучаецца, але ўнутраныя імёны перастаюць разбірацца.

Пацвердзіце дасяжнасць пасля стварэння інтэрфейсу

Прыняцце builder-ам — першы кантрольны пункт, а не апошні. Пасля ўзняцця інтэрфейсу LinkProperties.getDnsServers() паказвае, што сетка фактычна атрымала. adb shell dumpsys connectivity друкуе тыя ж уласцівасці звонку.

Private DNS — асобнае кіраванне, і яно не ідзе праз гэты аргумент. Імя хоста DoT-правайдера належыць сістэмнай наладзе, а не полю IP.

Перш чым зноў змяняць парсер профілю, прайдзіце такі парадак: запішыце сыры радок і яго крыніцу, пацвердзіце, што гэта літарал, а не імя хоста, што гэта не loopback і не wildcard, і перадайце разгледжаны аб’ект.

Падобныя артыкулы