تنفيذات خوادم DNS
يُنفّذ Xray خمسة أنواع مختلفة من خوادم DNS بالإضافة إلى خادم FakeDNS الزائف. جميعها تُنفّذ واجهة Server من app/dns/nameserver.go. يتم اختيار نوع الخادم بواسطة مصنع NewServer() بناءً على مخطط URL لعنوان خادم الأسماء.
منطق توزيع الخوادم
تُوجّه دالة NewServer() في app/dns/nameserver.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 بالمرور عبر سلاسل الوكيل.
ملخص أنواع الخوادم
| النوع | الهيكل | المخطط | النقل | عبر الموزع |
|---|---|---|---|---|
| UDP | ClassicNameServer | عنوان IP / بدون مخطط | UDP | نعم |
| TCP | TCPNameServer | tcp:// | TCP | نعم (بعيد) / لا (محلي) |
| DoH | DoHNameServer | https:// أو h2c:// | HTTP/2 POST | نعم (بعيد) / لا (محلي) |
| DoQ | QUICNameServer | quic+local:// | تدفقات QUIC | لا (محلي فقط) |
| محلي | LocalNameServer | localhost | مُحلّل نظام التشغيل | لا |
| FakeDNS | FakeDNSServer | fakedns | لا شيء (في الذاكرة) | لا |
نمط CachedNameserver
تتشارك جميع خوادم DNS الحقيقية (UDP، TCP، DoH، DoQ) نمط استعلام مشترك عبر واجهة CachedNameserver المُعرّفة في app/dns/nameserver_cached.go:
type CachedNameserver interface {
getCacheController() *CacheController
sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns.IPOption)
}تُنسّق دالة queryIP() تدفق التحقق-من-الذاكرة-ثم-الجلب:
- التحقق من الذاكرة المؤقتة للعثور على نتيجة (انظر caching.md)
- إذا كانت منتهية الصلاحية وكان
serveStaleمُفعّلاً، إرجاع النتيجة المنتهية وتحديثها في الخلفية - خلاف ذلك، استدعاء
fetch()التي تستخدمsingleflight.Groupلمنع تكرار الاستعلامات المتزامنة - تُسجّل
fetch()مشتركي pubsub، وتستدعيsendQuery()، وتنتظر الاستجابات
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.
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
}تدفق الاستعلام:
- تبني
sendQuery()رسائل طلب A و/أو AAAA باستخدامbuildReqMsgs() - يتم حزم كل رسالة عبر
dns.PackMessage()وتوزيعها عبرudp.Dispatcher - تصل الاستجابات بشكل غير متزامن عبر استدعاء
HandleResponse() - عند القطع، يُعاد الاستعلام مع مورد EDNS0 OPT (حجم حمولة UDP يبلغ 1350)
- تُمرر الاستجابات الناجحة إلى
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. يوجد متغيران: بعيد (عبر الموزع) ومحلي (مباشر).
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():
- تحزم رسالة DNS
- تكتب
uint16(length) || messageإلى الاتصال - تقرأ
uint16(length)ثم جسم الاستجابة - تحلل وتُحدّث الذاكرة المؤقتة
كل طلب يفتح اتصال TCP جديد (لا يوجد تجميع اتصالات).
خادم DoH (DoHNameServer)
الملف: app/dns/nameserver_doh.go
DNS-over-HTTPS باستخدام HTTP/2، متوافق مع تنسيق السلك RFC 8484.
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.
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
غلاف خفيف حول مُحلّل نظام التشغيل.
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 مزيفة من مجمع مُهيّأ.
type FakeDNSServer struct {
fakeDNSEngine dns.FakeDNSEngine
}- يُعيد عناوين IP مزيفة مع TTL=1 (لمنع التخزين المؤقت من قبل المصبّ)
- يدعم المكدس المزدوج عبر
FakeDNSEngineRev0.GetFakeIPForDomain3() - يتم حل ميزة
dns.FakeDNSEngineعبرcore.RequireFeatures() - الذاكرة المؤقتة مُعطّلة دائمًا
- يتم تخطيه أثناء الاستعلام إذا كان
option.FakeEnableخاطئًا
توجيه حركة مرور DNS
يتم توجيه استعلامات DNS عبر موزع Xray باستخدام حقل tag الخاص بالعميل. تُنشئ دالة toDnsContext() سياق توجيه:
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.