Skip to content

المُوزِّع والاستشعار

DefaultDispatcher هو المحور المركزي الذي يربط وكلاء الوارد بمعالجات الصادر عبر التوجيه. كما يقوم بـاستشعار البروتوكول لاكتشاف البروتوكول الفعلي واسم النطاق من حركة المرور.

المصدر: app/dispatcher/default.go، app/dispatcher/sniffer.go

DefaultDispatcher

go
type DefaultDispatcher struct {
    ohm    outbound.Manager    // مدير معالجات الصادر
    router routing.Router      // محرك التوجيه
    policy policy.Manager      // سياسات المهلة الزمنية
    stats  stats.Manager       // عدادات حركة المرور
    fdns   dns.FakeDNSEngine   // محرك fake DNS (اختياري)
}

ينفذ المُوزِّع واجهة routing.Dispatcher:

go
type Dispatcher interface {
    Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error)
    DispatchLink(ctx context.Context, dest net.Destination, link *transport.Link) error
}
  • Dispatch() — ينشئ زوج أنابيب جديد داخلياً. يُرجع الرابط من جانب الوارد. يُستخدم من قبل معظم وكلاء الوارد. يُشغِّل التوجيه في goroutine (بشكل غير متزامن).

  • DispatchLink() — يقبل رابطاً موجوداً (مثلاً من معالج TUN الذي ينشئ قارئه/كاتبه الخاصين من الاتصال). يُشغِّل التوجيه بشكل متزامن (يحجب حتى اكتمال النقل).

استشعار البروتوكول

عندما يكون الاستشعار مُفعَّلاً، يفحص المُوزِّع البايتات الأولى من حركة المرور لاكتشاف البروتوكول واستخراج أسماء النطاقات.

سلسلة المُستشعرات

go
// app/dispatcher/sniffer.go
func NewSniffer(ctx context.Context) *Sniffer {
    return &Sniffer{
        sniffer: []protocolSnifferWithMetadata{
            {http.SniffHTTP,          false, net.Network_TCP},
            {tls.SniffTLS,            false, net.Network_TCP},
            {bittorrent.SniffBittorrent, false, net.Network_TCP},
            {quic.SniffQUIC,          false, net.Network_UDP},
            {bittorrent.SniffUTP,     false, net.Network_UDP},
            // + مُستشعر FakeDNS (مبني على البيانات الوصفية، لا يحتاج حمولة)
            // + مُستشعر مركب FakeDNS+Others
        },
    }
}

كل مُستشعر يُرجع أحد:

  • نجاح: (SniffResult, nil) — تم اكتشاف البروتوكول، واستُخرج النطاق
  • لا دليل: (nil, common.ErrNoClue) — لا يمكن التحديد بعد، جرب المزيد من البيانات
  • يحتاج المزيد من البيانات: (nil, protocol.ErrProtoNeedMoreData) — تطابق البروتوكول لكنه غير مكتمل
  • خطأ: البروتوكول بشكل قاطع ليس من هذا النوع

عملية الاستشعار

mermaid
flowchart TB
    Start([وصول البيانات]) --> Cache["تخزين البايتات الأولى مؤقتاً<br/>(مهلة 200 مللي ثانية)"]
    Cache --> Meta["SniffMetadata()<br/>(فحص FakeDNS)"]
    Meta --> Content["Sniff(payload, network)"]

    Content --> HTTP{HTTP؟}
    HTTP -->|نعم| Done
    HTTP -->|لا| TLS{TLS SNI؟}
    TLS -->|نعم| Done
    TLS -->|لا| BT{BitTorrent؟}
    BT -->|نعم| Done
    BT -->|لا| QUIC{QUIC SNI؟}
    QUIC -->|نعم| Done
    QUIC -->|لا| Retry{المحاولات < 2<br/>والمهلة > 0؟}
    Retry -->|نعم| Cache
    Retry -->|لا| Timeout[انتهاء مهلة الاستشعار]

    Done([SniffResult])
    Timeout --> MetaFallback{نتيجة البيانات الوصفية<br/>متوفرة؟}
    MetaFallback -->|نعم| Done
    MetaFallback -->|لا| NoResult([لا نتيجة استشعار])

CachedReader

يُغلِّف cachedReader قارئ الأنبوب للسماح بالاستشعار دون استهلاك البيانات:

go
type cachedReader struct {
    reader buf.TimeoutReader  // قارئ الأنبوب الأصلي
    cache  buf.MultiBuffer    // البايتات المُخزَّنة مؤقتاً
}
  • Cache() — يقرأ مع مهلة زمنية، يُخزِّن في الذاكرة المؤقتة، وينسخ إلى مخزن الاستشعار
  • ReadMultiBuffer() — يُرجع البيانات المُخزَّنة مؤقتاً أولاً، ثم يقرأ من القارئ الأساسي
  • بعد الاستشعار، تُعاد البيانات المُخزَّنة مؤقتاً بشفافية إلى قارئ الصادر

نتائج الاستشعار

go
type SniffResult interface {
    Protocol() string  // "http"، "tls"، "bittorrent"، "quic"، "fakedns"
    Domain() string    // اسم النطاق المُستخرَج (SNI، رأس Host، إلخ.)
}

