The Packet Journey
This page traces the complete lifecycle of a connection through Xray-core, from the moment a client connects to when data reaches the remote server.
Overview
flowchart TB
Client([Client App]) -->|"Connect to<br/>listen port"| Listener
subgraph Inbound["Inbound (app/proxyman/inbound)"]
Listener["internet.Listener<br/>(TCP Hub)"]
Worker["tcpWorker / udpWorker"]
Proxy["proxy.Inbound.Process()<br/>(VLESS/VMess/Trojan/...)"]
end
subgraph Core["Core Pipeline"]
Dispatcher["DefaultDispatcher.Dispatch()"]
Sniff["Sniffer<br/>(HTTP/TLS/QUIC/FakeDNS)"]
Router["Router.PickRoute()"]
end
subgraph Outbound["Outbound (app/proxyman/outbound)"]
OHandler["outbound.Handler.Dispatch()"]
Mux["Mux ClientManager<br/>(if mux enabled)"]
OProxy["proxy.Outbound.Process()<br/>(VLESS/Freedom/...)"]
Transport["internet.Dialer.Dial()<br/>(TCP/WS/gRPC/...)"]
end
Listener -->|stat.Connection| Worker
Worker -->|"build ctx + call"| Proxy
Proxy -->|"dispatcher.Dispatch(ctx, dest)"| Dispatcher
Dispatcher --> Sniff
Sniff --> Router
Router -->|outbound tag| OHandler
OHandler --> Mux
Mux --> OProxy
OProxy --> Transport
Transport -->|"encrypted conn"| Server([Remote/Target])Phase 1: Connection Acceptance
TCP Worker (app/proxyman/inbound/worker.go)
When a TCP connection arrives, the tcpWorker.callback() method fires:
func (w *tcpWorker) callback(conn stat.Connection) {
ctx, cancel := context.WithCancel(w.ctx)
sid := session.NewID()
ctx = c.ContextWithID(ctx, sid)
// Build outbound metadata
outbounds := []*session.Outbound{{}}
// For transparent proxy: get original destination
if w.recvOrigDest {
switch getTProxyType(w.stream) {
case internet.SocketConfig_Redirect:
dest, _ = tcp.GetOriginalDestination(conn)
case internet.SocketConfig_TProxy:
dest = net.DestinationFromAddr(conn.LocalAddr())
}
outbounds[0].Target = dest
}
ctx = session.ContextWithOutbounds(ctx, outbounds)
// Attach inbound metadata
ctx = session.ContextWithInbound(ctx, &session.Inbound{
Source: net.DestinationFromAddr(conn.RemoteAddr()),
Gateway: net.TCPDestination(w.address, w.port),
Tag: w.tag,
Conn: conn,
})
// Attach sniffing config
content := new(session.Content)
content.SniffingRequest = ... // from config
ctx = session.ContextWithContent(ctx, content)
// Hand off to protocol handler
w.proxy.Process(ctx, net.Network_TCP, conn, w.dispatcher)
}Key context values set here:
session.Inbound— source address, inbound tag, raw connectionsession.Outbound— target (populated for TProxy/redirect)session.Content— sniffing configuration
UDP Worker
For UDP, the udpWorker handles packets differently:
- Uses
udp.Dispatcherto manage UDP "connections" (keyed by source) - Each unique source gets a virtual connection dispatched through the proxy
- Timeout-based cleanup for idle UDP sessions
Phase 2: Protocol Processing (Inbound)
Each proxy protocol implements the proxy.Inbound interface:
type Inbound interface {
Network() []net.Network
Process(ctx context.Context, network net.Network,
conn stat.Connection, dispatcher routing.Dispatcher) error
}The protocol handler:
- Reads and decodes the protocol header from
conn - Extracts the target destination (address + port)
- Authenticates the user (if applicable)
- Calls
dispatcher.Dispatch(ctx, destination)to get a pipe pair - Copies data bidirectionally between
connand the pipe
Example: VLESS Inbound (simplified)
func (h *Handler) Process(ctx, network, connection, dispatch) error {
// Read first bytes
first := buf.FromBytes(make([]byte, buf.Size))
first.ReadFrom(connection)
// Decode VLESS header
userSentID, request, requestAddons, err :=
encoding.DecodeRequestHeader(first, reader, h.validator)
// Set user in context
ctx = session.ContextWithInbound(ctx, &session.Inbound{
User: user,
...
})
// Dispatch to routing
link, _ := dispatch.Dispatch(ctx, request.Destination())
// Bidirectional copy
// Upload: connection → link.Writer (to outbound)
// Download: link.Reader → connection (to client)
task.Run(ctx, requestDone, responseDone)
}Phase 3: Dispatching
The DefaultDispatcher.Dispatch() is the central hub (app/dispatcher/default.go):
func (d *DefaultDispatcher) Dispatch(ctx, destination) (*transport.Link, error) {
// Set target in outbound metadata
ob.OriginalTarget = destination
ob.Target = destination
// Create pipe pair
inbound, outbound := d.getLink(ctx)
if sniffingRequest.Enabled {
go func() {
// Wrap reader with caching
cReader := &cachedReader{reader: outbound.Reader}
outbound.Reader = cReader
// Sniff the first bytes
result, err := sniffer(ctx, cReader, ...)
// Override destination if sniffing matches
if d.shouldOverride(ctx, result, ...) {
destination.Address = net.ParseAddress(result.Domain())
ob.Target = destination // or ob.RouteTarget for RouteOnly
}
d.routedDispatch(ctx, outbound, destination)
}()
} else {
go d.routedDispatch(ctx, outbound, destination)
}
return inbound, nil // returned to the inbound proxy
}The Pipe Pair
getLink() creates two linked pipe pairs:
Client ←→ [InboundLink] ←→ Pipe ←→ [OutboundLink] ←→ Server
InboundLink: OutboundLink:
Reader = downlinkReader Reader = uplinkReader
Writer = uplinkWriter Writer = downlinkWriter
Client writes → uplinkWriter → uplinkReader → Server reads
Server writes → downlinkWriter → downlinkReader → Client readsIf stats are enabled, SizeStatWriter wrappers are inserted to count bytes.
Phase 4: Routing
routedDispatch() selects the outbound handler:
func (d *DefaultDispatcher) routedDispatch(ctx, link, destination) {
// 1. Check forced outbound tag (from platform/API)
if forcedTag := session.GetForcedOutboundTagFromContext(ctx); forcedTag != "" {
handler = d.ohm.GetHandler(forcedTag)
}
// 2. Ask router to pick route
else if route, err := d.router.PickRoute(routingCtx); err == nil {
handler = d.ohm.GetHandler(route.GetOutboundTag())
}
// 3. Fall back to default outbound
else {
handler = d.ohm.GetDefaultHandler()
}
// Dispatch to the selected outbound
handler.Dispatch(ctx, link)
}The router evaluates rules sequentially (see Routing Engine).
Phase 5: Outbound Processing
Outbound Handler (app/proxyman/outbound/handler.go)
The outbound handler wrapper:
func (h *Handler) Dispatch(ctx, link) {
// Check for mux
if h.mux != nil && shouldUseMux(ctx) {
h.mux.Dispatch(ctx, link)
return
}
// Direct proxy processing
h.proxy.Process(ctx, link, h) // h implements internet.Dialer
}Transport Dialing
When proxy.Process() calls dialer.Dial(ctx, dest):
- Look up stream settings for the outbound
- Select transport dialer (TCP/WS/gRPC/etc.)
- Dial the raw connection
- Apply security layer (TLS/REALITY/none)
- Return
stat.Connection
Outbound Proxy Processing
The outbound proxy encodes its protocol and copies data:
func (h *Handler) Process(ctx, link, dialer) error {
// Dial transport connection
conn, _ := dialer.Dial(ctx, serverAddress)
// Encode protocol header
encoding.EncodeRequestHeader(conn, request, addons)
// Bidirectional copy
// Upload: link.Reader → conn (to server)
// Download: conn → link.Writer (to client via pipe)
task.Run(ctx, postRequest, getResponse)
}Complete Sequence Diagram
sequenceDiagram
participant C as Client
participant TW as tcpWorker
participant PI as Proxy Inbound
participant D as Dispatcher
participant R as Router
participant PO as Proxy Outbound
participant T as Transport
participant S as Remote Server
C->>TW: TCP connect
TW->>TW: Build session context
TW->>PI: Process(ctx, conn, dispatcher)
PI->>PI: Decode protocol header
PI->>D: Dispatch(ctx, destination)
D->>D: Create pipe pair
D-->>PI: return inboundLink
Note over D: async goroutine:
D->>D: Sniff first bytes
D->>R: PickRoute(ctx)
R-->>D: outbound tag
D->>PO: handler.Dispatch(ctx, outboundLink)
PO->>T: dialer.Dial(ctx, server)
T->>S: Transport connect + TLS
T-->>PO: conn
PO->>S: Encode header + payload
par Upload (client → server)
PI->>D: pipe.Write (client data)
D->>PO: pipe.Read → conn.Write
and Download (server → client)
S->>PO: conn.Read
PO->>D: pipe.Write (server data)
D->>PI: pipe.Read → conn.Write
PI->>C: response data
endImplementation Notes
When reimplementing, the critical pieces are:
- Session context — carries all metadata; must be threaded through every call
- Pipe pair — the async bridge between inbound and outbound; needs backpressure
- Sniffing — must happen on the first bytes before routing; cache consumed bytes
- Bidirectional copy — two goroutines (upload + download) with shared cancellation
- Activity timer — resets on each data transfer; triggers close on idle timeout
The task.Run(ctx, postRequest, task.OnSuccess(getResponse, task.Close(writer))) pattern is used everywhere: run upload first, then on success start download, close writer when download ends.