الوكيل العكسي: بنية Bridge + Portal
يُتيح الوكيل العكسي في Xray كشف خدمة خلف NAT أو جدار حماية للإنترنت العام. يقوم Bridge (على الجانب الخاص) ببدء اتصالات صادرة نحو Portal (على الجانب العام)، الذي يقوم بدوره بتعدد الإرسال (mux-multiplex) لاتصالات العملاء الواردة عبر أنفاق Bridge.
البنية العامة
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
الميزة الرئيسية التي تحتوي على جميع الجسور والبوابات:
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.
type Bridge struct {
dispatcher routing.Dispatcher
tag string
domain string
workers []*BridgeWorker
monitorTask *task.Periodic
}حلقة المراقبة: تعمل كل ثانيتين. إذا لم يكن هناك عمّال نشطون، أو إذا تجاوز متوسط الاتصالات لكل عامل 16 اتصالاً، يتم إنشاء BridgeWorker جديد.
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:
type BridgeWorker struct {
Tag string
Worker *mux.ServerWorker
Dispatcher routing.Dispatcher
State Control_State
Timer *signal.ActivityTimer
}مسار الإنشاء:
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 الاتصالات الفرعية الواردة من خلاله:
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 من الاتصال الداخلي لتتبع الحالة:
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 العملاء.
type Portal struct {
ohm outbound.Manager
tag string
domain string
picker *StaticMuxPicker
client *mux.ClientManager
}البدء: يضيف معالج Outbound مخصص إلى مدير الاتصالات الصادرة:
func (p *Portal) Start() error {
return p.ohm.AddHandler(context.Background(), &Outbound{
portal: p,
tag: p.tag,
})
}HandleConnection: يُستدعى عندما تصل حركة مرور إلى المعالج الصادر لـ Portal:
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:
- اتصالات Bridge (الوجهة تطابق النطاق المُعَدّ): تنشئ
PortalWorkerجديد يغلّفmux.ClientWorker - اتصالات العملاء (أي وجهة أخرى): يتم تعدد إرسالها عبر أنفاق mux الحالية نحو Bridge
PortalWorker
الملف: app/reverse/portal.go
يدير اتصال mux واحد نحو Bridge، بما في ذلك نبضات القلب والتصريف:
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 ثوانٍ):
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 بايت) لرسالة التحكم لإخفاء حركة المرور:
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 الأقل حمولة وغير المُصرَّف للاتصالات الجديدة:
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 من خلال قناة النطاق الداخلي:
message Control {
enum State {
ACTIVE = 0;
DRAIN = 1;
}
State state = 1;
bytes random = 99; // random padding
}- ACTIVE: النفق متاح لاتصالات جديدة
- DRAIN: النفق قيد الإيقاف (عدد كبير جدًا من إجمالي الاتصالات)
عندما يرسل Portal حالة DRAIN، يُحدّث Bridge حالته ولا يُعتبر عامل Bridge نشطًا بعد ذلك، مما يدفع المراقب لإنشاء بديل.
مخطط تسلسل الاتصال
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الإعدادات
{
"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"يتم اعتراضها للاستخدام الداخلي.