Skip to content

Уровень безопасности 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(), извлечение конфигурации

Архитектура

mermaid
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-соединения

go
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):

go
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):

go
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

go
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-сервером:

go
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), клиент имитирует поведение реального браузера:

go
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:

go
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, которая:

  1. Читает ClientHello
  2. Расшифровывает Session ID своим закрытым ключом
  3. Проверяет Short ID, временную метку и версию клиента
  4. Если авторизован: генерирует ed25519-сертификат, подписанный общим AuthKey
  5. Если не авторизован: прозрачно проксирует к dest-серверу

Конфигурация

Конфигурация клиента

Из reality/config.go:74-83:

go
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:

go
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]

Сервер расшифровывает это для:

  1. Проверки Short ID по списку допустимых
  2. Проверки, что временная метка в пределах MaxTimeDiff
  3. Проверки, что версия клиента в допустимом диапазоне

Постквантовая поддержка

Обмен ключами

REALITY поддерживает обмен ключами X25519-MLKEM768, когда он доступен в TLS-отпечатке (reality.go:156-161):

go
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe
if ecdhe == nil {
    ecdhe = uConn.HandshakeState.State13.KeyShareKeys.MlkemEcdhe
}

Проверка сертификата

Опциональная проверка ML-DSA-65 (Dilithium) (reality.go:88-95):

go
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):

go
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 и применяет ее:

go
// tcp/dialer.go:89-93
if config := reality.ConfigFromStreamSettings(streamSettings); config != nil {
    conn, err = reality.UClient(conn, config, ctx, dest)
}

Сторона сервера

Транспорты оборачивают свой слушатель:

go
// 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).

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