Уровень безопасности REALITY
Введение
REALITY -- это пользовательский протокол безопасности, расширяющий TLS 1.3, который позволяет серверу выдавать себя за любой реальный TLS-сервер без необходимости иметь его сертификат или закрытый ключ. Вместо предъявления собственного сертификата REALITY-сервер проксирует TLS-рукопожатие к легитимному серверу назначения ("dest"). Авторизованные клиенты (знающие открытый ключ сервера и Short ID) могут аутентифицировать сервер через пользовательскую схему, встроенную в Session ID TLS 1.3 ClientHello. Неавторизованные клиенты или активные зондировщики получают подлинный сертификат от dest-сервера, что делает прокси неотличимым от настоящего сервиса. Центр сертификации не требуется.
Ключевые файлы
transport/internet/reality/reality.go-- Логика клиентского (UClient) и серверного (Server) соединенийtransport/internet/reality/config.go--GetREALITYConfig(), извлечение конфигурации
Архитектура
sequenceDiagram
participant C as REALITY-клиент
participant S as REALITY-сервер
participant D as Dest-сервер (напр. google.com)
Note over C: Знает: PublicKey, ShortId, ServerName
Note over S: Знает: PrivateKey, ShortIds[], ServerNames[]
C->>S: TLS ClientHello (SessionId = зашифрованная аутентификация)
S->>S: Расшифровка SessionId, проверка ShortId + временной метки
alt Авторизованный клиент
S->>C: TLS ServerHello (с ed25519-сертификатом, подписанным AuthKey)
C->>C: Проверка подписи сертификата с AuthKey
Note over C,S: Аутентифицированное REALITY-соединение
else Неавторизованный / Зондировщик
S->>D: Пересылка ClientHello
D->>S: Настоящий ServerHello + Сертификат
S->>C: Пересылка настоящего ответа
Note over C,S: Клиент видит настоящий сертификат google.com
Note over S: Соединение становится прозрачным прокси к Dest
endРеализация клиента
UClient
reality.UClient (reality/reality.go:117-277) выполняет REALITY-рукопожатие:
Шаг 1: Создание uTLS-соединения
func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destination) (net.Conn, error) {
uConn := &UConn{Config: config}
utlsConfig := &utls.Config{
VerifyPeerCertificate: uConn.VerifyPeerCertificate,
ServerName: config.ServerName,
InsecureSkipVerify: true,
SessionTicketsDisabled: true,
}
fingerprint := tls.GetFingerprint(config.Fingerprint)
uConn.UConn = utls.UClient(c, utlsConfig, *fingerprint)InsecureSkipVerify установлен в true, поскольку проверка выполняется пользовательской функцией VerifyPeerCertificate REALITY, а не встроенной цепочкой Go.
Шаг 2: Построение аутентифицированного Session ID
Session ID (32 байта) конструируется и шифруется (reality.go:138-176):
hello := uConn.HandshakeState.Hello
hello.SessionId = make([]byte, 32)
// Байты 0-2: Версия Xray
hello.SessionId[0] = core.Version_x
hello.SessionId[1] = core.Version_y
hello.SessionId[2] = core.Version_z
hello.SessionId[3] = 0 // зарезервировано
// Байты 4-7: Текущая Unix-метка времени (big-endian)
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
// Байты 8-15: Short ID (8 байт)
copy(hello.SessionId[8:], config.ShortId)Шаг 3: Вывод Auth Key
Auth Key выводится из общего секрета ECDH (reality.go:152-169):
publicKey, _ := ecdh.X25519().NewPublicKey(config.PublicKey)
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe
// Резервный вариант MlkemEcdhe для постквантового обмена ключами
uConn.AuthKey, _ = ecdhe.ECDH(publicKey)
// Вывод HKDF
hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:20], []byte("REALITY")).Read(uConn.AuthKey)HKDF использует первые 20 байт ClientHello.Random в качестве соли и литеральную строку "REALITY" в качестве info.
Шаг 4: Шифрование Session ID
aead := crypto.NewAesGcm(uConn.AuthKey)
aead.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw)- Nonce: Последние 12 байт
ClientHello.Random(байты 20-31) - Открытый текст: Первые 16 байт Session ID
- Дополнительные данные: Весь сырой ClientHello
- Результат: 16 байт шифротекста + 16 байт тега GCM = перезаписывают полный 32-байтный Session ID
Зашифрованный Session ID затем копируется обратно в сырые байты ClientHello (reality.go:175).
Шаг 5: Проверка серверного сертификата
VerifyPeerCertificate (reality/reality.go:76-115) проверяет, является ли сервер подлинным REALITY-сервером:
func (c *UConn) VerifyPeerCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
certs := // разбор rawCerts
if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok {
h := hmac.New(sha512.New, c.AuthKey)
h.Write(pub)
if bytes.Equal(h.Sum(nil), certs[0].Signature) {
// Опциональная проверка ML-DSA-65 для постквантовой безопасности
if len(c.Config.Mldsa65Verify) > 0 {
// Проверка подписи ML-DSA-65 по ClientHello + ServerHello
}
c.Verified = true
return nil
}
}
// Откат к стандартной проверке x509 (настоящий сертификат от dest-сервера)
opts := x509.VerifyOptions{DNSName: c.ServerName, ...}
certs[0].Verify(opts)
return nil // проверка проходит (это настоящий сертификат), но Verified остается false
}REALITY-сервер генерирует ed25519-сертификат, в котором:
- Открытый ключ: ed25519 открытый ключ
- Подпись:
HMAC-SHA512(AuthKey, PublicKey)
Если HMAC совпадает, клиент знает, что общается с настоящим REALITY-сервером. Если нет, сертификат проверяется обычным образом (он пришел от dest-сервера), но c.Verified остается false.
Шаг 6: Обработка неудачной проверки
Если uConn.Verified равен false после рукопожатия (reality.go:183-274), клиент имитирует поведение реального браузера:
if !uConn.Verified {
// Режим "Spider": обход dest-сервера для генерации реалистичного трафика
client := &http.Client{Transport: &http2.Transport{...}}
// GET-запросы страниц, переход по ссылкам, добавление cookies
// Это делает соединение похожим на реального браузера, посещающего сайт
time.Sleep(randomDuration)
return nil, errors.New("REALITY: processed invalid connection")
}Spider обходит страницы dest-сервера с реалистичными временными задержками, затем закрывает соединение. Это делает активное зондирование неотличимым от посещения реальным браузером.
Серверное соединение
reality.Server (reality/reality.go:52-55) оборачивает с помощью библиотеки reality:
func Server(c net.Conn, config *reality.Config) (net.Conn, error) {
realityConn, err := reality.Server(context.Background(), c, config)
return &Conn{Conn: realityConn}, err
}Фактическая серверная логика находится в библиотеке github.com/xtls/reality, которая:
- Читает ClientHello
- Расшифровывает Session ID своим закрытым ключом
- Проверяет Short ID, временную метку и версию клиента
- Если авторизован: генерирует ed25519-сертификат, подписанный общим AuthKey
- Если не авторизован: прозрачно проксирует к dest-серверу
Конфигурация
Конфигурация клиента
Из reality/config.go:74-83:
func ConfigFromStreamSettings(settings *internet.MemoryStreamConfig) *Config {
config, ok := settings.SecuritySettings.(*Config)
if !ok { return nil }
return config
}Клиенту необходимы:
- ServerName: SNI для TLS-рукопожатия (имитируемый сервер)
- Fingerprint: Отпечаток uTLS (обязателен, не может быть пустым)
- PublicKey: Открытый ключ X25519 сервера (32 байта)
- ShortId: Short ID клиента (до 8 байт)
- SpiderX: Начальный путь для обхода spider
- SpiderY: Массив из 10 значений int64, управляющих поведением spider (дополнение cookies, конкурентность, интервалы и т.д.)
- Show: Режим отладки, выводящий состояние REALITY в stdout
- Mldsa65Verify: Открытый ключ ML-DSA-65 для постквантовой проверки
Конфигурация сервера
GetREALITYConfig (reality/config.go:16-58) строит reality.Config:
func (c *Config) GetREALITYConfig() *reality.Config {
config := &reality.Config{
Show: c.Show,
Type: c.Type,
Dest: c.Dest, // адрес dest-сервера
Xver: byte(c.Xver), // версия протокола PROXY
PrivateKey: c.PrivateKey, // закрытый ключ X25519
MinClientVer: c.MinClientVer, // минимальная версия клиента
MaxClientVer: c.MaxClientVer, // максимальная версия клиента
MaxTimeDiff: time.Duration(c.MaxTimeDiff) * time.Millisecond,
SessionTicketsDisabled: true,
}
// Заполнение карты ServerNames и карты ShortIds
config.ServerNames[serverName] = true
config.ShortIds[shortId] = true
// Опциональный ключ подписи ML-DSA-65
// Опциональное ограничение скорости для fallback-соединений
return config
}Серверу необходимы:
- PrivateKey: Закрытый ключ X25519 (32 байта)
- ServerNames: Допустимые значения SNI
- ShortIds: Допустимые Short ID (карта [8]byte)
- Dest: Адрес реального сервера для проксирования неавторизованных клиентов
- MaxTimeDiff: Максимально допустимое расхождение часов (миллисекунды)
- MinClientVer / MaxClientVer: Допустимый диапазон версий клиента Xray
Формат Session ID на проводе
Байт 0-2: Версия Xray (x.y.z)
Байт 3: Зарезервировано (0)
Байт 4-7: Unix-метка времени (big-endian uint32)
Байт 8-15: Short ID (8 байт, может быть частично нулевым)
[Зашифровано AES-GCM с использованием AuthKey]
[Nonce = ClientHello.Random[20:32]]
[AAD = сырые байты ClientHello]Сервер расшифровывает это для:
- Проверки Short ID по списку допустимых
- Проверки, что временная метка в пределах
MaxTimeDiff - Проверки, что версия клиента в допустимом диапазоне
Постквантовая поддержка
Обмен ключами
REALITY поддерживает обмен ключами X25519-MLKEM768, когда он доступен в TLS-отпечатке (reality.go:156-161):
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe
if ecdhe == nil {
ecdhe = uConn.HandshakeState.State13.KeyShareKeys.MlkemEcdhe
}Проверка сертификата
Опциональная проверка ML-DSA-65 (Dilithium) (reality.go:88-95):
if len(c.Config.Mldsa65Verify) > 0 {
h.Write(c.HandshakeState.Hello.Raw)
h.Write(c.HandshakeState.ServerHello.Raw)
verify, _ := mldsa65.Scheme().UnmarshalBinaryPublicKey(c.Config.Mldsa65Verify)
if mldsa65.Verify(verify.(*mldsa65.PublicKey), h.Sum(nil), nil, certs[0].Extensions[0].Value) {
c.Verified = true
}
}Это обеспечивает постквантовую аутентификацию: сервер подписывает HMAC(AuthKey, PublicKey || ClientHello.Raw || ServerHello.Raw) своим закрытым ключом ML-DSA-65, а клиент проверяет с помощью предварительно распространенного открытого ключа.
Поддержка журналирования ключей
И клиент, и сервер поддерживают журналирование TLS-ключей для отладки (reality/config.go:61-72):
func KeyLogWriterFromConfig(c *Config) io.Writer {
if len(c.MasterKeyLog) <= 0 || c.MasterKeyLog == "none" { return nil }
writer, _ := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
return writer
}Интеграция с транспортами
REALITY интегрируется с транспортами в двух точках:
Сторона клиента
Каждый транспорт проверяет наличие конфигурации REALITY и применяет ее:
// tcp/dialer.go:89-93
if config := reality.ConfigFromStreamSettings(streamSettings); config != nil {
conn, err = reality.UClient(conn, config, ctx, dest)
}Сторона сервера
Транспорты оборачивают свой слушатель:
// tcp/hub.go:76-79
if config := reality.ConfigFromStreamSettings(streamSettings); config != nil {
l.realityConfig = config.GetREALITYConfig()
go goreality.DetectPostHandshakeRecordsLens(l.realityConfig)
}DetectPostHandshakeRecordsLens вызывается при запуске для снятия отпечатка поведения dest-сервера после рукопожатия, что обеспечивает более точную имитацию.
Примечания по реализации
- Без CA: REALITY не требует никаких сертификатов. Сервер либо предъявляет настоящий сертификат dest-сервера (для неавторизованных клиентов), либо динамически сгенерированный ed25519-сертификат (для авторизованных клиентов).
- Устойчивость к активному зондированию: Неавторизованные соединения прозрачно проксируются к dest-серверу. Предъявляется настоящий сертификат dest-сервера, что делает зондирование неотличимым от прямого соединения.
- Spider против фингерпринтинга: Когда REALITY-клиент получает настоящий сертификат (что указывает на зондирование или ошибку конфигурации), он обходит dest-сервер как настоящий браузер перед отключением, предотвращая обнаружение на основе временных характеристик.
- Отпечаток обязателен: В отличие от TLS, REALITY требует отпечатка uTLS (
reality.go:133-136). Стандартный Go TLS не может использоваться, поскольку REALITY нуждается в доступе к внутренней структуре ClientHello. - Фиксированная позиция Session ID: Session ID занимает байты начиная с 39-го в сыром ClientHello (
reality.go:142), что является фиксированной позицией в структуре TLS 1.3 ClientHello. - Синхронизация часов: Временные метки клиента и сервера должны быть в пределах
MaxTimeDiff(по умолчанию зависит от конфигурации). Большое расхождение часов приводит к ошибке аутентификации. - Short ID как контроль доступа: Разные Short ID могут назначаться разным пользователям/клиентам, обеспечивая контроль доступа на уровне пользователя без смены ключей.
- Ограничение скорости:
LimitFallbackUpload/LimitFallbackDownloadмогут ограничивать полосу пропускания для неавторизованных (fallback) соединений (config.go:41-49).