Skip to content

تعمّق في Fake DNS

Fake DNS (FakeDNS) هي آلية تُخصّص عناوين IP مؤقتة وفريدة من مجمع خاص لأسماء النطاقات. عندما يتم إجراء اتصال لاحقًا بأحد عناوين IP المزيفة هذه، يقوم النظام بعكس الربط للحصول على النطاق الأصلي. هذا يُمكّن إعدادات الوكيل الشفاف حيث تصل استعلامات DNS قبل الاتصال الفعلي، مما يسمح بالتوجيه المبني على النطاق حتى لحركة المرور ذات وجهة IP غير شفافة.

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

mermaid
flowchart LR
    subgraph DNS Resolution
        A[Client DNS query for example.com] --> B[FakeDNSServer]
        B --> C[Holder.GetFakeIPForDomain]
        C --> D[Return 198.18.0.42]
    end

    subgraph Connection Phase
        E[Client connects to 198.18.0.42:443] --> F[Dispatcher sniffing]
        F --> G[fakeDNSSniffer]
        G --> H[Holder.GetDomainFromFakeDNS]
        H --> I[Returns example.com]
        I --> J[Override destination to example.com:443]
    end

المكونات الأساسية

Holder -- مجمع واحد

الملف: app/dns/fakedns/fake.go

يُدير Holder مجمع IP مزيف واحد (إما IPv4 أو IPv6).

go
type Holder struct {
    domainToIP cache.Lru    // bidirectional LRU: domain <-> IP
    ipRange    *net.IPNet   // the CIDR pool (e.g., 198.18.0.0/15)
    mu         *sync.Mutex
    config     *FakeDnsPool
}

التهيئة:

go
func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
    _, ipRange, _ := net.ParseCIDR(ipPoolCidr)
    ones, bits := ipRange.Mask.Size()
    rooms := bits - ones
    if math.Log2(float64(lruSize)) >= float64(rooms) {
        return errors.New("LRU size is bigger than subnet size")
    }
    fkdns.domainToIP = cache.NewLru(lruSize)
    fkdns.ipRange = ipRange
    fkdns.mu = new(sync.Mutex)
}

يجب أن يكون حجم LRU أصغر بشكل صارم من حجم الشبكة الفرعية (يُتحقق عبر log2(lruSize) < rooms). المجمع الافتراضي هو 198.18.0.0/15 مع 65535 إدخال.

خوارزمية تخصيص IP

تُخصّص دالة GetFakeIPForDomain() عناوين IP باستخدام نهج مبني على الوقت:

go
func (fkdns *Holder) GetFakeIPForDomain(domain string) []net.Address {
    fkdns.mu.Lock()
    defer fkdns.mu.Unlock()

    // 1. Check if domain already has an IP
    if v, ok := fkdns.domainToIP.Get(domain); ok {
        return []net.Address{v.(net.Address)}
    }

    // 2. Seed from current time (milliseconds)
    currentTimeMillis := uint64(time.Now().UnixNano() / 1e6)
    ones, bits := fkdns.ipRange.Mask.Size()
    rooms := bits - ones
    if rooms < 64 {
        currentTimeMillis %= (uint64(1) << rooms)
    }

    // 3. Compute candidate IP: base + offset
    bigIntIP := big.NewInt(0).SetBytes(fkdns.ipRange.IP)
    bigIntIP = bigIntIP.Add(bigIntIP, new(big.Int).SetUint64(currentTimeMillis))

    // 4. Linear probe for unused IP
    for {
        ip = net.IPAddress(bigIntIP.Bytes())
        if _, ok := fkdns.domainToIP.PeekKeyFromValue(ip); !ok {
            break  // IP not in use
        }
        bigIntIP = bigIntIP.Add(bigIntIP, big.NewInt(1))
        if !fkdns.ipRange.Contains(bigIntIP.Bytes()) {
            bigIntIP = big.NewInt(0).SetBytes(fkdns.ipRange.IP)  // wrap around
        }
    }

    // 5. Store and return
    fkdns.domainToIP.Put(domain, ip)
    return []net.Address{ip}
}

الخوارزمية:

  1. تُعيد عنوان IP المُخزّن مؤقتًا إذا كان النطاق قد شُوهد من قبل
  2. تستخدم الوقت الحالي بالمللي ثانية مقسومًا على حجم المجمع كإزاحة بداية
  3. تبحث خطيًا للأمام حتى تجد IP غير مُستخدم
  4. تلتف حول قاعدة المجمع إذا وصلت إلى النهاية
  5. تُخزّن الربط ثنائي الاتجاه في ذاكرة LRU المؤقتة

البحث العكسي

