Skip to content

رحلة الحزمة

تتتبع هذه الصفحة دورة الحياة الكاملة لاتصال عبر Xray-core، من لحظة اتصال العميل حتى وصول البيانات إلى الخادم البعيد.

نظرة عامة

mermaid
flowchart TB
    Client([تطبيق العميل]) -->|"الاتصال بمنفذ<br/>الاستماع"| Listener

    subgraph Inbound["الوارد (app/proxyman/inbound)"]
        Listener["internet.Listener<br/>(TCP Hub)"]
        Worker["tcpWorker / udpWorker"]
        Proxy["proxy.Inbound.Process()<br/>(VLESS/VMess/Trojan/...)"]
    end

    subgraph Core["خط الأنابيب الأساسي"]
        Dispatcher["DefaultDispatcher.Dispatch()"]
        Sniff["المُستشعر<br/>(HTTP/TLS/QUIC/FakeDNS)"]
        Router["Router.PickRoute()"]
    end

    subgraph Outbound["الصادر (app/proxyman/outbound)"]
        OHandler["outbound.Handler.Dispatch()"]
        Mux["Mux ClientManager<br/>(إذا كان mux مُفعَّلاً)"]
        OProxy["proxy.Outbound.Process()<br/>(VLESS/Freedom/...)"]
        Transport["internet.Dialer.Dial()<br/>(TCP/WS/gRPC/...)"]
    end

    Listener -->|stat.Connection| Worker
    Worker -->|"بناء ctx + استدعاء"| Proxy
    Proxy -->|"dispatcher.Dispatch(ctx, dest)"| Dispatcher
    Dispatcher --> Sniff
    Sniff --> Router
    Router -->|وسم الصادر| OHandler
    OHandler --> Mux
    Mux --> OProxy
    OProxy --> Transport
    Transport -->|"اتصال مُشفَّر"| Server([البعيد/الهدف])

المرحلة 1: قبول الاتصال

عامل TCP (app/proxyman/inbound/worker.go)

عند وصول اتصال TCP، تُطلق الدالة tcpWorker.callback():

go
func (w *tcpWorker) callback(conn stat.Connection) {
    ctx, cancel := context.WithCancel(w.ctx)
    sid := session.NewID()
    ctx = c.ContextWithID(ctx, sid)

    // بناء بيانات الصادر الوصفية
    outbounds := []*session.Outbound{{}}

    // للوكيل الشفاف: الحصول على الوجهة الأصلية
    if w.recvOrigDest {
        switch getTProxyType(w.stream) {
        case internet.SocketConfig_Redirect:
            dest, _ = tcp.GetOriginalDestination(conn)
        case internet.SocketConfig_TProxy:
            dest = net.DestinationFromAddr(conn.LocalAddr())
        }
        outbounds[0].Target = dest
    }
    ctx = session.ContextWithOutbounds(ctx, outbounds)

    // إرفاق بيانات الوارد الوصفية
    ctx = session.ContextWithInbound(ctx, &session.Inbound{
        Source:  net.DestinationFromAddr(conn.RemoteAddr()),
        Gateway: net.TCPDestination(w.address, w.port),
        Tag:     w.tag,
        Conn:    conn,
    })

    // إرفاق إعدادات الاستشعار
    content := new(session.Content)
    content.SniffingRequest = ... // من الإعدادات
    ctx = session.ContextWithContent(ctx, content)

    // تسليم إلى معالج البروتوكول
    w.proxy.Process(ctx, net.Network_TCP, conn, w.dispatcher)
}

قيم السياق الرئيسية المُعيَّنة هنا:

  • session.Inbound — عنوان المصدر، وسم الوارد، الاتصال الخام
  • session.Outbound — الهدف (يُملأ لـ TProxy/redirect)
  • session.Content — إعدادات الاستشعار

عامل UDP

بالنسبة لـ UDP، يعالج udpWorker الحزم بطريقة مختلفة:

  • يستخدم udp.Dispatcher لإدارة "اتصالات" UDP (مفتاحها عنوان المصدر)
  • كل مصدر فريد يحصل على اتصال افتراضي يُمرَّر عبر الوكيل
  • تنظيف بناءً على المهلة الزمنية لجلسات UDP الخاملة

المرحلة 2: معالجة البروتوكول (الوارد)

