Skip to content

نظرة عامة على بنية DNS

النظام الفرعي لـ DNS في Xray هو مُحلّل DNS مستقل بالكامل يحل محل مُحلّل نظام التشغيل. يدعم التوجيه المبني على النطاق لاستعلامات DNS عبر خوادم متعددة، وتجاوزات المضيف الثابتة، واستراتيجيات الاستعلام المتوازية/التسلسلية، وتصفية IP، والتكامل مع Fake DNS.

هياكل البيانات الأساسية

هيكل DNS

يعيش المنسق المركزي في app/dns/dns.go. يُنفّذ واجهة الميزة dns.Client ويُنسّق كل منطق الاستعلام.

go
// 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:

go
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 المتوقعة/غير المتوقعة، المهلة الزمنية، الوسم للتوجيه، وتجاوز استراتيجية الاستعلام.

go
// 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
}

مخطط البنية

mermaid
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:

  1. تحليل استراتيجية الاستعلام -- يربط QueryStrategy_USE_IP، USE_IP4، USE_IP6، USE_SYS بأعلام dns.IPOption. استراتيجية USE_SYS تُفعّل فحص المسار في وقت التشغيل عبر checkRoutes().

  2. بناء المضيفات الثابتة -- إما التحميل من ملف تخزين MPH (حد أدنى للتجزئة المثالية) أو البناء من config.StaticHosts.

  3. بناء العملاء -- لكل NameServer في الإعداد، يُستدعى NewClient()، الذي:

    • يُنشئ Server عبر NewServer() (يوزّع حسب مخطط URL)
    • يُسجّل قواعد النطاقات في domainMatcher المشترك
    • يبني مُطابقات expectedIPs وunexpectedIPs لـ GeoIP
    • يُعيّن الذاكرة المؤقتة لكل خادم، وخدمة المنتهية الصلاحية، والمهلة الزمنية، والوسم
  4. العميل الافتراضي -- إذا لم تكن هناك خوادم مُهيّأة، يُضاف LocalNameServer (مُحلّل النظام) تلقائيًا.

التوجيه المبني على النطاق

يوجّه نظام DNS الاستعلامات إلى خوادم مختلفة بناءً على قواعد مطابقة النطاقات. هذه هي دالة sortClients():

go
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 أي عميل وأي قاعدة نطاق محددة تمت مطابقتها:

go
type DomainMatcherInfo struct {
    clientIdx     uint16
    domainRuleIdx uint16
}

هذا يعني أن النطاق "google.com" قد يُطابق قاعدة الخادم A وهي "geosite:google"، ويذهب استعلام DNS إلى الخادم A أولاً، ثم يتراجع إلى الآخرين.

أوضاع الاستعلام

الاستعلام التسلسلي

تتكرر serialQuery() عبر قائمة العملاء المرتبة بالتتابع. أول عميل يُعيد مجموعة IP غير فارغة يفوز. إذا أعاد عميل خطأ، يُجرّب العميل التالي.

go
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()، ثم تجمع النتائج باستخدام استراتيجية سباق مبنية على المجموعات:

  1. التجميع: العملاء المتجاورون بنفس policyID يُشكّلون مجموعة. دالة makeGroups() تدمج فقط العملاء المتجاورين والمتكافئين في القواعد.

  2. سباق المجموعات: داخل كل مجموعة، أول نتيجة ناجحة تفوز (أقل وقت استجابة). إذا فشل جميع أعضاء المجموعة، يُجرّب المجموعة التالية.

  3. معالجة المهلة الزمنية: للخوادم المُفعّل فيها التخزين المؤقت، يتم مضاعفة مهلة السياق (c.timeoutMs * 2) باستخدام context.WithoutCancel للسماح بتحديثات الذاكرة المؤقتة في الخلفية.

go
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 الاتصالية الفعلية قبل الاستعلام:

go
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.goIPRecord، dnsRequest، buildReqMsgs()، parseResponse()، خيارات EDNS0
app/dns/hosts.goStaticHosts لربط الإعداد "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 المؤقتة.

تحليل تقني لأغراض إعادة التنفيذ.