Skip to content

تنفيذات خوادم DNS

يُنفّذ Xray خمسة أنواع مختلفة من خوادم DNS بالإضافة إلى خادم FakeDNS الزائف. جميعها تُنفّذ واجهة Server من app/dns/nameserver.go. يتم اختيار نوع الخادم بواسطة مصنع NewServer() بناءً على مخطط URL لعنوان خادم الأسماء.

منطق توزيع الخوادم

تُوجّه دالة NewServer() في app/dns/nameserver.go إنشاء الخوادم:

go
func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dispatcher, ...) (Server, error) {
    if address := dest.Address; address.Family().IsDomain() {
        u, _ := url.Parse(address.Domain())
        switch {
        case strings.EqualFold(u.String(), "localhost"):       // -> LocalNameServer
        case strings.EqualFold(u.Scheme, "https"):             // -> DoHNameServer (remote)
        case strings.EqualFold(u.Scheme, "h2c"):               // -> DoHNameServer (h2c remote)
        case strings.EqualFold(u.Scheme, "https+local"):       // -> DoHNameServer (local)
        case strings.EqualFold(u.Scheme, "h2c+local"):         // -> DoHNameServer (h2c local)
        case strings.EqualFold(u.Scheme, "quic+local"):        // -> QUICNameServer
        case strings.EqualFold(u.Scheme, "tcp"):               // -> TCPNameServer (remote)
        case strings.EqualFold(u.Scheme, "tcp+local"):         // -> TCPNameServer (local)
        case strings.EqualFold(u.String(), "fakedns"):          // -> FakeDNSServer
        }
    }
    if dest.Network == net.Network_UDP {                        // -> ClassicNameServer
        return NewClassicNameServer(dest, dispatcher, ...)
    }
}

اللاحقة +local تعني أن الخادم يتصل مباشرة دون المرور عبر نظام التوجيه/الموزع في Xray. المتغيرات البعيدة تُوجّه حركة مرور DNS عبر الموزع، مما يسمح لحزم DNS بالمرور عبر سلاسل الوكيل.

ملخص أنواع الخوادم

النوعالهيكلالمخططالنقلعبر الموزع
UDPClassicNameServerعنوان IP / بدون مخططUDPنعم
TCPTCPNameServertcp://TCPنعم (بعيد) / لا (محلي)
DoHDoHNameServerhttps:// أو h2c://HTTP/2 POSTنعم (بعيد) / لا (محلي)
DoQQUICNameServerquic+local://تدفقات QUICلا (محلي فقط)
محليLocalNameServerlocalhostمُحلّل نظام التشغيللا
FakeDNSFakeDNSServerfakednsلا شيء (في الذاكرة)لا

نمط CachedNameserver

تتشارك جميع خوادم DNS الحقيقية (UDP، TCP، DoH، DoQ) نمط استعلام مشترك عبر واجهة CachedNameserver المُعرّفة في app/dns/nameserver_cached.go:

go
type CachedNameserver interface {
    getCacheController() *CacheController
    sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns.IPOption)
}

تُنسّق دالة queryIP() تدفق التحقق-من-الذاكرة-ثم-الجلب:

  1. التحقق من الذاكرة المؤقتة للعثور على نتيجة (انظر caching.md)
  2. إذا كانت منتهية الصلاحية وكان serveStale مُفعّلاً، إرجاع النتيجة المنتهية وتحديثها في الخلفية
  3. خلاف ذلك، استدعاء fetch() التي تستخدم singleflight.Group لمنع تكرار الاستعلامات المتزامنة
  4. تُسجّل fetch() مشتركي pubsub، وتستدعي sendQuery()، وتنتظر الاستجابات
mermaid
sequenceDiagram
    participant Caller
    participant queryIP
    participant Cache as CacheController
    participant PubSub
    participant sendQuery
    participant Upstream

    Caller->>queryIP: QueryIP(domain, option)
    queryIP->>Cache: findRecords(fqdn)
    alt Cache hit (TTL > 0)
        Cache-->>queryIP: cached IPs
        queryIP-->>Caller: return IPs
    else Cache stale + serveStale
        Cache-->>queryIP: stale IPs
        queryIP->>sendQuery: background refresh
        queryIP-->>Caller: return stale IPs (TTL=1)
    else Cache miss
        queryIP->>PubSub: registerSubscribers(domain)
        queryIP->>sendQuery: sendQuery(domain)
        sendQuery->>Upstream: DNS wire protocol
        Upstream-->>sendQuery: response
        sendQuery->>Cache: updateRecord()
        Cache->>PubSub: Publish(domain+"4"|"6")
        PubSub-->>queryIP: IPRecord
        queryIP-->>Caller: return IPs
    end