عندما ينجح كل من استشعار البيانات الوصفية (FakeDNS) واستشعار المحتوى، يتم دمجهما:

go
type compositeResult struct {
    domainResult   SniffResult  // من FakeDNS أو المحتوى
    protocolResult SniffResult  // من المحتوى
}

تجاوز الوجهة

بعد الاستشعار، تُقرر shouldOverride() ما إذا كان يجب استبدال الوجهة:

go
func (d *DefaultDispatcher) shouldOverride(ctx, result, request, destination) bool {
    domain := result.Domain()

    // التحقق من قائمة الاستبعاد
    for _, d := range request.ExcludeForDomain {
        if matches(domain, d) { return false }
    }

    // التحقق من قائمة تجاوز البروتوكول
    for _, p := range request.OverrideDestinationForProtocol {
        if matches(protocol, p) { return true }

        // حالة خاصة: FakeDNS
        if p == "fakedns" && fkr0.IsIPInIPPool(destination.Address) {
            return true  // تجاوز دائم لعناوين IP الوهمية
        }
    }
    return false
}

أوضاع التجاوز

تُطبَّق نتيجة الاستشعار بشكل مختلف حسب الإعدادات:

الوضعRouteOnlyالسلوك
تجاوز كاملfalseob.Target = النطاق المُستشعَر (الاتصال يذهب إلى النطاق)
للتوجيه فقطtrueob.RouteTarget = النطاق المُستشعَر (التوجيه يستخدم النطاق، الاتصال يستخدم IP الأصلي)
FakeDNSأي منهماتجاوز كامل دائماً (يجب حل عناوين IP الوهمية إلى نطاقات حقيقية)

شرح RouteOnly

مع routeOnly: true:

  • يرى المُوجِّه النطاق المُستشعَر لمطابقة القواعد
  • لكن اتصال الصادر الفعلي يظل يذهب إلى IP الأصلي
  • مفيد عندما تريد توجيهاً مبنياً على النطاق بدون عبء حل DNS

مع routeOnly: false (الافتراضي):

  • يحل النطاق المُستشعَر محل الهدف
  • الصادر (مثل Freedom) سيحتاج إلى حل النطاق إلى IP

تكامل FakeDNS في الاستشعار

مُستشعر FakeDNS هو مُستشعر بيانات وصفية — لا يحتاج إلى بايتات الحمولة:

go
// app/dispatcher/fakednssniffer.go
func newFakeDNSSniffer(ctx) (protocolSnifferWithMetadata, error) {
    // يُرجع مُستشعراً يتحقق مما إذا كان IP الهدف في التجمع الوهمي
    // إذا نعم، يبحث عن النطاق من ذاكرة fake DNS المؤقتة
    return protocolSnifferWithMetadata{
        protocolSniffer: func(ctx, _) (SniffResult, error) {
            dest := session.OutboundFromContext(ctx).Target
            if fkr0.IsIPInIPPool(dest.Address) {
                domain := fkr0.GetDomainFromFakeDNS(dest.Address)
                return &fakeDNSSniffResult{domain: domain}, nil
            }
            return nil, common.ErrNoClue
        },
        metadataSniffer: true,  // يُستدعى بدون حمولة
        network: net.Network_TCP,
    }
}

المُستشعر المركب fakedns+others يجمع بين بحث نطاقات FakeDNS واكتشاف البروتوكول المبني على المحتوى.

التوزيع المُوجَّه

بعد الاستشعار وتجاوز الوجهة، يختار routedDispatch() الصادر:

go
func (d *DefaultDispatcher) routedDispatch(ctx, link, destination) {
    // الأولوية:
    // 1. وسم الصادر الإجباري (من الواجهة البرمجية/المنصة)
    // 2. تطابق قاعدة المُوجِّه
    // 3. الصادر الافتراضي (الأول في الإعدادات)

    handler.Dispatch(ctx, link)
}

يُسجَّل وسم المعالج في ob.Tag للتسجيل والإحصائيات.

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

السلوكيات الحرجة الواجب إعادة إنتاجها

  1. الاستشعار غير المتزامن: Dispatch() يُرجع فوراً؛ الاستشعار + التوجيه يحدث في goroutine. يبدأ وكيل الوارد بالكتابة إلى الأنبوب قبل اتخاذ قرار التوجيه.

  2. مهلة الاستشعار: مهلة 200 مللي ثانية مع محاولتين كحد أقصى. لا تنتظر إلى الأبد لبيانات العميل.

  3. شفافية الذاكرة المؤقتة: يجب أن يُرجع القارئ المُخزَّن مؤقتاً البيانات المخزنة قبل قراءة بيانات جديدة. لا يجوز فقدان أي بايتات.

  4. FakeDNS يتجاوز دائماً: إذا كان IP الهدف في التجمع الوهمي، يجب استعادة النطاق بغض النظر عن إعداد routeOnly.

  5. النتائج المركبة: عندما ينجح كل من استشعار البيانات الوصفية واستشعار المحتوى، استخدم بروتوكول المحتوى لكن نطاق البيانات الوصفية (نطاق FakeDNS أكثر موثوقية).

  6. الضغط العكسي: الأنبوب بين الوارد والصادر له حد حجم (من السياسة). إذا كان الصادر بطيئاً، ستحجز كتابة الوارد.

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