Skip to content

Протокол VMess

VMess — оригинальный зашифрованный прокси-протокол V2Ray/Xray. Он обеспечивает аутентифицированное шифрование с несколькими вариантами шифров, защиту от повторного воспроизведения на основе времени и опциональное дополнение/маскирование для противодействия анализу трафика.

Обзор

  • Направление: Входящий + Исходящий
  • Транспорт: TCP, UNIX-сокет
  • Шифрование: AES-128-GCM, ChaCha20-Poly1305, None
  • Аутентификация: На основе UUID с AEAD-шифрованием заголовка
  • Mux: Поддерживается через виртуальный домен v1.mux.cool и XUDP

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

Запрос (от клиента к серверу)

AEAD-запрос VMess состоит из двух уровней: внешняя AEAD-зашифрованная оболочка заголовка и внутренний командный заголовок.

Внешняя оболочка (AEAD-заголовок)

+----------+---------------------------+--------------+---------------------------+
| Auth ID  | Encrypted Payload Length  | Conn Nonce   | Encrypted Payload         |
| 16 bytes | 2 + 16 bytes (GCM tag)    | 8 bytes      | variable + 16 bytes (tag) |
+----------+---------------------------+--------------+---------------------------+

Исходный код: proxy/vmess/aead/encrypt.go:14-61

Auth ID (16 байт): Блок, зашифрованный AES-ECB, содержащий:

+---------------+-----------+----------+
| Timestamp     | Random    | CRC32    |
| 8 bytes (BE)  | 4 bytes   | 4 bytes  |
+---------------+-----------+----------+

Ключ AES для шифрования Auth ID вычисляется следующим образом:

go
aesKey = KDF16(cmdKey, "AES Auth ID Encryption")

Исходный код: proxy/vmess/aead/authid.go:26-40

Валидация времени: Сервер расшифровывает Auth ID для каждого известного пользователя, проверяет CRC32 и убеждается, что временная метка находится в пределах 120 секунд от серверного времени.

Исходный код: proxy/vmess/aead/authid.go:99-121

Шифрование длины полезной нагрузки: AES-128-GCM с ключом/nonce, полученными из:

go
key   = KDF16(cmdKey, "VMess Header AEAD Key_Length", authID, connNonce)
nonce = KDF(cmdKey, "VMess Header AEAD Nonce_Length", authID, connNonce)[:12]
// Additional data = authID

Шифрование полезной нагрузки: AES-128-GCM с ключом/nonce, полученными из:

go
key   = KDF16(cmdKey, "VMess Header AEAD Key", authID, connNonce)
nonce = KDF(cmdKey, "VMess Header AEAD Nonce", authID, connNonce)[:12]
// Additional data = authID

Исходный код: proxy/vmess/aead/encrypt.go:30-51

Внутренний командный заголовок (расшифрованная полезная нагрузка)

+-----+--------+--------+---------+--------+----------+-----+---------+---------+-------+
| Ver | BodyIV | BodyKey | RespHdr | Option | Security | Rsv | Command | Address | Pad   | FNV1a |
| 1B  | 16B    | 16B    | 1B      | 1B     | 1B       | 1B  | 1B      | var     | 0-15B | 4B    |
+-----+--------+--------+---------+--------+----------+-----+---------+---------+-------+

Исходный код: proxy/vmess/encoding/client.go:63-101

ПолеРазмерОписание
Version1 байтВсегда 0x01
Body IV16 байтСлучайный IV для шифрования тела
Body Key16 байтСлучайный ключ для шифрования тела
Response Header1 байтСлучайный байт, сервер должен отправить его обратно
Option1 байтБитовая маска: ChunkStream(0x01), ChunkMasking(0x04), GlobalPadding(0x08), AuthenticatedLength(0x10)
Security1 байтВерхние 4 бита = длина дополнения (0-15), нижние 4 бита = тип шифра
Reserved1 байтВсегда 0x00
Command1 байт0x01=TCP, 0x02=UDP, 0x03=Mux
AddressпеременныйPort(2B, BE) + AddrType(1B) + Address
Padding0-15 байтСлучайное дополнение
FNV1a4 байтаХеш FNV-1a всех предшествующих полей (целостность)

Типы шифрования (нижние 4 бита):

ЗначениеШифр
0x00AUTO / UNKNOWN
0x03AES-128-GCM
0x04ChaCha20-Poly1305
0x05None

