رحلة الحزمة
تتتبع هذه الصفحة دورة الحياة الكاملة لاتصال عبر Xray-core، من لحظة اتصال العميل حتى وصول البيانات إلى الخادم البعيد.
نظرة عامة
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():
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:
type Inbound interface {
Network() []net.Network
Process(ctx context.Context, network net.Network,
conn stat.Connection, dispatcher routing.Dispatcher) error
}معالج البروتوكول:
- يقرأ ويفك ترميز رأس البروتوكول من
conn - يستخرج وجهة الهدف (العنوان + المنفذ)
- يُصادق على المستخدم (إن كان ذلك ينطبق)
- يستدعي
dispatcher.Dispatch(ctx, destination)للحصول على زوج أنابيب - ينسخ البيانات بشكل ثنائي الاتجاه بين
connوالأنبوب
مثال: VLESS الوارد (مبسط)
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):
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() يختار معالج الصادر:
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)
مُغلِّف معالج الصادر:
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):
- البحث عن إعدادات التدفق للصادر
- اختيار مُتصل النقل (TCP/WS/gRPC/إلخ.)
- إنشاء الاتصال الخام
- تطبيق طبقة الأمان (TLS/REALITY/بدون)
- إرجاع
stat.Connection
معالجة وكيل الصادر
وكيل الصادر يُرمِّز بروتوكوله وينسخ البيانات:
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)
}مخطط التسلسل الكامل
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ملاحظات التنفيذ
عند إعادة التنفيذ، القطع الحرجة هي:
- سياق الجلسة — يحمل جميع البيانات الوصفية؛ يجب تمريره عبر كل استدعاء
- زوج الأنابيب — الجسر غير المتزامن بين الوارد والصادر؛ يحتاج إلى ضغط عكسي
- الاستشعار — يجب أن يحدث على البايتات الأولى قبل التوجيه؛ تخزين البايتات المُستهلكة مؤقتاً
- النسخ ثنائي الاتجاه — goroutine-ان (رفع + تنزيل) مع إلغاء مشترك
- مؤقت النشاط — يُعاد تعيينه مع كل نقل بيانات؛ يُطلق الإغلاق عند مهلة الخمول
النمط task.Run(ctx, postRequest, task.OnSuccess(getResponse, task.Close(writer))) يُستخدم في كل مكان: تشغيل الرفع أولاً، ثم عند النجاح بدء التنزيل، وإغلاق الكاتب عند انتهاء التنزيل.