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.