Skip to content

الوكيل العكسي: بنية Bridge + Portal

يُتيح الوكيل العكسي في Xray كشف خدمة خلف NAT أو جدار حماية للإنترنت العام. يقوم Bridge (على الجانب الخاص) ببدء اتصالات صادرة نحو Portal (على الجانب العام)، الذي يقوم بدوره بتعدد الإرسال (mux-multiplex) لاتصالات العملاء الواردة عبر أنفاق Bridge.

البنية العامة

mermaid
flowchart LR
    subgraph Private Network
        S[Local Service] <-->|direct| BW[BridgeWorker]
        BW <-->|mux tunnel| B[Bridge]
    end

    subgraph Public Network
        B <-->|outbound connection| P[Portal]
        P <-->|Outbound handler| PW[PortalWorker]
        PW <-->|mux dispatch| C[External Client]
    end

الفكرة الجوهرية: يبدأ Bridge الاتصال، لكن Portal يتحكم في mux. من منظور بروتوكول mux:

  • يشغّل جانب Bridge مكوّن mux.ServerWorker (يستقبل الاتصالات الفرعية)
  • يشغّل جانب Portal مكوّن mux.ClientWorker (ينشئ الاتصالات الفرعية)

هذا العكس هو ما يجعل الوكيل العكسي يعمل -- الجانب الذي يستقبل اتصال TCP (أي Portal) هو من يوزّع التدفقات الجديدة، بينما الجانب الذي بدأ اتصال TCP (أي Bridge) يستقبلها ويعالجها.

الأنواع الأساسية

Reverse

الملف: app/reverse/reverse.go

الميزة الرئيسية التي تحتوي على جميع الجسور والبوابات:

go
type Reverse struct {
    bridges []*Bridge
    portals []*Portal
}

func (r *Reverse) Init(config *Config, d routing.Dispatcher, ohm outbound.Manager) error {
    for _, bConfig := range config.BridgeConfig {
        b, _ := NewBridge(bConfig, d)
        r.bridges = append(r.bridges, b)
    }
    for _, pConfig := range config.PortalConfig {
        p, _ := NewPortal(pConfig, ohm)
        r.portals = append(r.portals, p)
    }
}

يتطلب كلاً من routing.Dispatcher (لـ Bridge) و outbound.Manager (لـ Portal) عبر core.RequireFeatures().

Bridge

الملف: app/reverse/bridge.go

يعمل Bridge على نسخة Xray في الجانب الخاص. يدير مجموعة من اتصالات BridgeWorker نحو Portal.

go
type Bridge struct {
    dispatcher  routing.Dispatcher
    tag         string
    domain      string
    workers     []*BridgeWorker
    monitorTask *task.Periodic
}

حلقة المراقبة: تعمل كل ثانيتين. إذا لم يكن هناك عمّال نشطون، أو إذا تجاوز متوسط الاتصالات لكل عامل 16 اتصالاً، يتم إنشاء BridgeWorker جديد.

go
func (b *Bridge) monitor() error {
    b.cleanup()  // remove closed workers

    var numConnections uint32
    var numWorker uint32
    for _, w := range b.workers {
        if w.IsActive() {
            numConnections += w.Connections()
            numWorker++
        }
    }
    // Spawn new worker if needed
    if numWorker == 0 || numConnections/numWorker > 16 {
        worker, _ := NewBridgeWorker(b.domain, b.tag, b.dispatcher)
        b.workers = append(b.workers, worker)
    }
}

BridgeWorker

الملف: app/reverse/bridge.go

كل BridgeWorker ينشئ نفق mux واحد نحو Portal:

go
type BridgeWorker struct {
    Tag        string
    Worker     *mux.ServerWorker
    Dispatcher routing.Dispatcher
    State      Control_State
    Timer      *signal.ActivityTimer
}

مسار الإنشاء:

go
func NewBridgeWorker(domain string, tag string, d routing.Dispatcher) (*BridgeWorker, error) {
    ctx := session.ContextWithInbound(context.Background(), &session.Inbound{Tag: tag})

    // 1. Dispatch to the Portal's domain (routes through outbound)
    link, _ := d.Dispatch(ctx, net.Destination{
        Network: net.Network_TCP,
        Address: net.DomainAddress(domain),
        Port:    0,
    })

    // 2. Create mux.ServerWorker over this link
    worker, _ := mux.NewServerWorker(context.Background(), w, link)

    // 3. Set inactivity timeout (60 seconds)
    w.Timer = signal.CancelAfterInactivity(ctx, terminate, 60*time.Second)
}

