VLESS Protocol
VLESS is Xray-core's flagship protocol — a lightweight, extensible proxy protocol with no built-in encryption (relies on transport-layer security). It supports TCP, UDP, multiplexing, XTLS Vision (direct TLS passthrough), XUDP (per-packet UDP addressing), and reverse proxy.
Source: proxy/vless/, proxy/vless/encoding/, proxy/vless/inbound/, proxy/vless/outbound/
Wire Format
Request Header
+----------+------+----------+-----------+---------+------+----------+
| Version | UUID | Addon | Command | Port | Addr | Addr |
| (1 byte) | (16) | Len+Data | (1 byte) | (2 BE) | Type | Value |
+----------+------+----------+-----------+---------+------+----------+
Version: 0x00 (always 0)
UUID: 16 bytes (user identity, raw bytes of UUID)
Addon: 1 byte length + protobuf Addons message (or 0x00 for no addons)
Command: 0x01=TCP, 0x02=UDP, 0x03=Mux, 0x04=Reverse
Port: 2 bytes big-endian (omitted for Mux/Reverse)
AddrType: 0x01=IPv4, 0x02=Domain, 0x03=IPv6
Address: 4 bytes (IPv4), 1+N bytes (domain length+domain), 16 bytes (IPv6)Response Header
+----------+----------+
| Version | Addon |
| (1 byte) | Len+Data |
+----------+----------+
Version: echoes request version (0x00)
Addon: 1 byte length + protobuf (or 0x00)Addons (Protobuf)
message Addons {
string Flow = 1; // e.g., "xtls-rprx-vision"
bytes Seed = 2; // padding seed
}When Flow is empty/none, addon length is 0 (single zero byte). When Flow is set (e.g., Vision), the addons are protobuf-encoded.
Body Encoding
| Mode | Body Format |
|---|---|
| TCP (no Vision) | Raw stream (no framing) |
| UDP | Length-prefixed packets: [2B length BE][payload] |
| Mux | Standard mux framing (see Mux) |
| Vision | Vision-wrapped stream (see XTLS Vision) |
| Mux + XUDP | XUDP framing (see XUDP) |
Request Encoding (encoding.go:30)
func EncodeRequestHeader(writer, request, requestAddons) error {
buffer.WriteByte(request.Version) // 1 byte: version
buffer.Write(account.ID.Bytes()) // 16 bytes: UUID
EncodeHeaderAddons(&buffer, requestAddons) // addons
buffer.WriteByte(byte(request.Command)) // 1 byte: command
if command != Mux && command != Rvs {
addrParser.WriteAddressPort(&buffer, addr, port) // address
}
writer.Write(buffer.Bytes())
}Request Decoding (encoding.go:64)
func DecodeRequestHeader(isfb, first, reader, validator) (userSentID, request, addons, isFallback, error) {
// Read version (1 byte)
// Read UUID (16 bytes)
// Validate user: validator.Get(id)
// If user not found and fallback enabled: return isFallback=true
// Read addons (protobuf)
// Read command (1 byte)
// Read address based on command type
}The isfb parameter controls whether fallback is enabled. If the UUID doesn't match any user AND fallback is configured, the connection is handed off to a fallback server.
Inbound Handler
Handler Structure (inbound/inbound.go:74)
type Handler struct {
inboundHandlerManager feature_inbound.Manager
policyManager policy.Manager
stats stats.Manager
validator vless.Validator // UUID→user mapping
decryption *encryption.ServerInstance // ML-KEM-768 (optional)
outboundHandlerManager outbound.Manager
defaultDispatcher routing.Dispatcher
fallbacks map[string]map[string]map[string]*Fallback // name→alpn→path
}Process Flow (inbound/inbound.go:267)
flowchart TB
Conn([Connection]) --> Decrypt{ML-KEM-768<br/>decryption?}
Decrypt -->|Yes| Handshake["Post-quantum handshake"]
Decrypt -->|No| ReadFirst
Handshake --> ReadFirst["Read first bytes"]
ReadFirst --> Decode["DecodeRequestHeader()"]
Decode --> Valid{Valid UUID?}
Valid -->|Yes| SetUser["Set user in context"]
Valid -->|No, fallback on| Fallback["Fallback handler"]
Valid -->|No, fallback off| Reject["Reject connection"]
SetUser --> CheckMux{Is Mux?}
CheckMux -->|"Mux + XUDP"| XUDP["XUDP: dispatch<br/>each UDP packet"]
CheckMux -->|"Mux (not XUDP)"| Mux["Mux: dispatch<br/>multiplexed streams"]
CheckMux -->|"TCP/UDP"| Dispatch["dispatcher.Dispatch(ctx, dest)"]
Dispatch --> Copy["Bidirectional copy:<br/>client ↔ pipe"]
Fallback --> DetectSNI["Get TLS SNI + ALPN"]
DetectSNI --> LookupFB["Lookup fallback:<br/>name → alpn → path"]
LookupFB --> ProxyFB["Proxy to fallback<br/>dest (with first bytes)"]Mux vs XUDP Detection (inbound/inbound.go:176)
func isMuxAndNotXUDP(request, first) bool {
if request.Command != protocol.RequestCommandMux {
return false
}
if first.Len() < 7 {
return true // not enough data, assume regular mux
}
firstBytes := first.Bytes()
// XUDP: session ID = 0, network type = UDP (2)
return !(firstBytes[2] == 0 && // ID high
firstBytes[3] == 0 && // ID low
firstBytes[6] == 2) // Network type: UDP
}XUDP is detected by checking if the first mux frame has session ID 0 and network type UDP.
Fallback System
The fallback system is a multi-level routing mechanism for connections that don't match VLESS:
fallbacks[name][alpn][path] → Fallback{Dest, Xver}Level 1 — Server Name (SNI):
- From TLS/REALITY connection state
- Allows multiple domains on the same port
Level 2 — ALPN:
- From TLS negotiated protocol (
h2,http/1.1) - Routes HTTP/2 vs HTTP/1.1 differently
Level 3 — HTTP Path:
- Parsed from the first HTTP request line
- Routes different paths to different backends
Fallback destinations can be:
- TCP address (
127.0.0.1:80) - Unix socket (
/dev/shm/nginx.sock) - Abstract socket (
@name)
PROXY protocol headers (v1/v2) can be prepended via Xver setting.
Outbound Handler
Process Flow (outbound/outbound.go:136)
func (h *Handler) Process(ctx, link, dialer) error {
// 1. Dial transport connection (with optional pre-connect pool)
conn, _ := dialer.Dial(ctx, rec.Destination)
// 2. Determine command
if target.Network == UDP { command = UDP }
if target.Address == "v1.mux.cool" { command = Mux }
// 3. Handle XUDP: for UDP with cone NAT, convert to mux
if command == UDP && (flow == XRV || cone) {
command = Mux
address = "v1.mux.cool"
port = 666
}
// 4. Encode request header
EncodeRequestHeader(conn, request, requestAddons)
// 5. Create body writer (may be Vision or XUDP)
serverWriter = EncodeBodyAddons(conn, request, requestAddons, ...)
if command == Mux && port == 666 {
serverWriter = xudp.NewPacketWriter(serverWriter, target, globalID)
}
// 6. Upload: link.Reader → serverWriter
// 7. Download: serverReader → link.Writer
task.Run(ctx, postRequest, getResponse)
}XTLS Vision Detection (outbound)
For Vision flow, the outbound accesses TLS internal state via unsafe.Pointer:
// Access Go TLS internal fields
t = reflect.TypeOf(tlsConn.Conn).Elem()
p = uintptr(unsafe.Pointer(tlsConn.Conn))
i, _ := t.FieldByName("input") // *bytes.Reader
r, _ := t.FieldByName("rawInput") // *bytes.Buffer
input = (*bytes.Reader)(unsafe.Pointer(p + i.Offset))
rawInput = (*bytes.Buffer)(unsafe.Pointer(p + r.Offset))These internal TLS buffers reveal when the inner TLS handshake is complete, allowing Vision to switch to direct passthrough. See XTLS Vision for details.
UDP Handling
Non-XUDP (simple length-prefixed)
For direct UDP without cone NAT:
[2B length BE][UDP payload]
[2B length BE][UDP payload]
...Written by MultiLengthPacketWriter, read by LengthPacketReader.
XUDP (cone NAT)
For UDP with cone NAT support, UDP is wrapped in mux frames with per-packet addressing. The outbound sets:
request.Command = Mux
request.Address = "v1.mux.cool"
request.Port = 666 // magic port indicating XUDPSee XUDP for the frame format.
Pre-Connect Pool
The outbound can maintain pre-established connections for latency reduction:
if h.testpre > 0 {
// Launch N goroutines that continuously dial and buffer connections
// Connections expire after 2 minutes
h.preConns = make(chan *ConnExpire)
for range h.testpre {
go func() {
for {
conn := dialer.Dial(ctx, dest)
h.preConns <- &ConnExpire{Conn: conn, Expire: time.Now().Add(2*time.Minute)}
time.Sleep(200ms)
}
}()
}
}ML-KEM-768 Post-Quantum Encryption
VLESS supports optional post-quantum encryption via ML-KEM-768 (formerly Kyber):
// Server side (inbound)
if h.decryption != nil {
connection, err = h.decryption.Handshake(connection, nil)
}
// Client side (outbound)
if h.encryption != nil {
conn, err = h.encryption.Handshake(conn)
}This adds a key encapsulation layer before the VLESS protocol, providing quantum-resistant security independent of the transport layer.
Reverse Proxy
VLESS supports bidirectional reverse proxy via mux:
- Client (outbound): Establishes mux connection to server with command
Rvs(0x04) - Server (inbound): Detects reverse user, creates
Reverseoutbound handler - Bridge workers multiplex traffic over the reverse connection
// Server creates reverse outbound on-demand
r := &Reverse{tag: a.Reverse.Tag, picker: picker, client: muxClient}
outboundManager.AddHandler(ctx, r)Implementation Notes
UUID is raw bytes: Not a string. The 16-byte UUID is sent directly, not as a hex or base64 string.
Addons encoding: When Flow is empty, write a single
0x00byte. When set, protobuf-encode the Addons message and prefix with its length (1 byte).Fallback is connection-level: If UUID doesn't match, the entire connection (including already-read bytes) is forwarded to the fallback destination. The proxy protocol header (PROXY v1/v2) is prepended if configured.
XUDP magic port:
port == 666with addressv1.mux.coolindicates XUDP mode. The server must detect this to use XUDP framing.Vision is not portable: The
unsafe.Pointertrick to read TLS internal state only works with specific Go TLS implementations. Other languages would need alternative approaches (e.g., custom TLS implementation with exposed state).Connection reuse: The pre-connect pool and mux allow connection reuse. Without mux, each TCP stream = one VLESS connection.