Протокол 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 вычисляется следующим образом:
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, полученными из:
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, полученными из:
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
| Поле | Размер | Описание |
|---|---|---|
| Version | 1 байт | Всегда 0x01 |
| Body IV | 16 байт | Случайный IV для шифрования тела |
| Body Key | 16 байт | Случайный ключ для шифрования тела |
| Response Header | 1 байт | Случайный байт, сервер должен отправить его обратно |
| Option | 1 байт | Битовая маска: ChunkStream(0x01), ChunkMasking(0x04), GlobalPadding(0x08), AuthenticatedLength(0x10) |
| Security | 1 байт | Верхние 4 бита = длина дополнения (0-15), нижние 4 бита = тип шифра |
| Reserved | 1 байт | Всегда 0x00 |
| Command | 1 байт | 0x01=TCP, 0x02=UDP, 0x03=Mux |
| Address | переменный | Port(2B, BE) + AddrType(1B) + Address |
| Padding | 0-15 байт | Случайное дополнение |
| FNV1a | 4 байта | Хеш FNV-1a всех предшествующих полей (целостность) |
Типы шифрования (нижние 4 бита):
| Значение | Шифр |
|---|---|
| 0x00 | AUTO / UNKNOWN |
| 0x03 | AES-128-GCM |
| 0x04 | ChaCha20-Poly1305 |
| 0x05 | None |
Исходный код: proxy/vmess/encoding/server.go:114-124
Ответ (от сервера к клиенту)
Заголовок ответа также зашифрован AEAD:
+----------------------------+----------------------------+
| Encrypted Length (2+16B) | Encrypted Header (var+16B) |
+----------------------------+----------------------------+Ключи вычисляются из ключа/IV тела ответа:
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 должен совпадать со случайным байтом из запроса.
Шифрование тела
Данные тела передаются в виде аутентифицированных блоков:
graph LR
subgraph "Каждый блок"
A[Длина 2B] --> B[Полезная нагрузка] --> C[Тег аутентификации]
endКодирование размера блока (с маскированием): Когда включён ChunkMasking, поток SHAKE128 XOR'ит 2-байтовое поле длины:
// ShakeSizeParser
mask = SHAKE128(bodyIV).next_2_bytes()
wire_length = actual_length XOR maskИсходный код: proxy/vmess/encoding/auth.go:51-87
Генерация nonce для AEAD тела:
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 байт:
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:
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 пользователя:
func NewID(uuid uuid.UUID) *ID {
// cmdKey = MD5(uuid.Bytes() + []byte("c48619fe-8f02-49e0-b9e9-edf763e17e21"))
}Исходный код: common/protocol/id.go
TimedUserValidator
Сервер поддерживает TimedUserValidator, который:
- Хранит все CmdKey пользователей в
AuthIDDecoderHolder - При каждом входящем соединении пытается AES-расшифровать 16-байтный Auth ID с ключом каждого пользователя
- Проверяет контрольную сумму CRC32, диапазон временных меток (+-120 сек.) и фильтр повторов
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
Входящий обработчик:
- Устанавливает тайм-аут чтения рукопожатия из политики
- Создаёт
ServerSessionсTimedUserValidator - Вызывает
DecodeRequestHeader()для аутентификации и разбора - Передаёт данные в диспетчер маршрутизации
- Запускает запрос (чтение от клиента, запись в канал) и ответ (чтение из канала, запись клиенту) как параллельные задачи
Ключевые строки: proxy/vmess/inbound/inbound.go:226-319
Исходящий обработчик
Файл: proxy/vmess/outbound/outbound.go
Исходящий обработчик:
- Выбирает сервер и пользователя из конфигурации
- Выбирает тип шифрования из настроек аккаунта
- Автоматически включает
ChunkMaskingиGlobalPaddingдля AEAD-шифров - Поддерживает
SecurityType_ZERO(без шифрования, без блочного разбиения) для случаев использования XTLS - Создаёт
ClientSessionсо случайными ключом/IV тела - Кодирует заголовок запроса + тело, затем читает ответ
Ключевые строки: proxy/vmess/outbound/outbound.go:57-225
Примечания по реализации
Устаревший протокол удалён: Старая не-AEAD аутентификация VMess (на основе MD5) полностью удалена. Поддерживается только AEAD. Если сервер не может сопоставить ни одного пользователя через AEAD-расшифровку, он возвращает ошибку с детерминированным сбросом данных.
Защита от повтора сессий: Два уровня —
AuthIDDecoderHolder.filter(120-секундный map-фильтр для Auth ID) иSessionHistory(3-минутный кеш кортежей{user, bodyKey, bodyIV}).Дополнение: Когда включён
GlobalPadding,ShakeSizeParser.NextPaddingLen()возвращаетshake128(IV) % 64, добавляя 0-63 байта случайного дополнения к каждому блоку.Эксперимент с аутентифицированной длиной: При включении через
TestsEnabled: "AuthenticatedLength"размеры блоков сами по себе шифруются AEAD с использованием отдельного ключа, полученного изKDF16(bodyKey, "auth_len").UDP через Mux: VMess оборачивает UDP-трафик как Mux-соединения к
v1.mux.cool:666, используя фрейминг XUDP.Формат адресов: VMess использует порядок порт-затем-адрес (в отличие от SOCKS5, где адрес-затем-порт). Байты типа адреса:
0x01=IPv4,0x02=Домен,0x03=IPv6.
Исходный код: proxy/vmess/encoding/encoding.go:12-17