تعمّق في Fake DNS
Fake DNS (FakeDNS) هي آلية تُخصّص عناوين IP مؤقتة وفريدة من مجمع خاص لأسماء النطاقات. عندما يتم إجراء اتصال لاحقًا بأحد عناوين IP المزيفة هذه، يقوم النظام بعكس الربط للحصول على النطاق الأصلي. هذا يُمكّن إعدادات الوكيل الشفاف حيث تصل استعلامات DNS قبل الاتصال الفعلي، مما يسمح بالتوجيه المبني على النطاق حتى لحركة المرور ذات وجهة IP غير شفافة.
نظرة عامة على البنية
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).
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
}التهيئة:
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 باستخدام نهج مبني على الوقت:
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}
}الخوارزمية:
- تُعيد عنوان IP المُخزّن مؤقتًا إذا كان النطاق قد شُوهد من قبل
- تستخدم الوقت الحالي بالمللي ثانية مقسومًا على حجم المجمع كإزاحة بداية
- تبحث خطيًا للأمام حتى تجد IP غير مُستخدم
- تلتف حول قاعدة المجمع إذا وصلت إلى النهاية
- تُخزّن الربط ثنائي الاتجاه في ذاكرة LRU المؤقتة
البحث العكسي
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)
}فحص عضوية المجمع
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 واحد).
type HolderMulti struct {
holders []*Holder
config *FakeDnsPoolMulti
}تُفوّض جميع الدوال لكل holder:
GetFakeIPForDomain()تُعيد عناوين IP من جميع المجمعاتGetFakeIPForDomain3()تُصفّي حسب تفعيل IPv4/IPv6 لكل holderGetDomainFromFakeDNS()تُعيد أول تطابق عبر المجمعاتIsIPInIPPool()تُعيد true إذا كان أي مجمع يحتوي على عنوان IP
تصفية IPv4 مقابل IPv6
تتحقق دالة GetFakeIPForDomain3() في Holder مما إذا كانت عائلة IP الخاصة بالمجمع تُطابق الطلب:
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:
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:
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() فاحصًا مُركّبًا:
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():
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:
{
"fakeDns": {
"ipPool": "198.18.0.0/15",
"poolSize": 65535
}
}أو للمكدس المزدوج:
{
"fakeDns": {
"pools": [
{ "ipPool": "198.18.0.0/15", "poolSize": 65535 },
{ "ipPool": "fc00::/18", "poolSize": 65535 }
]
}
}ثم يُشير خادم DNS إليه:
{
"dns": {
"servers": ["fakedns"]
}
}تدفق البيانات الكامل
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()لمنع مشاكل المتتبع.