go
func (fkdns *Holder) GetDomainFromFakeDNS(ip net.Address) string {
    if !ip.Family().IsIP() || !fkdns.ipRange.Contains(ip.IP()) {
        return ""
    }
    if k, ok := fkdns.domainToIP.GetKeyFromValue(ip); ok {
        return k.(string)
    }
    return ""  // IP in pool but no mapping (evicted from LRU)
}

فحص عضوية المجمع

go
func (fkdns *Holder) IsIPInIPPool(ip net.Address) bool {
    if ip.Family().IsDomain() { return false }
    return fkdns.ipRange.Contains(ip.IP())
}

HolderMulti -- دعم المكدس المزدوج

الملف: app/dns/fakedns/fake.go

يُغلّف HolderMulti عدة نسخ من Holder (عادةً مجمع IPv4 واحد ومجمع IPv6 واحد).

go
type HolderMulti struct {
    holders []*Holder
    config  *FakeDnsPoolMulti
}

تُفوّض جميع الدوال لكل holder:

  • GetFakeIPForDomain() تُعيد عناوين IP من جميع المجمعات
  • GetFakeIPForDomain3() تُصفّي حسب تفعيل IPv4/IPv6 لكل holder
  • GetDomainFromFakeDNS() تُعيد أول تطابق عبر المجمعات
  • IsIPInIPPool() تُعيد true إذا كان أي مجمع يحتوي على عنوان IP

تصفية IPv4 مقابل IPv6

تتحقق دالة GetFakeIPForDomain3() في Holder مما إذا كانت عائلة IP الخاصة بالمجمع تُطابق الطلب:

go
func (fkdns *Holder) GetFakeIPForDomain3(domain string, ipv4, ipv6 bool) []net.Address {
    isIPv6 := fkdns.ipRange.IP.To4() == nil
    if (isIPv6 && ipv6) || (!isIPv6 && ipv4) {
        return fkdns.GetFakeIPForDomain(domain)
    }
    return []net.Address{}
}

ذاكرة LRU المؤقتة

نوع cache.Lru هو ذاكرة تخزين مؤقت ثنائية الاتجاه LRU تدعم:

  • Get(key) -- بحث قياسي بالمفتاح
  • Put(key, value) -- إدراج مع إخلاء أقدم إدخال
  • GetKeyFromValue(value) -- بحث عكسي (من القيمة إلى المفتاح)
  • PeekKeyFromValue(value) -- بحث عكسي دون تحديث الحداثة

هذه القدرة ثنائية الاتجاه ضرورية: الربط الأمامي (نطاق إلى IP) يُستخدم أثناء حل DNS، بينما الربط العكسي (IP إلى نطاق) يُستخدم أثناء فحص الاتصال.

عندما يُخلي LRU إدخالاً، يصبح عنوان IP متاحًا لإعادة الاستخدام بواسطة نطاق مختلف. هذا يعني أن الاتصالات بعناوين IP المزيفة المُخلاة لا يمكن عكس ربطها، ويتم تسجيل ذلك كرسالة معلوماتية.

تكامل خادم FakeDNS

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

يندمج FakeDNSServer في نظام خادم DNS:

go
type FakeDNSServer struct {
    fakeDNSEngine dns.FakeDNSEngine
}

func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOption) ([]net.IP, uint32, error) {
    var ips []net.Address
    if fkr0, ok := f.fakeDNSEngine.(dns.FakeDNSEngineRev0); ok {
        ips = fkr0.GetFakeIPForDomain3(domain, opt.IPv4Enable, opt.IPv6Enable)
    } else {
        ips = f.fakeDNSEngine.GetFakeIPForDomain(domain)
    }
    // ... convert to net.IP
    return netIP, 1, nil  // TTL = 1
}

قيمة TTL دائمًا 1 ثانية لمنع التخزين المؤقت من قبل المصبّ لعناوين IP المزيفة.

تكامل الموزع -- فحص Fake DNS

الملف: app/dispatcher/fakednssniffer.go

عندما يكون الفحص مُفعّلاً مع "fakedns" في destOverride، يُنشئ الموزع fakeDNSSniffer:

go
func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error) {
    fakeDNSEngine := core.MustFromContext(ctx).GetFeature((*dns.FakeDNSEngine)(nil))

    return protocolSnifferWithMetadata{
        protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
            ob := outbounds[len(outbounds)-1]
            domainFromFakeDNS := fakeDNSEngine.GetDomainFromFakeDNS(ob.Target.Address)
            if domainFromFakeDNS != "" {
                return &fakeDNSSniffResult{domainName: domainFromFakeDNS}, nil
            }
            // Check if IP is in pool (for fakedns+others mode)
            return nil, common.ErrNoClue
        },
        metadataSniffer: true,  // Can sniff without payload data
    }, nil
}

هذا فاحص بيانات وصفية -- يعمل دون فحص بايتات الحمولة. يبحث عن عنوان IP الوجهة في محرك Fake DNS ويُعيد النطاق المُربط.

وضع fakedns+others

