Протокол 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)
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)
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)
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)
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)
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)
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)
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 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-фреймы с поадресной адресацией пакетов. Исходящий обработчик устанавливает:
request.Command = Mux
request.Address = "v1.mux.cool"
request.Port = 666 // магический порт, указывающий на XUDPФормат фрейма см. в XUDP.
Пул предподключений
Исходящий обработчик может поддерживать заранее установленные соединения для снижения задержки:
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):
// Серверная сторона (входящий)
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 - Рабочие процессы мультиплексируют трафик через обратное соединение
// Сервер создаёт обратный исходящий обработчик по требованию
r := &Reverse{tag: a.Reverse.Tag, picker: picker, client: muxClient}
outboundManager.AddHandler(ctx, r)Примечания по реализации
UUID передаётся в необработанном виде: Не в виде строки. 16-байтный UUID отправляется напрямую, а не как hex- или base64-строка.
Кодирование дополнений: Когда Flow пуст, записывается один байт
0x00. Когда задан — сообщение Addons кодируется в protobuf с префиксом длины (1 байт).Fallback на уровне соединения: Если UUID не совпадает, всё соединение (включая уже прочитанные байты) перенаправляется на fallback-назначение. Заголовок PROXY-протокола (PROXY v1/v2) добавляется при соответствующей настройке.
Магический порт XUDP:
port == 666с адресомv1.mux.coolуказывает на режим XUDP. Сервер должен определить это для использования XUDP-фрейминга.Vision непортируем: Приём с
unsafe.Pointerдля чтения внутреннего состояния TLS работает только с определёнными реализациями Go TLS. Для других языков потребуются альтернативные подходы (например, пользовательская реализация TLS с открытым состоянием).Повторное использование соединений: Пул предподключений и mux позволяют повторно использовать соединения. Без mux каждый TCP-поток = одно VLESS-соединение.