Skip to content

Протокол VLESS

VLESS — флагманский протокол Xray-core, лёгкий и расширяемый прокси-протокол без встроенного шифрования (полагается на безопасность транспортного уровня). Он поддерживает TCP, UDP, мультиплексирование, XTLS Vision (прямое проксирование TLS), XUDP (поадресная адресация UDP-пакетов) и обратный прокси.

Исходный код: proxy/vless/, proxy/vless/encoding/, proxy/vless/inbound/, proxy/vless/outbound/

Формат передачи данных

Заголовок запроса

+----------+------+----------+-----------+---------+------+----------+
| Version  | UUID | Addon    | Command   | Port    | Addr | Addr     |
| (1 byte) | (16) | Len+Data | (1 byte)  | (2 BE)  | Type | Value    |
+----------+------+----------+-----------+---------+------+----------+

Version: 0x00 (всегда 0)
UUID:    16 байт (идентификатор пользователя, необработанные байты UUID)
Addon:   1 байт длины + protobuf-сообщение Addons (или 0x00 при отсутствии дополнений)
Command: 0x01=TCP, 0x02=UDP, 0x03=Mux, 0x04=Reverse
Port:    2 байта big-endian (не указывается для Mux/Reverse)
AddrType: 0x01=IPv4, 0x02=Домен, 0x03=IPv6
Address: 4 байта (IPv4), 1+N байт (длина домена + домен), 16 байт (IPv6)

Заголовок ответа

+----------+----------+
| Version  | Addon    |
| (1 byte) | Len+Data |
+----------+----------+

Version: повторяет версию запроса (0x00)
Addon:   1 байт длины + protobuf (или 0x00)

Дополнения (Protobuf)

protobuf
message Addons {
    string Flow = 1;  // e.g., "xtls-rprx-vision"
    bytes  Seed = 2;  // padding seed
}

Когда Flow пуст или не задан, длина дополнений равна 0 (один нулевой байт). Когда Flow задан (например, Vision), дополнения кодируются в protobuf.

Кодирование тела

РежимФормат тела
TCP (без Vision)Сырой поток (без фрейминга)
UDPПакеты с префиксом длины: [2B length BE][payload]
MuxСтандартный mux-фрейминг (см. Mux)
VisionПоток, обёрнутый Vision (см. XTLS Vision)
Mux + XUDPФрейминг XUDP (см. XUDP)

Кодирование запроса (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())
}

Декодирование запроса (encoding.go:64)

go
func DecodeRequestHeader(isfb, first, reader, validator) (userSentID, request, addons, isFallback, error) {
    // Чтение версии (1 байт)
    // Чтение UUID (16 байт)
    // Валидация пользователя: validator.Get(id)
    // Если пользователь не найден и fallback включён: возврат isFallback=true
    // Чтение дополнений (protobuf)
    // Чтение команды (1 байт)
    // Чтение адреса в зависимости от типа команды
}

Параметр isfb определяет, включён ли механизм fallback. Если UUID не совпадает ни с одним пользователем И fallback настроен, соединение передаётся на резервный сервер.

Входящий обработчик

Структура обработчика (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
}

Поток обработки (inbound/inbound.go:267)

mermaid
flowchart TB
    Conn([Соединение]) --> Decrypt{ML-KEM-768<br/>расшифровка?}
    Decrypt -->|Да| Handshake["Постквантовое рукопожатие"]
    Decrypt -->|Нет| ReadFirst
    Handshake --> ReadFirst["Чтение первых байт"]
    ReadFirst --> Decode["DecodeRequestHeader()"]
    Decode --> Valid{Валидный UUID?}

    Valid -->|Да| SetUser["Установка пользователя в контексте"]
    Valid -->|Нет, fallback вкл.| Fallback["Обработчик fallback"]
    Valid -->|Нет, fallback выкл.| Reject["Отклонение соединения"]

    SetUser --> CheckMux{Это Mux?}
    CheckMux -->|"Mux + XUDP"| XUDP["XUDP: диспетчеризация<br/>каждого UDP-пакета"]
    CheckMux -->|"Mux (не XUDP)"| Mux["Mux: диспетчеризация<br/>мультиплексированных потоков"]
    CheckMux -->|"TCP/UDP"| Dispatch["dispatcher.Dispatch(ctx, dest)"]

    Dispatch --> Copy["Двунаправленное копирование:<br/>клиент <-> pipe"]

    Fallback --> DetectSNI["Получение TLS SNI + ALPN"]
    DetectSNI --> LookupFB["Поиск fallback:<br/>name -> alpn -> path"]
    LookupFB --> ProxyFB["Проксирование на fallback<br/>назначение (с первыми байтами)"]

Определение Mux vs XUDP (inbound/inbound.go:176)

