نظرة عامة على بنية طبقة النقل
مقدمة
توفر طبقة النقل في Xray-core تجريدًا قابلاً للتوصيل فوق الاتصالات الشبكية. يقوم كل بروتوكول نقل (TCP، WebSocket، gRPC، HTTPUpgrade، SplitHTTP، mKCP) بتسجيل نفسه في سجل عام عند بدء التشغيل، ويقوم النواة بتوجيه استدعاءات الاتصال والاستماع بناءً على الإعدادات. تعمل طبقة أمان موازية (TLS، REALITY) على تغليف وسائل النقل بشكل مستقل. يغطي هذا المستند نمط التسجيل، والواجهات الأساسية، وكائن الإعدادات MemoryStreamConfig، وكيفية اختيار وسائل النقل أثناء التشغيل.
الواجهات الرئيسية
واجهة Dialer
واجهة Dialer (transport/internet/dialer.go:22-31) هي التجريد عالي المستوى للاتصالات الصادرة:
type Dialer interface {
Dial(ctx context.Context, destination net.Destination) (stat.Connection, error)
DestIpAddress() net.IP
SetOutboundGateway(ctx context.Context, ob *session.Outbound)
}داخليًا، تستخدم طبقة النقل توقيع دالة منخفض المستوى لمتصلي كل بروتوكول:
// dialer.go:34
type dialFunc func(ctx context.Context, dest net.Destination, streamSettings *MemoryStreamConfig) (stat.Connection, error)واجهة Listener
واجهة Listener (transport/internet/tcp_hub.go:25-28) تمثل مستمعًا من جانب الخادم:
type Listener interface {
Close() error
Addr() net.Addr
}يقوم كل بروتوكول نقل أيضًا بتسجيل ListenFunc (tcp_hub.go:23):
type ListenFunc func(ctx context.Context, address net.Address, port net.Port,
settings *MemoryStreamConfig, handler ConnHandler) (Listener, error)حيث ConnHandler هي ببساطة func(stat.Connection).
واجهة SystemDialer
SystemDialer (transport/internet/system_dialer.go:18-21) هو التجريد الأدنى مستوى الذي ينشئ اتصالات فعلية على مستوى نظام التشغيل:
type SystemDialer interface {
Dial(ctx context.Context, source net.Address, destination net.Destination,
sockopt *SocketConfig) (net.Conn, error)
DestIpAddress() net.IP
}التطبيق الافتراضي (DefaultSystemDialer) يستدعي net.Dialer.DialContext من Go مع تطبيق خيارات المقبس عبر RawConn.Control.
نمط التسجيل
سجل متصلي النقل
يقوم كل بروتوكول نقل بتسجيل المتصل الخاص به في دالة init() باستخدام RegisterTransportDialer (dialer.go:39-45):
var transportDialerCache = make(map[string]dialFunc)
func RegisterTransportDialer(protocol string, dialer dialFunc) error {
if _, found := transportDialerCache[protocol]; found {
return errors.New(protocol, " dialer already registered").AtError()
}
transportDialerCache[protocol] = dialer
return nil
}مثال من transport/internet/tcp/dialer.go:110-112:
func init() {
common.Must(internet.RegisterTransportDialer(protocolName, Dial))
}سجل مستمعي النقل
بالمثل، يتم تسجيل المستمعين عبر RegisterTransportListener (tcp_hub.go:13-19):
var transportListenerCache = make(map[string]ListenFunc)
func RegisterTransportListener(protocol string, listener ListenFunc) error {
if _, found := transportListenerCache[protocol]; found {
return errors.New(protocol, " listener already registered.").AtError()
}
transportListenerCache[protocol] = listener
return nil
}سجل منشئ الإعدادات
يقوم كل بروتوكول نقل أيضًا بتسجيل مصنع إعدادات (config.go:32-38):
var globalTransportConfigCreatorCache = make(map[string]ConfigCreator)
func RegisterProtocolConfigCreator(name string, creator ConfigCreator) error {
if _, found := globalTransportConfigCreatorCache[name]; found {
return errors.New("protocol ", name, " is already registered").AtError()
}
globalTransportConfigCreatorCache[name] = creator
return nil
}يتم استدعاء هذا من دالة init() في ملف config.go الخاص بكل بروتوكول نقل، مثل transport/internet/tcp/config.go:8-12.
أسماء البروتوكولات المسجلة
| النقل | اسم البروتوكول | ملف المتصل | ملف المستمع |
|---|---|---|---|
| TCP | "tcp" | tcp/dialer.go | tcp/hub.go |
| WebSocket | "websocket" | websocket/dialer.go | websocket/hub.go |
| gRPC | "grpc" | grpc/dial.go | grpc/hub.go |
| HTTPUpgrade | "httpupgrade" | httpupgrade/dialer.go | httpupgrade/hub.go |
| SplitHTTP | "splithttp" | splithttp/dialer.go | splithttp/hub.go |
| mKCP | "mkcp" | kcp/dialer.go | kcp/listener.go |
MemoryStreamConfig
MemoryStreamConfig (transport/internet/memory_settings.go:9-19) هو الشكل المحلل في الذاكرة لرسالة protobuf StreamConfig. يتجنب إلغاء تسلسل protobuf المتكرر أثناء التشغيل:
type MemoryStreamConfig struct {
Destination *net.Destination
ProtocolName string
ProtocolSettings interface{}
SecurityType string
SecuritySettings interface{}
TcpmaskManager *finalmask.TcpmaskManager
UdpmaskManager *finalmask.UdpmaskManager
SocketSettings *SocketConfig
DownloadSettings *MemoryStreamConfig
}الحقول الرئيسية:
- ProtocolName: مثل
"tcp"،"websocket"،"grpc"-- يحدد أي متصل/مستمع مسجل سيتم استخدامه. - ProtocolSettings: إعدادات خاصة بالنقل (مثل
*tcp.Config،*websocket.Config). يتم التحويل عبر تأكيد النوع. - SecurityType / SecuritySettings:
"tls"أو"reality"مع كائن الإعدادات المقابل. - SocketSettings: خيارات المقبس منخفضة المستوى (protobuf
SocketConfig). - DownloadSettings: يُستخدم بواسطة SplitHTTP لإعدادات تيار التنزيل المنفصلة.
- TcpmaskManager / UdpmaskManager: طبقة تشويش الحزم (finalmask).
دالة التحويل ToMemoryStreamConfig (memory_settings.go:22-78) تحلل protobuf StreamConfig:
func ToMemoryStreamConfig(s *StreamConfig) (*MemoryStreamConfig, error) {
ets, err := s.GetEffectiveTransportSettings()
// ... builds MemoryStreamConfig from StreamConfig fields
}تدفق اختيار النقل
الصادر (Dial)
نقطة الدخول الرئيسية هي internet.Dial (dialer.go:48-75):
func Dial(ctx context.Context, dest net.Destination, streamSettings *MemoryStreamConfig) (stat.Connection, error) {
if dest.Network == net.Network_TCP {
if streamSettings == nil {
s, _ := ToMemoryStreamConfig(nil)
streamSettings = s
}
protocol := streamSettings.ProtocolName
dialer := transportDialerCache[protocol]
if dialer == nil {
return nil, errors.New(protocol, " dialer not registered")
}
return dialer(ctx, dest, streamSettings)
}
if dest.Network == net.Network_UDP {
udpDialer := transportDialerCache["udp"]
// ...
}
}عندما يكون streamSettings فارغًا (nil)، تنتج ToMemoryStreamConfig(nil) إعدادات افتراضية مع ProtocolName = "tcp" (من config.go:58-64).
الوارد (Listen)
يتم الاستماع عبر ListenTCP أو ListenUnix (tcp_hub.go:52-79):
func ListenTCP(ctx context.Context, address net.Address, port net.Port,
settings *MemoryStreamConfig, handler ConnHandler) (Listener, error) {
// ...
protocol := settings.ProtocolName
listenFunc := transportListenerCache[protocol]
listener, err := listenFunc(ctx, address, port, settings, handler)
return listener, nil
}حل الإعدادات
الطريقة StreamConfig.GetEffectiveProtocol (config.go:58-64) تعود إلى "tcp" كقيمة افتراضية:
func (c *StreamConfig) GetEffectiveProtocol() string {
if c == nil || len(c.ProtocolName) == 0 {
return "tcp"
}
return c.ProtocolName
}يتم جلب الإعدادات الخاصة بالنقل عبر GetTransportSettingsFor (config.go:71-81)، التي تتكرر على قائمة TransportSettings بحثًا عن اسم البروتوكول المطابق. إذا لم يتم العثور على أي شيء، يتم إنشاء إعدادات افتراضية عبر ConfigCreator المسجل.
يتم حل إعدادات الأمان بالمثل عبر GetEffectiveSecuritySettings (config.go:83-90).
مخطط البنية
flowchart TD
subgraph "Application Layer"
OUT[Outbound Handler]
IN[Inbound Handler]
end
subgraph "Transport Dispatch"
DIAL["internet.Dial()"]
LISTEN["internet.ListenTCP()"]
MSC[MemoryStreamConfig]
end
subgraph "Registry (map[string]func)"
DR[transportDialerCache]
LR[transportListenerCache]
CR[globalTransportConfigCreatorCache]
end
subgraph "Transport Implementations"
TCP["tcp.Dial / tcp.ListenTCP"]
WS["websocket.Dial / websocket.ListenWS"]
GRPC["grpc.Dial / grpc.Listen"]
HU["httpupgrade.Dial / httpupgrade.ListenHTTPUpgrade"]
SH["splithttp.Dial / splithttp.ListenXH"]
KCP["kcp.DialKCP / kcp.ListenKCP"]
end
subgraph "Security Layer"
TLS["tls.Client / tls.Server"]
REALITY["reality.UClient / reality.Server"]
end
subgraph "System Layer"
SD["internet.DialSystem"]
SL["internet.ListenSystem"]
OS["OS net.Dialer / net.ListenConfig"]
end
OUT --> DIAL
IN --> LISTEN
DIAL --> MSC --> DR
LISTEN --> MSC --> LR
DR --> TCP & WS & GRPC & HU & SH & KCP
LR --> TCP & WS & GRPC & HU & SH & KCP
TCP --> TLS & REALITY
TCP --> SD
WS --> TLS
WS --> SD
GRPC --> TLS & REALITY
GRPC --> SD
HU --> TLS
HU --> SD
SH --> TLS & REALITY
SH --> SD
KCP --> TLS
KCP --> SL
SD --> OS
SL --> OSاستراتيجية النطاق وحل DNS
قبل إنشاء اتصالات على مستوى النظام، يتعامل DialSystem (dialer.go:227-283) مع حل DNS وفقًا لاستراتيجية DomainStrategy المُعدّة. جدول الاستراتيجيات (config.go:15-28) يتضمن 11 خيارًا:
| الاستراتيجية | السلوك | التفضيل | البديل |
|---|---|---|---|
| AsIs | بدون حل | - | - |
| UseIP | حل الاثنين | IPv4+IPv6 | لا يوجد |
| UseIPv4 | حل v4 | IPv4 | لا يوجد |
| UseIPv6 | حل v6 | IPv6 | لا يوجد |
| UseIPv4v6 | حل v4، بديل v6 | IPv4 | IPv6 |
| UseIPv6v4 | حل v6، بديل v4 | IPv6 | IPv4 |
| ForceIP/v4/v6/v4v6/v6v4 | نفس السابق لكن يفشل عند عدم وجود نتيجة | ... | ... |
عند تفعيل Happy Eyeballs وحل عناوين IP متعددة، يتم استدعاء TcpRaceDial لمحاولات الاتصال المتزامنة (انظر نقل TCP).
ملاحظات التنفيذ
- أمان الخيوط: خرائط التسجيل (
transportDialerCache،transportListenerCache،globalTransportConfigCreatorCache) تُكتب فقط أثناءinit()، لذا لا حاجة لقفل mutex للقراءة أثناء التشغيل. - StreamConfig فارغ: تمرير
nilإلىDialأوListenTCPينشئ تلقائيًا إعدادات TCP افتراضية. - DialerProxy: يدعم
DialSystem(dialer.go:271-280) التسلسل عبر معالج صادر آخر عبرsockopt.DialerProxy، مما يتيح طوبولوجيات بروكسي فوق بروكسي. - AddressPortStrategy: يمكن لـ
DialSystemتجاوز عنوان الوجهة/المنفذ باستخدام سجلات DNS من نوع SRV أو TXT (dialer.go:139-224). - استبدال SystemDialer: تسمح
UseAlternativeSystemDialer(system_dialer.go:232-237) باستبدال متصل النظام بالكامل، وهو ما يُستخدم على منصات مثل Android. - PacketConnWrapper: اتصالات UDP هي في الواقع
ListenPacket+WriteToمغلفة في واجهةnet.Conn(system_dialer.go:152-204).