Skip to content

نظرة عامة على بنية طبقة النقل

مقدمة

توفر طبقة النقل في Xray-core تجريدًا قابلاً للتوصيل فوق الاتصالات الشبكية. يقوم كل بروتوكول نقل (TCP، WebSocket، gRPC، HTTPUpgrade، SplitHTTP، mKCP) بتسجيل نفسه في سجل عام عند بدء التشغيل، ويقوم النواة بتوجيه استدعاءات الاتصال والاستماع بناءً على الإعدادات. تعمل طبقة أمان موازية (TLS، REALITY) على تغليف وسائل النقل بشكل مستقل. يغطي هذا المستند نمط التسجيل، والواجهات الأساسية، وكائن الإعدادات MemoryStreamConfig، وكيفية اختيار وسائل النقل أثناء التشغيل.

الواجهات الرئيسية

واجهة Dialer

واجهة Dialer (transport/internet/dialer.go:22-31) هي التجريد عالي المستوى للاتصالات الصادرة:

go
type Dialer interface {
    Dial(ctx context.Context, destination net.Destination) (stat.Connection, error)
    DestIpAddress() net.IP
    SetOutboundGateway(ctx context.Context, ob *session.Outbound)
}

داخليًا، تستخدم طبقة النقل توقيع دالة منخفض المستوى لمتصلي كل بروتوكول:

go
// 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) تمثل مستمعًا من جانب الخادم:

go
type Listener interface {
    Close() error
    Addr() net.Addr
}

يقوم كل بروتوكول نقل أيضًا بتسجيل ListenFunc (tcp_hub.go:23):

go
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) هو التجريد الأدنى مستوى الذي ينشئ اتصالات فعلية على مستوى نظام التشغيل:

go
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):

go
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:

go
func init() {
    common.Must(internet.RegisterTransportDialer(protocolName, Dial))
}

سجل مستمعي النقل

بالمثل، يتم تسجيل المستمعين عبر RegisterTransportListener (tcp_hub.go:13-19):

go
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):

go
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.gotcp/hub.go
WebSocket"websocket"websocket/dialer.gowebsocket/hub.go
gRPC"grpc"grpc/dial.gogrpc/hub.go
HTTPUpgrade"httpupgrade"httpupgrade/dialer.gohttpupgrade/hub.go
SplitHTTP"splithttp"splithttp/dialer.gosplithttp/hub.go
mKCP"mkcp"kcp/dialer.gokcp/listener.go

MemoryStreamConfig

MemoryStreamConfig (transport/internet/memory_settings.go:9-19) هو الشكل المحلل في الذاكرة لرسالة protobuf StreamConfig. يتجنب إلغاء تسلسل protobuf المتكرر أثناء التشغيل:

go
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:

go
func ToMemoryStreamConfig(s *StreamConfig) (*MemoryStreamConfig, error) {
    ets, err := s.GetEffectiveTransportSettings()
    // ... builds MemoryStreamConfig from StreamConfig fields
}

تدفق اختيار النقل

الصادر (Dial)

نقطة الدخول الرئيسية هي internet.Dial (dialer.go:48-75):

go
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):

go
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" كقيمة افتراضية:

go
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).

مخطط البنية

mermaid
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حل v4IPv4لا يوجد
UseIPv6حل v6IPv6لا يوجد
UseIPv4v6حل v4، بديل v6IPv4IPv6
UseIPv6v4حل v6، بديل v4IPv6IPv4
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).

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