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. الضغط العكسي: الأنبوب بين الوارد والصادر له حد حجم (من السياسة). إذا كان الصادر بطيئاً، ستحجز كتابة الوارد.

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