Skip to content

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)

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

ModeBody Format
TCP (no Vision)Raw stream (no framing)
UDPLength-prefixed packets: [2B length BE][payload]
MuxStandard mux framing (see Mux)
VisionVision-wrapped stream (see XTLS Vision)
Mux + XUDPXUDP framing (see XUDP)

Request Encoding (encoding.go:30)

go
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)

go
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)

go
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)

mermaid
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)

go
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)

go
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:

go
// 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:

go
request.Command = Mux
request.Address = "v1.mux.cool"
request.Port = 666  // magic port indicating XUDP

See XUDP for the frame format.

Pre-Connect Pool

The outbound can maintain pre-established connections for latency reduction:

go
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):

go
// 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 Reverse outbound handler
  • Bridge workers multiplex traffic over the reverse connection
go
// Server creates reverse outbound on-demand
r := &Reverse{tag: a.Reverse.Tag, picker: picker, client: muxClient}
outboundManager.AddHandler(ctx, r)

Implementation Notes

  1. UUID is raw bytes: Not a string. The 16-byte UUID is sent directly, not as a hex or base64 string.

  2. Addons encoding: When Flow is empty, write a single 0x00 byte. When set, protobuf-encode the Addons message and prefix with its length (1 byte).

  3. 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.

  4. XUDP magic port: port == 666 with address v1.mux.cool indicates XUDP mode. The server must detect this to use XUDP framing.

  5. Vision is not portable: The unsafe.Pointer trick 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).

  6. Connection reuse: The pre-connect pool and mux allow connection reuse. Without mux, each TCP stream = one VLESS connection.

Technical analysis for re-implementation purposes.