خادم UDP (ClassicNameServer)

الملف: app/dns/nameserver_udp.go

تنفيذ DNS-over-UDP الكلاسيكي. يُدير خريطة من الطلبات المعلقة مفهرسة بمعرف رسالة DNS.

go
type ClassicNameServer struct {
    sync.RWMutex
    cacheController *CacheController
    address         *net.Destination
    requests        map[uint16]*udpDnsRequest
    udpServer       *udp.Dispatcher
    requestsCleanup *task.Periodic
    reqID           uint32
    clientIP        net.IP
}

تدفق الاستعلام:

  1. تبني sendQuery() رسائل طلب A و/أو AAAA باستخدام buildReqMsgs()
  2. يتم حزم كل رسالة عبر dns.PackMessage() وتوزيعها عبر udp.Dispatcher
  3. تصل الاستجابات بشكل غير متزامن عبر استدعاء HandleResponse()
  4. عند القطع، يُعاد الاستعلام مع مورد EDNS0 OPT (حجم حمولة UDP يبلغ 1350)
  5. تُمرر الاستجابات الناجحة إلى cacheController.updateRecord()

تنظيف الطلبات: يعمل task.Periodic كل دقيقة لإخلاء الطلبات الأقدم من 8 ثوانٍ.

EDNS0 Client Subnet: إذا تم تهيئة clientIP، يُلحق خيار EDNS0 Subnet بقناع /24 لـ IPv4 أو /96 لـ IPv6.

خادم TCP (TCPNameServer)

الملف: app/dns/nameserver_tcp.go

DNS-over-TCP وفقًا لـ RFC 7766. يوجد متغيران: بعيد (عبر الموزع) ومحلي (مباشر).

go
type TCPNameServer struct {
    cacheController *CacheController
    destination     *net.Destination
    reqID           uint32
    dial            func(context.Context) (net.Conn, error)
    clientIP        net.IP
}