Исходный код: proxy/vmess/encoding/server.go:114-124

Ответ (от сервера к клиенту)

Заголовок ответа также зашифрован AEAD:

+----------------------------+----------------------------+
| Encrypted Length (2+16B)   | Encrypted Header (var+16B) |
+----------------------------+----------------------------+

Ключи вычисляются из ключа/IV тела ответа:

go
responseBodyKey = SHA256(requestBodyKey)[:16]
responseBodyIV  = SHA256(requestBodyIV)[:16]

lengthKey   = KDF16(responseBodyKey, "AEAD Resp Header Len Key")
lengthNonce = KDF(responseBodyIV, "AEAD Resp Header Len IV")[:12]
payloadKey  = KDF16(responseBodyKey, "AEAD Resp Header Key")
payloadNonce = KDF(responseBodyIV, "AEAD Resp Header IV")[:12]

Исходный код: proxy/vmess/encoding/client.go:179-253, proxy/vmess/encoding/server.go:328-369

Расшифрованный заголовок ответа:

+----------+--------+---------+---------+
| RespHdr  | Option | CmdID   | CmdLen  | CmdData |
| 1B       | 1B     | 1B      | 1B      | var     |
+----------+--------+---------+---------+

Байт RespHdr должен совпадать со случайным байтом из запроса.

Шифрование тела

Данные тела передаются в виде аутентифицированных блоков:

mermaid
graph LR
    subgraph "Каждый блок"
        A[Длина 2B] --> B[Полезная нагрузка] --> C[Тег аутентификации]
    end

Кодирование размера блока (с маскированием): Когда включён ChunkMasking, поток SHAKE128 XOR'ит 2-байтовое поле длины:

go
// ShakeSizeParser
mask = SHAKE128(bodyIV).next_2_bytes()
wire_length = actual_length XOR mask

Исходный код: proxy/vmess/encoding/auth.go:51-87

Генерация nonce для AEAD тела:

go
func GenerateChunkNonce(nonce []byte, size uint32) BytesGenerator {
    c := copy(nonce)
    count := uint16(0)
    return func() []byte {
        binary.BigEndian.PutUint16(c, count)
        count++
        return c[:size]
    }
}

Nonce — это IV тела с первыми 2 байтами, заменёнными инкрементируемым счётчиком.

Исходный код: proxy/vmess/encoding/client.go:332-340

Получение ключа ChaCha20-Poly1305: 16-байтный ключ тела расширяется до 32 байт:

go
func GenerateChacha20Poly1305Key(b []byte) []byte {
    key := make([]byte, 32)
    t := md5.Sum(b)
    copy(key, t[:])
    t = md5.Sum(key[:16])
    copy(key[16:], t[:])
    return key
}

Исходный код: proxy/vmess/encoding/auth.go:42-49

Сигнал завершения: Когда NoTerminationSignal не установлен, пустой блок (длина=0) сигнализирует об окончании потока.

Исходный код: proxy/vmess/outbound/outbound.go:185-189

KDF (функция получения ключей)

VMess использует вложённую KDF на основе HMAC-SHA256:

go
func KDF(key []byte, path ...string) []byte {
    hmacf := hmac.New(sha256.New, []byte("VMess AEAD KDF"))
    for _, v := range path {
        hmacf = hmac.New(func() hash.Hash {
            // uses previous hmac as inner hash
            return hmacf
        }, []byte(v))
    }
    hmacf.Write(key)
    return hmacf.Sum(nil)
}

Исходный код: proxy/vmess/aead/kdf.go:13-28

Константы солей KDF:

КонстантаЗначение
KDFSaltConstVMessAEADKDF"VMess AEAD KDF"
KDFSaltConstAuthIDEncryptionKey"AES Auth ID Encryption"
KDFSaltConstVMessHeaderPayloadAEADKey"VMess Header AEAD Key"
KDFSaltConstVMessHeaderPayloadAEADIV"VMess Header AEAD Nonce"
KDFSaltConstVMessHeaderPayloadLengthAEADKey"VMess Header AEAD Key_Length"
KDFSaltConstVMessHeaderPayloadLengthAEADIV"VMess Header AEAD Nonce_Length"
KDFSaltConstAEADRespHeaderLenKey"AEAD Resp Header Len Key"
KDFSaltConstAEADRespHeaderLenIV"AEAD Resp Header Len IV"
KDFSaltConstAEADRespHeaderPayloadKey"AEAD Resp Header Key"
KDFSaltConstAEADRespHeaderPayloadIV"AEAD Resp Header IV"