كل بروتوكول وكيل ينفذ واجهة proxy.Inbound:

go
type Inbound interface {
    Network() []net.Network
    Process(ctx context.Context, network net.Network,
        conn stat.Connection, dispatcher routing.Dispatcher) error
}

معالج البروتوكول:

  1. يقرأ ويفك ترميز رأس البروتوكول من conn
  2. يستخرج وجهة الهدف (العنوان + المنفذ)
  3. يُصادق على المستخدم (إن كان ذلك ينطبق)
  4. يستدعي dispatcher.Dispatch(ctx, destination) للحصول على زوج أنابيب
  5. ينسخ البيانات بشكل ثنائي الاتجاه بين conn والأنبوب

مثال: VLESS الوارد (مبسط)

go
func (h *Handler) Process(ctx, network, connection, dispatch) error {
    // قراءة البايتات الأولى
    first := buf.FromBytes(make([]byte, buf.Size))
    first.ReadFrom(connection)

    // فك ترميز رأس VLESS
    userSentID, request, requestAddons, err :=
        encoding.DecodeRequestHeader(first, reader, h.validator)

    // تعيين المستخدم في السياق
    ctx = session.ContextWithInbound(ctx, &session.Inbound{
        User: user,
        ...
    })

    // التوزيع إلى التوجيه
    link, _ := dispatch.Dispatch(ctx, request.Destination())

    // النسخ ثنائي الاتجاه
    // الرفع: connection → link.Writer (إلى الصادر)
    // التنزيل: link.Reader → connection (إلى العميل)
    task.Run(ctx, requestDone, responseDone)
}

المرحلة 3: التوزيع

DefaultDispatcher.Dispatch() هو المحور المركزي (app/dispatcher/default.go):

go
func (d *DefaultDispatcher) Dispatch(ctx, destination) (*transport.Link, error) {
    // تعيين الهدف في بيانات الصادر الوصفية
    ob.OriginalTarget = destination
    ob.Target = destination

    // إنشاء زوج أنابيب
    inbound, outbound := d.getLink(ctx)

    if sniffingRequest.Enabled {
        go func() {
            // تغليف القارئ بالتخزين المؤقت
            cReader := &cachedReader{reader: outbound.Reader}
            outbound.Reader = cReader

            // استشعار البايتات الأولى
            result, err := sniffer(ctx, cReader, ...)

            // تجاوز الوجهة إذا تطابق الاستشعار
            if d.shouldOverride(ctx, result, ...) {
                destination.Address = net.ParseAddress(result.Domain())
                ob.Target = destination // أو ob.RouteTarget لـ RouteOnly
            }

            d.routedDispatch(ctx, outbound, destination)
        }()
    } else {
        go d.routedDispatch(ctx, outbound, destination)
    }

    return inbound, nil  // يُعاد إلى وكيل الوارد
}

زوج الأنابيب

getLink() ينشئ زوجي أنابيب مترابطين:

Client ←→ [InboundLink] ←→ Pipe ←→ [OutboundLink] ←→ Server

InboundLink:                    OutboundLink:
  Reader = downlinkReader         Reader = uplinkReader
  Writer = uplinkWriter           Writer = downlinkWriter

Client writes → uplinkWriter → uplinkReader → Server reads
Server writes → downlinkWriter → downlinkReader → Client reads

إذا كانت الإحصائيات مُفعَّلة، يتم إدراج مُغلِّفات SizeStatWriter لحساب البايتات.

المرحلة 4: التوجيه

routedDispatch() يختار معالج الصادر:

go
func (d *DefaultDispatcher) routedDispatch(ctx, link, destination) {
    // 1. التحقق من وسم الصادر الإجباري (من المنصة/الواجهة البرمجية)
    if forcedTag := session.GetForcedOutboundTagFromContext(ctx); forcedTag != "" {
        handler = d.ohm.GetHandler(forcedTag)
    }
    // 2. طلب من المُوجِّه اختيار المسار
    else if route, err := d.router.PickRoute(routingCtx); err == nil {
        handler = d.ohm.GetHandler(route.GetOutboundTag())
    }
    // 3. الرجوع إلى الصادر الافتراضي
    else {
        handler = d.ohm.GetDefaultHandler()
    }

    // التوزيع إلى الصادر المُختار
    handler.Dispatch(ctx, link)
}