go
func isMuxAndNotXUDP(request, first) bool {
    if request.Command != protocol.RequestCommandMux {
        return false
    }
    if first.Len() < 7 {
        return true  // недостаточно данных, предполагаем обычный 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 определяется путём проверки, имеет ли первый mux-фрейм ID сессии 0 и тип сети UDP.

Система Fallback

Система fallback — это многоуровневый механизм маршрутизации для соединений, не соответствующих VLESS:

fallbacks[name][alpn][path] -> Fallback{Dest, Xver}

Уровень 1 — Имя сервера (SNI):

  • Из состояния соединения TLS/REALITY
  • Позволяет использовать несколько доменов на одном порту

Уровень 2 — ALPN:

  • Из согласованного протокола TLS (h2, http/1.1)
  • Маршрутизирует HTTP/2 и HTTP/1.1 по-разному

Уровень 3 — HTTP-путь:

  • Извлекается из первой строки HTTP-запроса
  • Направляет разные пути к разным бэкендам

Назначения fallback могут быть:

  • TCP-адрес (127.0.0.1:80)
  • Unix-сокет (/dev/shm/nginx.sock)
  • Абстрактный сокет (@name)

Заголовки PROXY-протокола (v1/v2) могут быть добавлены через настройку Xver.

Исходящий обработчик

Поток обработки (outbound/outbound.go:136)

go
func (h *Handler) Process(ctx, link, dialer) error {
    // 1. Установка транспортного соединения (с опциональным пулом предподключений)
    conn, _ := dialer.Dial(ctx, rec.Destination)

    // 2. Определение команды
    if target.Network == UDP { command = UDP }
    if target.Address == "v1.mux.cool" { command = Mux }

    // 3. Обработка XUDP: для UDP с cone NAT, преобразование в mux
    if command == UDP && (flow == XRV || cone) {
        command = Mux
        address = "v1.mux.cool"
        port = 666
    }

    // 4. Кодирование заголовка запроса
    EncodeRequestHeader(conn, request, requestAddons)

    // 5. Создание записывателя тела (может быть Vision или XUDP)
    serverWriter = EncodeBodyAddons(conn, request, requestAddons, ...)
    if command == Mux && port == 666 {
        serverWriter = xudp.NewPacketWriter(serverWriter, target, globalID)
    }

    // 6. Загрузка: link.Reader -> serverWriter
    // 7. Скачивание: serverReader -> link.Writer
    task.Run(ctx, postRequest, getResponse)
}

Обнаружение XTLS Vision (исходящий)

Для потока Vision исходящий обработчик обращается к внутреннему состоянию TLS через unsafe.Pointer:

go
// Доступ к внутренним полям Go TLS
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))

Эти внутренние TLS-буферы позволяют определить, когда внутреннее TLS-рукопожатие завершено, что позволяет Vision переключиться на прямое проксирование. Подробности см. в XTLS Vision.

Обработка UDP

Без XUDP (простой формат с префиксом длины)

Для прямого UDP без cone NAT:

[2B length BE][UDP payload]
[2B length BE][UDP payload]
...

Запись осуществляется MultiLengthPacketWriter, чтение — LengthPacketReader.

XUDP (cone NAT)

Для UDP с поддержкой cone NAT UDP оборачивается в mux-фреймы с поадресной адресацией пакетов. Исходящий обработчик устанавливает:

go
request.Command = Mux
request.Address = "v1.mux.cool"
request.Port = 666  // магический порт, указывающий на XUDP

Формат фрейма см. в XUDP.

Пул предподключений

Исходящий обработчик может поддерживать заранее установленные соединения для снижения задержки:

go
if h.testpre > 0 {
    // Запуск N горутин, которые непрерывно устанавливают и буферизуют соединения
    // Соединения истекают через 2 минуты
    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

VLESS поддерживает опциональное постквантовое шифрование через ML-KEM-768 (ранее Kyber):

go
// Серверная сторона (входящий)
if h.decryption != nil {
    connection, err = h.decryption.Handshake(connection, nil)
}

// Клиентская сторона (исходящий)
if h.encryption != nil {
    conn, err = h.encryption.Handshake(conn)
}

Это добавляет уровень инкапсуляции ключей перед протоколом VLESS, обеспечивая квантово-устойчивую безопасность независимо от транспортного уровня.

Обратный прокси

VLESS поддерживает двунаправленный обратный прокси через mux:

  • Клиент (исходящий): Устанавливает mux-соединение с сервером с командой Rvs (0x04)
  • Сервер (входящий): Обнаруживает обратного пользователя, создаёт исходящий обработчик Reverse
  • Рабочие процессы мультиплексируют трафик через обратное соединение
go
// Сервер создаёт обратный исходящий обработчик по требованию
r := &Reverse{tag: a.Reverse.Tag, picker: picker, client: muxClient}
outboundManager.AddHandler(ctx, r)

Примечания по реализации

  1. UUID передаётся в необработанном виде: Не в виде строки. 16-байтный UUID отправляется напрямую, а не как hex- или base64-строка.

  2. Кодирование дополнений: Когда Flow пуст, записывается один байт 0x00. Когда задан — сообщение Addons кодируется в protobuf с префиксом длины (1 байт).

  3. Fallback на уровне соединения: Если UUID не совпадает, всё соединение (включая уже прочитанные байты) перенаправляется на fallback-назначение. Заголовок PROXY-протокола (PROXY v1/v2) добавляется при соответствующей настройке.

  4. Магический порт XUDP: port == 666 с адресом v1.mux.cool указывает на режим XUDP. Сервер должен определить это для использования XUDP-фрейминга.

  5. Vision непортируем: Приём с unsafe.Pointer для чтения внутреннего состояния TLS работает только с определёнными реализациями Go TLS. Для других языков потребуются альтернативные подходы (например, пользовательская реализация TLS с открытым состоянием).

  6. Повторное использование соединений: Пул предподключений и mux позволяют повторно использовать соединения. Без mux каждый TCP-поток = одно VLESS-соединение.

Технический анализ для целей повторной реализации.