Исходный код: proxy/vmess/aead/consts.go:1-14

Аутентификация пользователей

CmdKey

CmdKey вычисляется из UUID пользователя:

go
func NewID(uuid uuid.UUID) *ID {
    // cmdKey = MD5(uuid.Bytes() + []byte("c48619fe-8f02-49e0-b9e9-edf763e17e21"))
}

Исходный код: common/protocol/id.go

TimedUserValidator

Сервер поддерживает TimedUserValidator, который:

  1. Хранит все CmdKey пользователей в AuthIDDecoderHolder
  2. При каждом входящем соединении пытается AES-расшифровать 16-байтный Auth ID с ключом каждого пользователя
  3. Проверяет контрольную сумму CRC32, диапазон временных меток (+-120 сек.) и фильтр повторов
go
func (a *AuthIDDecoderHolder) Match(authID [16]byte) (interface{}, error) {
    for _, v := range a.decoders {
        t, z, _, d := v.dec.Decode(authID)
        if z != crc32.ChecksumIEEE(d[:12]) { continue }
        if math.Abs(float64(t) - float64(time.Now().Unix())) > 120 { return nil, ErrInvalidTime }
        if !a.filter.Check(authID) { return nil, ErrReplay }
        return v.ticket, nil
    }
    return nil, ErrNotFound
}

Исходный код: proxy/vmess/aead/authid.go:99-121

Зерно поведения

Детерминированное «зерно поведения» вычисляется из всех ID пользователей с помощью HMAC-SHA256 + CRC64. Это зерно управляет паттерном сброса данных (случайные длины чтения перед закрытием невалидных соединений) для предотвращения атак зондирования.

Исходный код: proxy/vmess/validator.go:114-123

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

Файл: proxy/vmess/inbound/inbound.go

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

  1. Устанавливает тайм-аут чтения рукопожатия из политики
  2. Создаёт ServerSession с TimedUserValidator
  3. Вызывает DecodeRequestHeader() для аутентификации и разбора
  4. Передаёт данные в диспетчер маршрутизации
  5. Запускает запрос (чтение от клиента, запись в канал) и ответ (чтение из канала, запись клиенту) как параллельные задачи

Ключевые строки: proxy/vmess/inbound/inbound.go:226-319

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

Файл: proxy/vmess/outbound/outbound.go

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

  1. Выбирает сервер и пользователя из конфигурации
  2. Выбирает тип шифрования из настроек аккаунта
  3. Автоматически включает ChunkMasking и GlobalPadding для AEAD-шифров
  4. Поддерживает SecurityType_ZERO (без шифрования, без блочного разбиения) для случаев использования XTLS
  5. Создаёт ClientSession со случайными ключом/IV тела
  6. Кодирует заголовок запроса + тело, затем читает ответ

Ключевые строки: proxy/vmess/outbound/outbound.go:57-225

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

  1. Устаревший протокол удалён: Старая не-AEAD аутентификация VMess (на основе MD5) полностью удалена. Поддерживается только AEAD. Если сервер не может сопоставить ни одного пользователя через AEAD-расшифровку, он возвращает ошибку с детерминированным сбросом данных.

  2. Защита от повтора сессий: Два уровня — AuthIDDecoderHolder.filter (120-секундный map-фильтр для Auth ID) и SessionHistory (3-минутный кеш кортежей {user, bodyKey, bodyIV}).

  3. Дополнение: Когда включён GlobalPadding, ShakeSizeParser.NextPaddingLen() возвращает shake128(IV) % 64, добавляя 0-63 байта случайного дополнения к каждому блоку.

  4. Эксперимент с аутентифицированной длиной: При включении через TestsEnabled: "AuthenticatedLength" размеры блоков сами по себе шифруются AEAD с использованием отдельного ключа, полученного из KDF16(bodyKey, "auth_len").

  5. UDP через Mux: VMess оборачивает UDP-трафик как Mux-соединения к v1.mux.cool:666, используя фрейминг XUDP.

  6. Формат адресов: VMess использует порядок порт-затем-адрес (в отличие от SOCKS5, где адрес-затем-порт). Байты типа адреса: 0x01=IPv4, 0x02=Домен, 0x03=IPv6.

Исходный код: proxy/vmess/encoding/encoding.go:12-17

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