addDnsServer Bad Address: Android가 실제로 검증하는 것
addDnsServer의 Bad address 오류는 로컬 형식 검사이며 DNS 장애가 아닙니다. 리터럴을 분석하고 loopback과 wildcard를 제외한 뒤 연결 가능성을 확인하세요.
값은 문자열로 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부터 그 파서의 공개된 얼굴입니다.
실무적인 대응은 다음과 같습니다.
| Input | Result | Reason |
|---|---|---|
10.8.0.1 | accepted | IPv4 literal |
2606:4700:4700::1111 | accepted | IPv6 literal |
dns.example.com | rejected | hostname; numeric parsing does not resolve it |
10.8.0.1:53 | rejected | port belongs to your own config model |
https://dns.example.com/dns-query | rejected | DoH URL, a different configuration path |
127.0.0.1 | rejected | loopback is not a remote resolver |
0.0.0.0 or :: | accepted by the builder | parses 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도 아닌지 확인한 뒤 분석된 객체를 넘기세요.