الملف: app/dispatcher/fakednssniffer.go

تُنفّذ دالة newFakeDNSThenOthers() فاحصًا مُركّبًا:

go
func newFakeDNSThenOthers(ctx context.Context, fakeDNSSniffer, others []protocolSnifferWithMetadata) {
    return protocolSnifferWithMetadata{
        protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
            // 1. Try fakeDNS lookup first
            result, err := fakeDNSSniffer.protocolSniffer(ctx, bytes)
            if err == nil { return result, nil }

            // 2. If IP is in fake pool but domain not found, try other sniffers
            if ipInRange {
                for _, v := range others {
                    if result, err := v.protocolSniffer(ctx, bytes); err == nil {
                        return DNSThenOthersSniffResult{
                            domainName: result.Domain(),
                            protocolOriginalName: result.Protocol(),
                        }, nil
                    }
                }
            }
            return nil, common.ErrNoClue
        },
    }
}

هذا يتعامل مع الحالة التي تم فيها تخصيص IP مزيف لكن LRU أخلى الربط. يتراجع إلى فحص TLS SNI أو HTTP Host لاسترداد النطاق.

تجاوز الوجهة في الموزع

في app/dispatcher/default.go، تتحقق دالة shouldOverride():

go
func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResult, ...) bool {
    for _, p := range request.OverrideDestinationForProtocol {
        if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok &&
           protocolString != "bittorrent" && p == "fakedns" &&
           fkr0.IsIPInIPPool(destination.Address) {
            return true  // Override even for non-fakedns protocols if IP is fake
        }
    }
}

عندما تكون الوجهة IP مزيف و"fakedns" في قائمة التجاوز، يحل النطاق المُكتشف محل وجهة IP المزيف بغض النظر عن الفاحص الذي وجد النطاق.

الإعداد

يتم تهيئة محرك FakeDNS في المستوى الأعلى من إعداد Xray:

json
{
    "fakeDns": {
        "ipPool": "198.18.0.0/15",
        "poolSize": 65535
    }
}

أو للمكدس المزدوج:

json
{
    "fakeDns": {
        "pools": [
            { "ipPool": "198.18.0.0/15", "poolSize": 65535 },
            { "ipPool": "fc00::/18", "poolSize": 65535 }
        ]
    }
}

ثم يُشير خادم DNS إليه:

json
{
    "dns": {
        "servers": ["fakedns"]
    }
}

تدفق البيانات الكامل

mermaid
sequenceDiagram
    participant App as Application
    participant DNS as Xray DNS
    participant FakeDNS as FakeDNS Holder
    participant Disp as Dispatcher
    participant Sniffer as FakeDNS Sniffer
    participant Router as Router

    App->>DNS: Resolve example.com
    DNS->>FakeDNS: GetFakeIPForDomain("example.com")
    FakeDNS-->>DNS: 198.18.0.42
    DNS-->>App: 198.18.0.42

    App->>Disp: Connect to 198.18.0.42:443
    Disp->>Sniffer: Sniff metadata
    Sniffer->>FakeDNS: GetDomainFromFakeDNS(198.18.0.42)
    FakeDNS-->>Sniffer: "example.com"
    Sniffer-->>Disp: domain = example.com
    Disp->>Disp: Override target to example.com:443
    Disp->>Router: Route example.com:443
    Router-->>Disp: Select outbound

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

  • يتم تسجيل Holder كنوعي إعداد FakeDnsPool وFakeDnsPoolMulti. يُنشئ النواة النوع المناسب بناءً على ما إذا كان الإعداد يحتوي على مجمع واحد أو مجمعات متعددة.

  • يتم إضافة FakeDNS في بداية (وليس نهاية) قائمة تطبيقات الإعداد الأساسي: config.App = append([]*serial.TypedMessage{serial.ToTypedMessage(r)}, config.App...). هذا يضمن تهيئته قبل أن يحاول نظام DNS حل core.RequireFeatures().

  • واجهة FakeDNSEngineRev0 توسع FakeDNSEngine بإضافة GetFakeIPForDomain3() وIsIPInIPPool(). جميع التنفيذات الحالية تُلبّي كلتا الواجهتين.

  • إخلاء LRU صامت -- عندما يُخلى ربط نطاق، لا يمكن عكس ربط الاتصالات اللاحقة بذلك IP المزيف. وضع فحص fakedns+others يُخفّف من هذا بالتراجع إلى فحص TLS/HTTP.

  • القفل على Holder هو sync.Mutex عادي (وليس RWMutex) لأن GetFakeIPForDomain() قد تكتب (تخصيص عناوين IP جديدة)، وحتى Get() على LRU تُحدّث ترتيب الوصول.

  • حركة مرور BitTorrent مُستثناة صراحة من تجاوز Fake DNS في shouldOverride() لمنع مشاكل المتتبع.

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