يقيّم المُوجِّه القواعد بالتسلسل (انظر محرك التوجيه).

المرحلة 5: معالجة الصادر

معالج الصادر (app/proxyman/outbound/handler.go)

مُغلِّف معالج الصادر:

go
func (h *Handler) Dispatch(ctx, link) {
    // التحقق من mux
    if h.mux != nil && shouldUseMux(ctx) {
        h.mux.Dispatch(ctx, link)
        return
    }

    // معالجة مباشرة عبر الوكيل
    h.proxy.Process(ctx, link, h)  // h ينفذ internet.Dialer
}

الاتصال عبر طبقة النقل

عندما يستدعي proxy.Process() الدالة dialer.Dial(ctx, dest):

  1. البحث عن إعدادات التدفق للصادر
  2. اختيار مُتصل النقل (TCP/WS/gRPC/إلخ.)
  3. إنشاء الاتصال الخام
  4. تطبيق طبقة الأمان (TLS/REALITY/بدون)
  5. إرجاع stat.Connection

معالجة وكيل الصادر

وكيل الصادر يُرمِّز بروتوكوله وينسخ البيانات:

go
func (h *Handler) Process(ctx, link, dialer) error {
    // إنشاء اتصال النقل
    conn, _ := dialer.Dial(ctx, serverAddress)

    // ترميز رأس البروتوكول
    encoding.EncodeRequestHeader(conn, request, addons)

    // النسخ ثنائي الاتجاه
    // الرفع: link.Reader → conn (إلى الخادم)
    // التنزيل: conn → link.Writer (إلى العميل عبر الأنبوب)
    task.Run(ctx, postRequest, getResponse)
}

مخطط التسلسل الكامل

mermaid
sequenceDiagram
    participant C as العميل
    participant TW as tcpWorker
    participant PI as وكيل الوارد
    participant D as المُوزِّع
    participant R as المُوجِّه
    participant PO as وكيل الصادر
    participant T as النقل
    participant S as الخادم البعيد

    C->>TW: اتصال TCP
    TW->>TW: بناء سياق الجلسة
    TW->>PI: Process(ctx, conn, dispatcher)
    PI->>PI: فك ترميز رأس البروتوكول
    PI->>D: Dispatch(ctx, destination)
    D->>D: إنشاء زوج أنابيب
    D-->>PI: إرجاع inboundLink
    Note over D: goroutine غير متزامن:
    D->>D: استشعار البايتات الأولى
    D->>R: PickRoute(ctx)
    R-->>D: وسم الصادر
    D->>PO: handler.Dispatch(ctx, outboundLink)
    PO->>T: dialer.Dial(ctx, server)
    T->>S: اتصال النقل + TLS
    T-->>PO: conn
    PO->>S: ترميز الرأس + الحمولة

    par الرفع (عميل → خادم)
        PI->>D: pipe.Write (بيانات العميل)
        D->>PO: pipe.Read → conn.Write
    and التنزيل (خادم → عميل)
        S->>PO: conn.Read
        PO->>D: pipe.Write (بيانات الخادم)
        D->>PI: pipe.Read → conn.Write
        PI->>C: بيانات الاستجابة
    end

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

عند إعادة التنفيذ، القطع الحرجة هي:

  1. سياق الجلسة — يحمل جميع البيانات الوصفية؛ يجب تمريره عبر كل استدعاء
  2. زوج الأنابيب — الجسر غير المتزامن بين الوارد والصادر؛ يحتاج إلى ضغط عكسي
  3. الاستشعار — يجب أن يحدث على البايتات الأولى قبل التوجيه؛ تخزين البايتات المُستهلكة مؤقتاً
  4. النسخ ثنائي الاتجاه — goroutine-ان (رفع + تنزيل) مع إلغاء مشترك
  5. مؤقت النشاط — يُعاد تعيينه مع كل نقل بيانات؛ يُطلق الإغلاق عند مهلة الخمول

النمط task.Run(ctx, postRequest, task.OnSuccess(getResponse, task.Close(writer))) يُستخدم في كل مكان: تشغيل الرفع أولاً، ثم عند النجاح بدء التنزيل، وإغلاق الكاتب عند انتهاء التنزيل.

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