يُنفّذ BridgeWorker واجهة routing.Dispatcher، لذا يوزّع mux.ServerWorker الاتصالات الفرعية الواردة من خلاله:

go
func (w *BridgeWorker) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) {
    if !isInternalDomain(dest) {
        // Real traffic: dispatch locally
        return w.Dispatcher.Dispatch(ctx, dest)
    }
    // Control channel: handle internally
    go w.handleInternalConn(link)
    return link, nil
}

النطاق الداخلي ("reverse"): يُستخدم لرسائل التحكم بين Bridge و Portal. يقرأ Bridge رسائل Control بتنسيق protobuf من الاتصال الداخلي لتتبع الحالة:

go
func (w *BridgeWorker) handleInternalConn(link *transport.Link) {
    for {
        mb, err := reader.ReadMultiBuffer()
        for _, b := range mb {
            var ctl Control
            proto.Unmarshal(b.Bytes(), &ctl)
            if ctl.State != w.State {
                w.State = ctl.State  // ACTIVE or DRAIN
            }
        }
    }
}

Portal

الملف: app/reverse/portal.go

يعمل Portal على نسخة Xray في الجانب العام. يسجّل معالج صادر ويدير عمّال mux العملاء.

go
type Portal struct {
    ohm    outbound.Manager
    tag    string
    domain string
    picker *StaticMuxPicker
    client *mux.ClientManager
}

البدء: يضيف معالج Outbound مخصص إلى مدير الاتصالات الصادرة:

go
func (p *Portal) Start() error {
    return p.ohm.AddHandler(context.Background(), &Outbound{
        portal: p,
        tag:    p.tag,
    })
}

HandleConnection: يُستدعى عندما تصل حركة مرور إلى المعالج الصادر لـ Portal:

go
func (p *Portal) HandleConnection(ctx context.Context, link *transport.Link) error {
    if isDomain(ob.Target, p.domain) {
        // Bridge connection: create a mux.ClientWorker
        muxClient, _ := mux.NewClientWorker(*link, mux.ClientStrategy{})
        worker, _ := NewPortalWorker(muxClient)
        p.picker.AddWorker(worker)
        return nil
    }
    // Client connection: dispatch through mux to Bridge
    return p.client.Dispatch(ctx, link)
}

نوعان من الاتصالات يصلان إلى Portal:

  1. اتصالات Bridge (الوجهة تطابق النطاق المُعَدّ): تنشئ PortalWorker جديد يغلّف mux.ClientWorker
  2. اتصالات العملاء (أي وجهة أخرى): يتم تعدد إرسالها عبر أنفاق mux الحالية نحو Bridge

PortalWorker

الملف: app/reverse/portal.go

يدير اتصال mux واحد نحو Bridge، بما في ذلك نبضات القلب والتصريف:

go
type PortalWorker struct {
    client   *mux.ClientWorker
    control  *task.Periodic
    writer   buf.Writer
    reader   buf.Reader
    draining bool
    counter  uint32
    timer    *signal.ActivityTimer
}

نبضات القلب: تعمل كل ثانيتين، وترسل رسالة Control كل 5 دورات (10 ثوانٍ):

go
func (w *PortalWorker) heartbeat() error {
    msg := &Control{}
    msg.FillInRandom()

    // Auto-drain after 256 total connections
    if w.client.TotalConnections() > 256 {
        w.draining = true
        msg.State = Control_DRAIN
    }

    w.counter = (w.counter + 1) % 5
    if w.draining || w.counter == 1 {
        b, _ := proto.Marshal(msg)
        return w.writer.WriteMultiBuffer(buf.MergeBytes(nil, b))
    }
    return nil
}

تضيف دالة FillInRandom() حشوًا عشوائيًا (من 1 إلى 65 بايت) لرسالة التحكم لإخفاء حركة المرور:

go
func (c *Control) FillInRandom() {
    randomLength := dice.Roll(64) + 1
    c.Random = make([]byte, randomLength)
    io.ReadFull(rand.Reader, c.Random)
}

StaticMuxPicker

الملف: app/reverse/portal.go

يختار PortalWorker الأقل حمولة وغير المُصرَّف للاتصالات الجديدة:

go
func (p *StaticMuxPicker) PickAvailable() (*mux.ClientWorker, error) {
    // 1. Try non-draining workers first, pick minimum active connections
    // 2. If all are draining, pick from draining workers
    // 3. Skip full workers
}

