addDnsServer Bad Address: τι ελέγχει πραγματικά το Android
Το σφάλμα Bad address από το addDnsServer είναι τοπικός έλεγχος μορφής, όχι διακοπή DNS. Αναλύστε το literal, αφαιρέστε loopback και wildcard και μετά δοκιμάστε την προσβασιμότητα.
Η τιμή έφτασε στο VpnService.Builder.addDnsServer ως συμβολοσειρά και ο builder την απέρριψε εκεί πριν στηθεί οτιδήποτε. Η απόρριψη είναι τοπική: κανένας resolver δεν κλήθηκε, δεν υπάρχει τούνελ και αυτό που απέτυχε είναι έλεγχος μορφής, όχι διακοπή DNS.
Αυτό μετράει γιατί μια τυπική εφαρμογή VPN έχει τρεις παρόμοιες εισόδους DNS: τιμή που πληκτρολογήθηκε ή εισήχθη στο προφίλ, τιμή που στέλνει ο διακομιστής κατά τη διαμόρφωση και Private DNS σε επίπεδο συστήματος. Μόνο οι δύο πρώτες φτάνουν σε αυτή την κλήση.
Ένα αντικείμενο προετοιμασίας ελέγχει το όρισμα αμέσως
Το VpnService.Builder δεν είναι το τούνελ. Είναι αντικείμενο προετοιμασίας που συλλέγει διευθύνσεις, διαδρομές και διακομιστές, και μόνο το establish() μετατρέπει αυτές τις λίστες σε ζωντανό ParcelFileDescriptor και διεπαφή. Η εγγραφή DNS ελέγχεται όταν καλείτε το addDnsServer, όχι όταν στήνετε.
Ακολουθούν δύο συνέπειες. Το ίχνος είναι σύντομο και σκόπιμα μη διαφωτιστικό: η τιμή έφτασε ως απλή συμβολοσειρά. Και ένα απρόσεκτο catch γύρω από όλη την κατασκευή αφήνει τούνελ με διευθύνσεις και διαδρομές αλλά χωρίς resolver.
Ο builder κρατά όποια λίστα του δώσετε. Η προσθήκη διακομιστή που περνά τον έλεγχο αποδεικνύει μόνο ότι η συμβολοσειρά αναλύθηκε ως literal διεύθυνσης κατά την κατασκευή.
Τι επιτρέπεται να είναι το όρισμα
Η παράμετρος είναι διεύθυνση, όχι endpoint. Η αναφορά Builder δέχεται literal IPv4 ή IPv6 και η ανάλυση είναι μόνο αριθμητική: το android.net.InetAddresses.parseNumericAddress είναι το δημόσιο πρόσωπο αυτού του parser από το 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 τα δέχεται· τίποτα δεν πετιέται στην κατασκευή. Η διεπαφή ανεβαίνει με εγγραφή που δεν δηλώνει προορισμό. Το να το πιάσετε είναι δουλειά του 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 του προφίλ, δουλέψτε με αυτή τη σειρά: καταγράψτε την ακατέργαστη συμβολοσειρά και την πηγή της, επιβεβαιώστε ότι είναι literal και όχι όνομα, ότι δεν είναι loopback ούτε wildcard, και περάστε το αναλυμένο αντικείμενο.