Skip to content

Transport Architecture Overview ​

Introduction ​

Xray-core's transport layer provides a pluggable abstraction over network communication. Each transport protocol (TCP, WebSocket, gRPC, HTTPUpgrade, SplitHTTP, mKCP) registers itself into a global registry at startup, and the core dispatches dial/listen calls based on configuration. A parallel security layer (TLS, REALITY) wraps transports independently. This document covers the registry pattern, core interfaces, the MemoryStreamConfig configuration object, and how transports are selected at runtime.

Key Interfaces ​

Dialer Interface ​

The Dialer interface (transport/internet/dialer.go:22-31) is the high-level abstraction for outbound connections:

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

Internally, the transport layer uses a lower-level function signature for per-protocol dialers:

go
// dialer.go:34
type dialFunc func(ctx context.Context, dest net.Destination, streamSettings *MemoryStreamConfig) (stat.Connection, error)

Listener Interface ​

The Listener interface (transport/internet/tcp_hub.go:25-28) represents a server-side listener:

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

Each transport also registers a ListenFunc (tcp_hub.go:23):

go
type ListenFunc func(ctx context.Context, address net.Address, port net.Port,
    settings *MemoryStreamConfig, handler ConnHandler) (Listener, error)

Where ConnHandler is simply func(stat.Connection).

SystemDialer Interface ​

The SystemDialer (transport/internet/system_dialer.go:18-21) is the lowest-level abstraction that makes actual OS-level connections:

go
type SystemDialer interface {
    Dial(ctx context.Context, source net.Address, destination net.Destination,
        sockopt *SocketConfig) (net.Conn, error)
    DestIpAddress() net.IP
}

The default implementation (DefaultSystemDialer) calls Go's net.Dialer.DialContext with socket options applied via RawConn.Control.

Registry Pattern ​

Transport Dialer Registry ​

Each transport registers its dialer in an init() function using 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
}

Example from transport/internet/tcp/dialer.go:110-112:

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

Transport Listener Registry ​

Similarly, listeners register via 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 Creator Registry ​

Each transport also registers a config factory (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
}

This is invoked from each transport's config.go init() function, e.g. transport/internet/tcp/config.go:8-12.

Registered Protocol Names ​

TransportProtocol NameDialer FileListener File
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) is the parsed, in-memory form of the protobuf StreamConfig. It avoids repeated protobuf deserialization at runtime:

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
}

Key fields:

  • ProtocolName: e.g. "tcp", "websocket", "grpc" -- determines which registered dialer/listener is used.
  • ProtocolSettings: Transport-specific config (e.g., *tcp.Config, *websocket.Config). Cast via type assertion.
  • SecurityType / SecuritySettings: "tls" or "reality" with corresponding config object.
  • SocketSettings: Low-level socket options (SocketConfig protobuf).
  • DownloadSettings: Used by SplitHTTP for separate download stream configuration.
  • TcpmaskManager / UdpmaskManager: Packet obfuscation layer (finalmask).

The conversion function ToMemoryStreamConfig (memory_settings.go:22-78) parses the protobuf StreamConfig:

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

Transport Selection Flow ​

Outbound (Dial) ​

The main entry point is 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"]
        // ...
    }
}

When streamSettings is nil, ToMemoryStreamConfig(nil) produces a default config with ProtocolName = "tcp" (from config.go:58-64).

Inbound (Listen) ​

Listening goes through ListenTCP or 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
}

Config Resolution ​

The StreamConfig.GetEffectiveProtocol method (config.go:58-64) defaults to "tcp":

go
func (c *StreamConfig) GetEffectiveProtocol() string {
    if c == nil || len(c.ProtocolName) == 0 {
        return "tcp"
    }
    return c.ProtocolName
}

Transport-specific settings are fetched via GetTransportSettingsFor (config.go:71-81), which iterates the TransportSettings list looking for a matching protocol name. If none is found, it creates a default config via the registered ConfigCreator.

Security settings are resolved similarly via GetEffectiveSecuritySettings (config.go:83-90).

Architecture Diagram ​

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

DomainStrategy and DNS Resolution ​

Before making system-level connections, DialSystem (dialer.go:227-283) handles DNS resolution according to the configured DomainStrategy. The strategy table (config.go:15-28) encodes 11 options:

StrategyBehaviorPreferFallback
AsIsNo resolution--
UseIPResolve bothIPv4+IPv6None
UseIPv4Resolve v4IPv4None
UseIPv6Resolve v6IPv6None
UseIPv4v6Resolve v4, fallback v6IPv4IPv6
UseIPv6v4Resolve v6, fallback v4IPv6IPv4
ForceIP/v4/v6/v4v6/v6v4Same but fail on no result......

When Happy Eyeballs is enabled and multiple IPs are returned, TcpRaceDial is invoked for concurrent connection attempts (see TCP transport).

Implementation Notes ​

  • Thread safety: The registry maps (transportDialerCache, transportListenerCache, globalTransportConfigCreatorCache) are written only during init(), so no mutex is needed for reads at runtime.
  • Nil StreamConfig: Passing nil to Dial or ListenTCP automatically creates a default TCP configuration.
  • DialerProxy: DialSystem (dialer.go:271-280) supports chaining through another outbound handler via sockopt.DialerProxy, enabling proxy-over-proxy topologies.
  • AddressPortStrategy: DialSystem can override destination address/port using SRV or TXT DNS records (dialer.go:139-224).
  • SystemDialer replacement: UseAlternativeSystemDialer (system_dialer.go:232-237) allows the entire system dialer to be swapped, which is used on platforms like Android.
  • PacketConnWrapper: UDP "connections" are actually ListenPacket + WriteTo wrapped in a net.Conn interface (system_dialer.go:152-204).

Technical analysis for re-implementation purposes.