البعيد مقابل المحلي: إغلاق دالة dial مختلف:

  • البعيد (tcp://): يستخدم dispatcher.Dispatch() لإنشاء اتصال مُوجّه، يُحوّل إلى net.Conn عبر cnc.NewConnection()
  • المحلي (tcp+local://): يستخدم internet.DialSystem() للاتصال المباشر بالنظام

تنسيق السلك: يُسبق DNS عبر TCP بادئة طول 2 بايت بترتيب كبير قبل كل رسالة. دالة sendQuery():

  1. تحزم رسالة DNS
  2. تكتب uint16(length) || message إلى الاتصال
  3. تقرأ uint16(length) ثم جسم الاستجابة
  4. تحلل وتُحدّث الذاكرة المؤقتة

كل طلب يفتح اتصال TCP جديد (لا يوجد تجميع اتصالات).

خادم DoH (DoHNameServer)

الملف: app/dns/nameserver_doh.go

DNS-over-HTTPS باستخدام HTTP/2، متوافق مع تنسيق السلك RFC 8484.

go
type DoHNameServer struct {
    cacheController *CacheController
    httpClient      *http.Client
    dohURL          string
    clientIP        net.IP
}

قرارات التصميم الرئيسية:

  • يستخدم http2.Transport مباشرة (وليس http.Transport القياسي) للتحكم الكامل في HTTP/2
  • مصافحة TLS تستخدم utls.UClient مع HelloChrome_Auto لمحاكاة بصمة TLS الخاصة بـ Chrome
  • متغير h2c (h2c://) يتخطى TLS تمامًا لـ HTTP/2 بنص واضح
  • يُطبّق حشو EDNS0 بطول عشوائي (100-300 بايت) لإخفاء أنماط حجم الاستعلام
  • يتضمن طلب HTTP ترويسة X-Padding مع حشو عشوائي بتنسيق base62
  • كشف الحل الذاتي: إذا حاول خادم DoH حل اسم مضيفه الخاص، يُطلق خطأ

تنسيق الطلب: POST إلى عنوان DoH مع Content-Type: application/dns-message وAccept: application/dns-message. الجسم هو تنسيق السلك DNS الخام.

أوضاع الاتصال:

  • البعيد (الموزع ليس nil): حركة مرور DNS مُوجّهة عبر نظام المخرجات في Xray
  • المحلي (الموزع nil): اتصال مباشر بالنظام مع تسجيل الوصول

خادم DoQ (QUICNameServer)

الملف: app/dns/nameserver_quic.go

DNS-over-QUIC، الوضع المحلي فقط. يستخدم مكتبة quic-go.

go
type QUICNameServer struct {
    sync.RWMutex
    cacheController *CacheController
    destination     *net.Destination
    connection      *quic.Conn
    clientIP        net.IP
}

إدارة الاتصال:

  • يحافظ على اتصال QUIC دائم واحد مع إعادة اتصال كسولة
  • يستخدم رموز ALPN: "doq"، "http/1.1"، "h2"
  • المنفذ الافتراضي هو 853
  • مهلة المصافحة: 8 ثوانٍ
  • عند فشل الاتصال، يُعيد المحاولة مرة واحدة قبل إرجاع الخطأ

تدفقات لكل استعلام: يفتح كل استعلام DNS تدفق QUIC جديد عبر conn.OpenStreamSync(). تنسيق الرسالة يستخدم بادئة طول 2 بايت مثل DNS عبر TCP.

الخادم المحلي (LocalNameServer)

الملف: app/dns/nameserver_local.go

غلاف خفيف حول مُحلّل نظام التشغيل.

go
type LocalNameServer struct {
    client *localdns.Client
}
  • الذاكرة المؤقتة مُعطّلة دائمًا (IsDisableCache() تُعيد true)
  • يُضيف تلقائيًا قواعد نطاقات geosite:private (نطاقات TLD المحلية والنطاقات بدون نقاط)
  • عند الإضافة كخادم وحيد (افتراضي)، يُغلّف في Client مع ipOption العام
  • يستخدم net.Resolver الخاص بـ Go تحت الغطاء عبر localdns.Client

خادم FakeDNS (FakeDNSServer)

الملف: app/dns/nameserver_fakedns.go

ليس خادم DNS حقيقي -- يُولّد عناوين IP مزيفة من مجمع مُهيّأ.

go
type FakeDNSServer struct {
    fakeDNSEngine dns.FakeDNSEngine
}
  • يُعيد عناوين IP مزيفة مع TTL=1 (لمنع التخزين المؤقت من قبل المصبّ)
  • يدعم المكدس المزدوج عبر FakeDNSEngineRev0.GetFakeIPForDomain3()
  • يتم حل ميزة dns.FakeDNSEngine عبر core.RequireFeatures()
  • الذاكرة المؤقتة مُعطّلة دائمًا
  • يتم تخطيه أثناء الاستعلام إذا كان option.FakeEnable خاطئًا

توجيه حركة مرور DNS

يتم توجيه استعلامات DNS عبر موزع Xray باستخدام حقل tag الخاص بالعميل. تُنشئ دالة toDnsContext() سياق توجيه:

go
func toDnsContext(ctx context.Context, addr string) context.Context {
    dnsCtx := core.ToBackgroundDetachedContext(ctx)
    // Preserve inbound tag for routing
    dnsCtx = session.ContextWithInbound(dnsCtx, inbound)
    dnsCtx = session.ContextWithContent(dnsCtx, ...)
    dnsCtx = log.ContextWithAccessMessage(dnsCtx, ...)
    return dnsCtx
}

يُعيّن محتوى الجلسة Protocol: "dns" (أو "https" لـ DoH، "quic" لـ DoQ) وSkipDNSResolve: true لمنع حل DNS المتكرر.

ملاحظات التنفيذ

  • جميع تنفيذات sendQuery() تُطلق استعلامات A وAAAA في goroutines متوازية عندما يكون كل من IPv4 وIPv6 مُفعّلين.

  • قناة noResponseErrCh (بسعة 2) تسمح لـ sendQuery بالإبلاغ عن أخطاء مستوى النقل إلى مستدعي doFetch()، مما يمنع الانتظار غير المحدود على مشترك pubsub.

  • يختلف توليد معرف الطلب: UDP يستخدم عدادًا ذريًا لمعرفات فريدة (ضروري للاستجابات المتعددة)، بينما يستخدم DoH وDoQ دائمًا المعرف 0 (لأن كل استعلام يحصل على طلب HTTP أو تدفق QUIC خاص به).

  • تستخدم خوادم TCP وDoH context.WithDeadline من السياق الأصلي، بينما يستخدم UDP مؤقت التنظيف الدوري (8 ثوانٍ) لفرض المهلة الزمنية.

  • دالة IsOwnLink() في هيكل DNS تتحقق مما إذا كان وسم المدخل الحالي يُطابق وسم أي عميل DNS، مما يمنع حلقات حل DNS.

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