שגיאת Bad address מ-addDnsServer: מה אנדרואיד באמת בודק
תקלת Bad address מ-addDnsServer היא בדיקת פרסור מקומית, לא הפסקת DNS. פרסרו את ה-literal, הסירו loopback ו-wildcard, ואז בדקו נגישות.
הערך שהועבר אל VpnService.Builder.addDnsServer הגיע כמחרוזת, והבונה דחה אותו שם, לפני שמשהו הוקם בכלל. הדחייה הזאת מקומית: אף resolver לא נוצר איתו קשר, אף מנהרה לא קיימת, ומה שנכשל הוא בדיקת תבנית ולא תקלת DNS. קריאה של ה-trace כהתנגשות עם ניהול צריכת חשמל או כבעיה בצד השרת שולחת אותך לשכבה הלא נכונה. הפריים מציין את הארגומנט שנדחה, ולא את ההגדרה שיצרה אותו.
זה חשוב מפני שלאפליקציית VPN טיפוסית יש שלושה שדות DNS שנראים דומים: ערך שהוקלד או יובא אל הפרופיל, ערך שהשרת דוחף בזמן התצורה, ו-Private DNS שמוגדר ברמת המערכת. רק הראשון והשני יכולים להגיע אל הקריאה הזאת, ורק אחד מהם הוא בדרך כלל המקור למחרוזת הפגומה.
אובייקט ביניים בודק את הארגומנט במקום
VpnService.Builder אינו המנהרה. הוא אובייקט ביניים שאוסף כתובות, מסלולים ושרתים, ורק establish() הוא היחיד שהופך את הרשומות האלה ל-ParcelFileDescriptor חי וממשק. ערך ה-DNS נבדק כשקוראים ל-addDnsServer, ולא כשמקימים, ולכן החריגה נזרקת כמה שורות מעל הקוד שאותו באמת ניפית.
שתי השלכות נובעות מכך. ה-stack trace קצר ולא מועיל בכוונה: הערך הגיע כמחרוזת פשוטה, ולמסגרת אין דרך לדעת אם הוא בא מייבוא QR, ממטען של שרת או מדריסת הגדרות. ומשום שכל שאר חלקי הבונה שורדים את הקריאה שנכשלה, תפיסת catch רשלנית סביב כל הבנייה משאירה אותך עם מנהרה שיש לה כתובות ומסלולים אבל אין לה resolver בכלל.
הבונה גם שומר על כל רשימה שתיתן לו. הוספת שרת שעובר את הבדיקה מוכיחה רק שהמחרוזת נפרסרה כ-literal של כתובת בזמן הבנייה, ומקרה ה-wildcard שלמטה מראה שזה אפילו לא מוכיח שהערך שמיש.
מה הארגומנט רשאי להיות
הפרמטר הוא כתובת, לא נקודת קצה. מדריך ה-Builder מקבל literal של 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 |
הגרסה קובעת איך נכון לבדוק, לא מה הבונה מקבל. InetAddresses זמין מ-API 29, ולכן ב-Android 10 ואילך פרסור ה-literal הוא קריאה אחת. גם ה-overload חשוב: addDnsServer(InetAddress) הוא הצורה המקורית, ואילו String הוא התוספת החדשה יותר, ולכן דוגמת מחרוזת מודבקת לא תתקמפל מול SDK ישן. מתחת ל-API 29 התחליף המובן מאליו, InetAddress.getByName, יפרסר בשמחה שם מארח עם חיפוש רשת חוסם, מה שהופך שגיאת הקלדה להתנעה איטית במקום לשגיאה ברורה. אם אתה עדיין משלח לגרסאות האלה, הרץ בדיקת literal מפורשת לפני שאתה נוגע ב-API הישן.
Loopback נדחה כאן, wildcard לא
לקוחות VPN בשולחן העבודה נוטים לכוון את ה-stub resolver שלהם אל 127.0.0.1:53 ולתת ללקוח המנהרה להעביר שאילתות. העתקת הסידור הזה אל פרופיל Android מייבאת ערך שמתאר את המכונה המקומית, ואין כאן מכונה מקומית שאליה אפשר למסור אותו: ה-resolver של המנהרה חייב להיות נגיש בצד השני של הממשק. זה המקרה היחיד שהבונה עצמו דוחה: כתובת null או loopback זורקת IllegalArgumentException, כפי שהחוזה במדריך קובע.
wildcard הוא המקרה ההפוך, ושם רוב הקריאות של החריגה הזאת טועות. 0.0.0.0 ו-:: אינם loopback, ולכן הבונה מקבל אותם ומוסיף אותם כמו כל literal אחר; שום דבר לא נזרק בזמן הבנייה. הממשק עולה אז עם רשומת resolver שלא מציינת יעד, והכשל עובר מחריגה לשאילתות שנעלמות בשקט לשום מקום. לתפוס את זה זה תפקיד הוולידטור שלך, ולא של המסגרת: התייחס אל 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-pointer ומצב שאתה רוצה לראות בבניית דיבוג.
התנגד לפיתוי להחליף resolver ציבורי כשהערך המיועד נדחה. פרופיל שמחליף בשקט שירות DNS ציבורי עדיין מתחבר, אבל שמות פנימיים מפסיקים להיפתר וכל שאילתה יוצאת דרך resolver שהמשתמש לא בחר. שגיאה גלויה עולה תשובת תמיכה אחת; ההחלפה השקטה עולה באמון שהמנהרה שומרת את השאילתות במקום שהפרופיל מציין.
ודא נגישות אחרי שהממשק קיים
הקבלה על ידי הבונה היא מחסום ראשון, לא אחרון. אחרי שהממשק עולה, LinkProperties.getDnsServers() מדווח מה הרשת באמת קיבלה, ושם מתגלה אי-התאמה: רשימה ריקה, פחות רשומות ממה שהוספת, או משפחה שהמסלולים שלך לא נושאים. adb shell dumpsys connectivity מדפיס את אותן תכונות לכל רשת כשאתה צריך להסתכל מחוץ לאפליקציה.
Private DNS הוא בקרה נפרדת והוא לא עובר דרך הארגומנט הזה. שם מארח של ספק DoT שייך להגדרת המערכת, או למסלול ה-resolver המוצפן שלך; זה לא ערך להדביק בשדה IP, ותיקון של הגדרת Private DNS לא ינקה חריגה שהבונה זרק.
לפני שאתה משנה שוב את פרסר הפרופיל, עבוד בסדר הזה: רשום את המחרוזת הגולמית ואת מקורה, ודא שהיא literal ולא שם מארח או URL, ודא שהיא לא loopback ולא wildcard, העבר את האובייקט המפורסר במקום לפרסר שוב את הטקסט, ורק אז השווה את הממשק שהוקם לערך שהתכוונת אליו.