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:
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:
// 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:
type Listener interface {
Close() error
Addr() net.Addr
}Each transport also registers a ListenFunc (tcp_hub.go:23):
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:
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):
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:
func init() {
common.Must(internet.RegisterTransportDialer(protocolName, Dial))
}Transport Listener Registry
Similarly, listeners register via 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 Creator Registry
Each transport also registers a config factory (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
}This is invoked from each transport's config.go init() function, e.g. transport/internet/tcp/config.go:8-12.
Registered Protocol Names
| Transport | Protocol Name | Dialer File | Listener File |
|---|---|---|---|
| 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) is the parsed, in-memory form of the protobuf StreamConfig. It avoids repeated protobuf deserialization at runtime:
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 (
SocketConfigprotobuf). - 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:
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):
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):
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":
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
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 --> OSDomainStrategy 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:
| Strategy | Behavior | Prefer | Fallback |
|---|---|---|---|
| AsIs | No resolution | - | - |
| UseIP | Resolve both | IPv4+IPv6 | None |
| UseIPv4 | Resolve v4 | IPv4 | None |
| UseIPv6 | Resolve v6 | IPv6 | None |
| UseIPv4v6 | Resolve v4, fallback v6 | IPv4 | IPv6 |
| UseIPv6v4 | Resolve v6, fallback v4 | IPv6 | IPv4 |
| ForceIP/v4/v6/v4v6/v6v4 | Same 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 duringinit(), so no mutex is needed for reads at runtime. - Nil StreamConfig: Passing
niltoDialorListenTCPautomatically creates a default TCP configuration. - DialerProxy:
DialSystem(dialer.go:271-280) supports chaining through another outbound handler viasockopt.DialerProxy, enabling proxy-over-proxy topologies. - AddressPortStrategy:
DialSystemcan 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+WriteTowrapped in anet.Conninterface (system_dialer.go:152-204).