يتم تنظيف العمّال المغلقين كل 30 ثانية.

بروتوكول التحكم

يتواصل Bridge و Portal عبر رسائل Control بتنسيق protobuf من خلال قناة النطاق الداخلي:

protobuf
message Control {
    enum State {
        ACTIVE = 0;
        DRAIN = 1;
    }
    State state = 1;
    bytes random = 99;  // random padding
}
  • ACTIVE: النفق متاح لاتصالات جديدة
  • DRAIN: النفق قيد الإيقاف (عدد كبير جدًا من إجمالي الاتصالات)

عندما يرسل Portal حالة DRAIN، يُحدّث Bridge حالته ولا يُعتبر عامل Bridge نشطًا بعد ذلك، مما يدفع المراقب لإنشاء بديل.

مخطط تسلسل الاتصال

mermaid
sequenceDiagram
    participant Client
    participant PortalOutbound as Portal Outbound
    participant Portal
    participant MuxTunnel as Mux Tunnel
    participant Bridge as BridgeWorker
    participant LocalService as Local Service

    Note over Bridge, Portal: Bridge initiates tunnel
    Bridge->>Portal: Connect to domain "example.reverse"
    Portal->>Portal: isDomain match -> create PortalWorker
    Portal->>Bridge: Mux established

    Note over Portal, Bridge: Heartbeat loop
    loop Every 10s
        Portal->>Bridge: Control{State: ACTIVE}
        Bridge->>Bridge: Update state
    end

    Note over Client, LocalService: Client request
    Client->>PortalOutbound: Connect to target.com:80
    PortalOutbound->>Portal: HandleConnection
    Portal->>MuxTunnel: Dispatch via mux.ClientManager
    MuxTunnel->>Bridge: New mux sub-stream
    Bridge->>Bridge: BridgeWorker.Dispatch(target.com:80)
    Bridge->>LocalService: Forward to local service
    LocalService-->>Bridge: Response
    Bridge-->>MuxTunnel: Response via mux
    MuxTunnel-->>Portal: Response
    Portal-->>Client: Response

الإعدادات

json
{
    "reverse": {
        "bridges": [
            { "tag": "bridge", "domain": "test.example.com" }
        ],
        "portals": [
            { "tag": "portal", "domain": "test.example.com" }
        ]
    }
}

يُحدّد tag في جانب Bridge وسم الوارد لحركة المرور المُوزَّعة. يجب أن يتطابق domain بين إعدادات Bridge و Portal. يجب أن توجّه قواعد التوجيه حركة المرور المُوجَّهة للنطاق إلى المعالج الصادر المناسب في جانب Bridge، ويجب استخدام وسم Portal كمعالج صادر في قواعد التوجيه لحركة مرور العملاء.

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

  • ينشئ Bridge العمّال بشكل كسول عبر مهمة المراقبة. عند البدء الأول، يكتشف المراقب فورًا أن numWorker == 0 وينشئ أول BridgeWorker.

  • بنية Outbound المُسجّلة بواسطة Portal تُنفّذ واجهة outbound.Handler مع دوال بسيطة (Tag()، Dispatch()، Start()، Close()). كما تحتوي على دوال مُعطّلة SenderSettings() و ProxySettings() التي تُرجع nil.

  • مؤقت عدم النشاط في BridgeWorker هو 60 ثانية. إذا لم يحدث أي نشاط mux، ينتهي العامل. يمتد المؤقت إلى 24 ساعة عندما يكون اتصال التحكم الداخلي نشطًا.

  • مؤقت PortalWorker هو 24 ساعة، وذلك بشكل أساسي لمنع تسرّب الـ goroutines بدلاً من إدارة حركة المرور.

  • يتم تنظيف عمّال Bridge بواسطة دالة cleanup() في المراقب، التي تتحقق من كل من IsActive() (الحالة ACTIVE و mux غير مغلق) و Closed() (عامل mux منتهٍ بالكامل).

  • يدعم تنفيذ mux كلاً من التدفقات الفرعية TCP و UDP. بالنسبة لـ UDP، يطبّق Portal مكوّنات EndpointOverrideReader/EndpointOverrideWriter لإعادة تعيين العناوين بين الوجهات الأصلية والمستهدفة.

  • الثابت internalDomain = "reverse" مُبرمج بشكل ثابت ويُستخدم كعلامة مميزة لحركة قناة التحكم. أي وجهة بعنوان "reverse" يتم اعتراضها للاستخدام الداخلي.

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