Значення дійшло до 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.

Не замінюйте публічним resolver, коли потрібне значення відхилено; профіль, що тихо підставляє публічний DNS, підключається, але внутрішні імена перестають розвʼязуватися.

Підтвердьте доступність після створення інтерфейсу

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

Private DNS це окреме керування, і воно не проходить через цей аргумент; імʼя хоста DoT-провайдера належить системному налаштуванню, а не полю IP.

Перш ніж знову змінювати парсер профілю, запишіть сирий рядок і його джерело, підтвердьте, що це літерал, а не імʼя хоста, що це не loopback і не wildcard, і передайте розібраний об’єкт.

Схожі статті