المُوزِّع والاستشعار
DefaultDispatcher هو المحور المركزي الذي يربط وكلاء الوارد بمعالجات الصادر عبر التوجيه. كما يقوم بـاستشعار البروتوكول لاكتشاف البروتوكول الفعلي واسم النطاق من حركة المرور.
المصدر: app/dispatcher/default.go، app/dispatcher/sniffer.go
DefaultDispatcher
type DefaultDispatcher struct {
ohm outbound.Manager // مدير معالجات الصادر
router routing.Router // محرك التوجيه
policy policy.Manager // سياسات المهلة الزمنية
stats stats.Manager // عدادات حركة المرور
fdns dns.FakeDNSEngine // محرك fake DNS (اختياري)
}ينفذ المُوزِّع واجهة routing.Dispatcher:
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 و DispatchLink
Dispatch()— ينشئ زوج أنابيب جديد داخلياً. يُرجع الرابط من جانب الوارد. يُستخدم من قبل معظم وكلاء الوارد. يُشغِّل التوجيه في goroutine (بشكل غير متزامن).DispatchLink()— يقبل رابطاً موجوداً (مثلاً من معالج TUN الذي ينشئ قارئه/كاتبه الخاصين من الاتصال). يُشغِّل التوجيه بشكل متزامن (يحجب حتى اكتمال النقل).
استشعار البروتوكول
عندما يكون الاستشعار مُفعَّلاً، يفحص المُوزِّع البايتات الأولى من حركة المرور لاكتشاف البروتوكول واستخراج أسماء النطاقات.
سلسلة المُستشعرات
// 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)— تطابق البروتوكول لكنه غير مكتمل - خطأ: البروتوكول بشكل قاطع ليس من هذا النوع
عملية الاستشعار
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 قارئ الأنبوب للسماح بالاستشعار دون استهلاك البيانات:
type cachedReader struct {
reader buf.TimeoutReader // قارئ الأنبوب الأصلي
cache buf.MultiBuffer // البايتات المُخزَّنة مؤقتاً
}Cache()— يقرأ مع مهلة زمنية، يُخزِّن في الذاكرة المؤقتة، وينسخ إلى مخزن الاستشعارReadMultiBuffer()— يُرجع البيانات المُخزَّنة مؤقتاً أولاً، ثم يقرأ من القارئ الأساسي- بعد الاستشعار، تُعاد البيانات المُخزَّنة مؤقتاً بشفافية إلى قارئ الصادر
نتائج الاستشعار
type SniffResult interface {
Protocol() string // "http"، "tls"، "bittorrent"، "quic"، "fakedns"
Domain() string // اسم النطاق المُستخرَج (SNI، رأس Host، إلخ.)
}عندما ينجح كل من استشعار البيانات الوصفية (FakeDNS) واستشعار المحتوى، يتم دمجهما:
type compositeResult struct {
domainResult SniffResult // من FakeDNS أو المحتوى
protocolResult SniffResult // من المحتوى
}تجاوز الوجهة
بعد الاستشعار، تُقرر shouldOverride() ما إذا كان يجب استبدال الوجهة:
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 | السلوك |
|---|---|---|
| تجاوز كامل | false | ob.Target = النطاق المُستشعَر (الاتصال يذهب إلى النطاق) |
| للتوجيه فقط | true | ob.RouteTarget = النطاق المُستشعَر (التوجيه يستخدم النطاق، الاتصال يستخدم IP الأصلي) |
| FakeDNS | أي منهما | تجاوز كامل دائماً (يجب حل عناوين IP الوهمية إلى نطاقات حقيقية) |
شرح RouteOnly
مع routeOnly: true:
- يرى المُوجِّه النطاق المُستشعَر لمطابقة القواعد
- لكن اتصال الصادر الفعلي يظل يذهب إلى IP الأصلي
- مفيد عندما تريد توجيهاً مبنياً على النطاق بدون عبء حل DNS
مع routeOnly: false (الافتراضي):
- يحل النطاق المُستشعَر محل الهدف
- الصادر (مثل Freedom) سيحتاج إلى حل النطاق إلى IP
تكامل FakeDNS في الاستشعار
مُستشعر FakeDNS هو مُستشعر بيانات وصفية — لا يحتاج إلى بايتات الحمولة:
// 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() الصادر:
func (d *DefaultDispatcher) routedDispatch(ctx, link, destination) {
// الأولوية:
// 1. وسم الصادر الإجباري (من الواجهة البرمجية/المنصة)
// 2. تطابق قاعدة المُوجِّه
// 3. الصادر الافتراضي (الأول في الإعدادات)
handler.Dispatch(ctx, link)
}يُسجَّل وسم المعالج في ob.Tag للتسجيل والإحصائيات.
ملاحظات التنفيذ
السلوكيات الحرجة الواجب إعادة إنتاجها
الاستشعار غير المتزامن:
Dispatch()يُرجع فوراً؛ الاستشعار + التوجيه يحدث في goroutine. يبدأ وكيل الوارد بالكتابة إلى الأنبوب قبل اتخاذ قرار التوجيه.مهلة الاستشعار: مهلة 200 مللي ثانية مع محاولتين كحد أقصى. لا تنتظر إلى الأبد لبيانات العميل.
شفافية الذاكرة المؤقتة: يجب أن يُرجع القارئ المُخزَّن مؤقتاً البيانات المخزنة قبل قراءة بيانات جديدة. لا يجوز فقدان أي بايتات.
FakeDNS يتجاوز دائماً: إذا كان IP الهدف في التجمع الوهمي، يجب استعادة النطاق بغض النظر عن إعداد
routeOnly.النتائج المركبة: عندما ينجح كل من استشعار البيانات الوصفية واستشعار المحتوى، استخدم بروتوكول المحتوى لكن نطاق البيانات الوصفية (نطاق FakeDNS أكثر موثوقية).
الضغط العكسي: الأنبوب بين الوارد والصادر له حد حجم (من السياسة). إذا كان الصادر بطيئاً، ستحجز كتابة الوارد.