نظرة عامة على بنية DNS
النظام الفرعي لـ DNS في Xray هو مُحلّل DNS مستقل بالكامل يحل محل مُحلّل نظام التشغيل. يدعم التوجيه المبني على النطاق لاستعلامات DNS عبر خوادم متعددة، وتجاوزات المضيف الثابتة، واستراتيجيات الاستعلام المتوازية/التسلسلية، وتصفية IP، والتكامل مع Fake DNS.
هياكل البيانات الأساسية
هيكل DNS
يعيش المنسق المركزي في app/dns/dns.go. يُنفّذ واجهة الميزة dns.Client ويُنسّق كل منطق الاستعلام.
// app/dns/dns.go
type DNS struct {
sync.Mutex
disableFallback bool
disableFallbackIfMatch bool
enableParallelQuery bool
ipOption *dns.IPOption
hosts *StaticHosts
clients []*Client
ctx context.Context
domainMatcher strmatcher.IndexMatcher
matcherInfos []*DomainMatcherInfo
checkSystem bool
}الحقول الرئيسية:
| الحقل | الغرض |
|---|---|
clients | قائمة مرتبة من أغلفة *Client، كل منها يحتوي على تنفيذ Server |
hosts | ربط ثابت للنطاقات بعناوين IP (قسم الإعداد "hosts") |
domainMatcher | مجموعة strmatcher.MatcherGroup تُفهرس قواعد النطاقات عبر جميع العملاء |
matcherInfos | يربط فهرس المُطابق بزوج (clientIdx, domainRuleIdx) |
ipOption | استراتيجية الاستعلام العامة (IPv4/IPv6/كلاهما) |
enableParallelQuery | يتنقل بين أوضاع الاستعلام التسلسلي والمتوازي |
disableFallback / disableFallbackIfMatch | يتحكم في ما إذا كان يتم استخدام العملاء غير المطابقين كاحتياط |
واجهة Server
مُعرّفة في app/dns/nameserver.go، هذا هو العقد لجميع تنفيذات خوادم DNS:
type Server interface {
Name() string
IsDisableCache() bool
QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error)
}كل نوع خادم (UDP، TCP، DoH، DoQ، Local، FakeDNS) يُنفّذ هذه الواجهة.
هيكل Client
يُغلّف Client خادم Server مع سياسة خاصة بكل عميل: قواعد النطاقات، تصفية IP المتوقعة/غير المتوقعة، المهلة الزمنية، الوسم للتوجيه، وتجاوز استراتيجية الاستعلام.
// app/dns/nameserver.go
type Client struct {
server Server
skipFallback bool
domains []string
expectedIPs router.GeoIPMatcher
unexpectedIPs router.GeoIPMatcher
actPrior bool
actUnprior bool
tag string
timeoutMs time.Duration
finalQuery bool
ipOption *dns.IPOption
checkSystem bool
policyID uint32
}مخطط البنية
flowchart TD
A[LookupIP called] --> B{Static hosts match?}
B -->|Domain replacement| C[Replace domain, continue]
B -->|IP found| D[Return IPs, TTL=10]
B -->|Not found| E{Parallel query enabled?}
E -->|Yes| F[parallelQuery]
E -->|No| G[serialQuery]
G --> H[sortClients by domain rules]
H --> I[Query clients in order]
I --> J{IPs returned?}
J -->|Yes| K[Apply expectedIPs / unexpectedIPs filter]
J -->|No| L[Try next client or return error]
K --> M[Return filtered IPs]
F --> N[sortClients by domain rules]
N --> O[asyncQueryAll - fire all queries]
O --> P[Collect results by group]
P --> Q{Any group succeeds?}
Q -->|Yes| M
Q -->|No| Lتدفق التهيئة
تبني دالة New() في app/dns/dns.go محرك DNS:
تحليل استراتيجية الاستعلام -- يربط
QueryStrategy_USE_IP،USE_IP4،USE_IP6،USE_SYSبأعلامdns.IPOption. استراتيجيةUSE_SYSتُفعّل فحص المسار في وقت التشغيل عبرcheckRoutes().بناء المضيفات الثابتة -- إما التحميل من ملف تخزين MPH (حد أدنى للتجزئة المثالية) أو البناء من
config.StaticHosts.بناء العملاء -- لكل
NameServerفي الإعداد، يُستدعىNewClient()، الذي:- يُنشئ
ServerعبرNewServer()(يوزّع حسب مخطط URL) - يُسجّل قواعد النطاقات في
domainMatcherالمشترك - يبني مُطابقات
expectedIPsوunexpectedIPsلـ GeoIP - يُعيّن الذاكرة المؤقتة لكل خادم، وخدمة المنتهية الصلاحية، والمهلة الزمنية، والوسم
- يُنشئ
العميل الافتراضي -- إذا لم تكن هناك خوادم مُهيّأة، يُضاف
LocalNameServer(مُحلّل النظام) تلقائيًا.
التوجيه المبني على النطاق
يوجّه نظام DNS الاستعلامات إلى خوادم مختلفة بناءً على قواعد مطابقة النطاقات. هذه هي دالة sortClients():
func (s *DNS) sortClients(domain string) []*Client {
// 1. Match domain against domainMatcher (returns indices)
// 2. Sort matched indices, look up (clientIdx, domainRuleIdx) pairs
// 3. Add matched clients in priority order (deduplicating)
// 4. If finalQuery is set on a matched client, stop immediately
// 5. Unless fallback is disabled, append remaining unmatched clients
// 6. If no clients matched and fallback is disabled, use first client
}يُخزّن كل DomainMatcherInfo أي عميل وأي قاعدة نطاق محددة تمت مطابقتها:
type DomainMatcherInfo struct {
clientIdx uint16
domainRuleIdx uint16
}هذا يعني أن النطاق "google.com" قد يُطابق قاعدة الخادم A وهي "geosite:google"، ويذهب استعلام DNS إلى الخادم A أولاً، ثم يتراجع إلى الآخرين.
أوضاع الاستعلام
الاستعلام التسلسلي
تتكرر serialQuery() عبر قائمة العملاء المرتبة بالتتابع. أول عميل يُعيد مجموعة IP غير فارغة يفوز. إذا أعاد عميل خطأ، يُجرّب العميل التالي.
func (s *DNS) serialQuery(domain string, option dns.IPOption) ([]net.IP, uint32, error) {
for _, client := range s.sortClients(domain) {
ips, ttl, err := client.QueryIP(s.ctx, domain, option)
if len(ips) > 0 {
return ips, ttl, nil
}
// log error, continue to next client
}
return nil, 0, mergeQueryErrors(domain, errs)
}الاستعلام المتوازي
تُطلق parallelQuery() استعلامات لجميع العملاء المرتبين في وقت واحد عبر asyncQueryAll()، ثم تجمع النتائج باستخدام استراتيجية سباق مبنية على المجموعات:
التجميع: العملاء المتجاورون بنفس
policyIDيُشكّلون مجموعة. دالةmakeGroups()تدمج فقط العملاء المتجاورين والمتكافئين في القواعد.سباق المجموعات: داخل كل مجموعة، أول نتيجة ناجحة تفوز (أقل وقت استجابة). إذا فشل جميع أعضاء المجموعة، يُجرّب المجموعة التالية.
معالجة المهلة الزمنية: للخوادم المُفعّل فيها التخزين المؤقت، يتم مضاعفة مهلة السياق (
c.timeoutMs * 2) باستخدامcontext.WithoutCancelللسماح بتحديثات الذاكرة المؤقتة في الخلفية.
func asyncQueryAll(domain string, option dns.IPOption, clients []*Client, ctx context.Context) chan queryResult {
ch := make(chan queryResult, len(clients))
for i, client := range clients {
go func(i int, c *Client) {
ips, ttl, err := c.QueryIP(qctx, domain, option)
ch <- queryResult{ips: ips, ttl: ttl, err: err, index: i}
}(i, client)
}
return ch
}تصفية IP على مستوى العميل
بعد أن يُعيد الخادم عناوين IP، تُطبّق دالة Client.QueryIP() مرشحين:
- expectedIPs -- إذا كان
actPriorخاطئًا، يتم الاحتفاظ فقط بعناوين IP المطابقة لمجموعة GeoIP (مرشح صارم). إذا كانactPriorصحيحًا، تُفضّل عناوين IP المطابقة لكن يُحتفظ بغير المطابقة كاحتياط. - unexpectedIPs -- العكس: عناوين IP المطابقة للمجموعة "غير المتوقعة" تُزال (أو تُخفض أولويتها إذا كان
actUnpriorصحيحًا).
هذا يُمكّن "تصفية تلويث DNS" حيث قد يُعيد خادم DNS محلي عناوين IP ملوثة للنطاقات الأجنبية.
فحص المسار (USE_SYS)
عندما تكون QueryStrategy هي USE_SYS، يفحص نظام DNS الاتصالية الفعلية قبل الاستعلام:
func probeRoutes() (ipv4 bool, ipv6 bool) {
// Dial root DNS servers to check IPv4/IPv6 connectivity
if conn, err := net.Dial("udp4", "192.33.4.12:53"); err == nil { ipv4 = true }
if conn, err := net.Dial("udp6", "[2001:500:2::c]:53"); err == nil { ipv6 = true }
}على منصات الواجهة الرسومية (Android، iOS، Windows، macOS)، يتم تخزين هذا الفحص مؤقتًا لمدة 100 مللي ثانية ويُعاد فحصه للتعامل مع تغييرات الشبكة. على منصات الخوادم، يُنفّذ مرة واحدة فقط.
ملفات المصدر الرئيسية
| الملف | الغرض |
|---|---|
app/dns/dns.go | هيكل DNS، New()، LookupIP()، sortClients()، الاستعلام التسلسلي/المتوازي |
app/dns/nameserver.go | واجهة Server، هيكل Client، مصنع NewServer()، NewClient()، تصفية IP |
app/dns/dnscommon.go | IPRecord، dnsRequest، buildReqMsgs()، parseResponse()، خيارات EDNS0 |
app/dns/hosts.go | StaticHosts لربط الإعداد "hosts" |
app/dns/config.go | مساعدات مُطابق النطاقات (toStrMatcher)، قواعد TLD المحلية |
ملاحظات التنفيذ
يُسجّل هيكل
DNSنفسه عبرcommon.RegisterConfig((*Config)(nil), ...)فيinit()، لذا ينشئه النواة عندما يظهر إعداد protobuf لـ DNS في قائمة التطبيقات.حقل
tagفي كل Client مهم: يصبح وسم المدخل لسياق توجيه استعلام DNS (session.ContextWithInbound). هذا يسمح لقواعد التوجيه بتوجيه حركة مرور DNS عبر مخرجات محددة (مثل إرسال DNS-over-HTTPS عبر وكيل).finalQueryعلى خادم أسماء يجعلsortClients()تتوقف عن إضافة عملاء بعد ذلك الخادم. هذا يمنع الاحتياط لقواعد نطاقات محددة.يُحسب
policyIDفي وقت البناء بناءً على تجزئة خصائص إعداد الخادم (النطاقات، عناوين IP المتوقعة، استراتيجية الاستعلام، الوسم، إلخ). الخوادم ذات معرفات السياسة المتطابقة تُجمّع معًا للتسابق المتوازي.عندما يفشل جميع العملاء، تُوحّد
mergeQueryErrors()الأخطاء. إذا كانت جميع الأخطاء هيerrRecordNotFound(الخادم لم يعط استجابة)، يُعادdns.ErrEmptyResponseعام.إدارة الذاكرة صريحة: بعد معالجة قواعد النطاقات وقوائم GeoIP أثناء التهيئة، يُستدعى
runtime.GC()لتحرير هياكل protobuf المؤقتة.