🎭 REALITY: как прокси прикрывается настоящим чужим сайтом

О чём заметка

Подробный разбор протокола REALITY — замены TLS в Xray-core, которая позволяет прокси-серверу маскироваться под настоящий сторонний сайт без собственного домена и сертификата. Здесь — что происходит на уровне рукопожатия, как устроена криптографическая аутентификация и почему активное зондирование (active probing) не может отличить REALITY-сервер от реального сайта. Разбор основан на чтении исходного кода: серверная часть — форк crypto/tls в github.com/XTLS/REALITY, клиентская — transport/internet/reality/reality.go в Xray-core. REALITY — это отдельный слой (security), не путать с Vision (flow) и VLESS (протокол).

TL;DR

  • REALITY — замена обычного серверного TLS. Владельцу прокси не нужен свой домен и сертификат: сервер маскируется под чужой публичный сайт (например крупный домен).
  • При рукопожатии сервер физически проксирует ClientHello клиента на настоящий сайт и возвращает его настоящий ответ. Для чужого наблюдателя сервер неотличим от порт-форварда на этот сайт.
  • «Свой» клиент опознаётся по криптографической метке, спрятанной в поле SessionId внутри ClientHello. Метка шифруется AES-GCM на общем секрете (X25519 ECDH) и невидима без серверного ключа.
  • Своему клиенту сервер выдаёт временный сертификат, «подписанный» HMAC на общем секрете — только владелец правильного ключа может его проверить. Чужой клиент/зонд получает настоящий сертификат реального сайта.
  • Итог: нет серверного TLS-отпечатка, есть защита от active probing, не нужен свой домен. Криптографически REALITY на 2026 год не вскрыт — DPI борется с ним поведенчески (см. разбор июня 2026).

Проблема, которую решает REALITY

До REALITY, чтобы прокси маскировался под HTTPS, владельцу нужен был собственный домен и настоящий TLS-сертификат (например от Let’s Encrypt). Но это само по себе создавало сигнал для цензора: свежезарегистрированный домен, на который вдруг идёт много странного трафика с одного IP, — подозрительно. Плюс сервер терминировал TLS сам, а значит имел собственный TLS-отпечаток (fingerprint), который можно было каталогизировать и блокировать.

REALITY убирает и то, и другое: не нужен свой домен, и у сервера нет своего TLS-отпечатка, потому что настоящий ответ рукопожатия генерирует не он, а реальный чужой сайт, под который он маскируется.

Как работает рукопожатие: зеркалирование на реальный сайт

Это самая необычная часть REALITY. Точка входа на сервере — функция Server() в tls.go. Как только приходит клиентское соединение, сервер сразу дозванивается до реального сайта (адрес из поля target, например example.com:443) и оборачивает клиентское соединение в специальный MirrorConn.

MirrorConn работает как зеркало: каждый байт, который читается от клиента (включая его ClientHello), немедленно копируется на настоящий сайт. То есть клиентское рукопожатие физически уходит на реальный сервер example.com, и от него приходит настоящий ServerHello. Важно: перехватывается только ServerHello — та часть рукопожатия, которая в TLS 1.3 идёт открытым текстом, — вместе с длинами и таймингами последующих (уже зашифрованных) сообщений. Всё после ServerHello в TLS 1.3 зашифровано, и сервер REALITY туда не лезет.

Проще говоря: REALITY-сервер сначала ведёт себя как «прокладка» между клиентом и настоящим сайтом — просто перекидывает трафик туда-сюда. И только если он узнаёт в клиенте «своего» (по секретной метке), он перехватывает управление. Если не узнаёт — соединение так и остаётся сквозным проксированием на реальный сайт, и клиент честно говорит с example.com.

Сервер берётся за аутентификацию только при двух условиях: соединение по TLS 1.3 и SNI из ClientHello входит в список ServerNames (разрешённых сайтов). Иначе — чистый проброс.

Криптография: как опознаётся «свой» клиент

Метка «я свой» прячется в поле SessionId внутри ClientHello — обычно там случайные 32 байта, и REALITY-метка от случайного мусора неотличима без ключа.

Пошагово (клиент — UClient в reality.go):

  1. Общий секрет. Клиент считает AuthKey через X25519 ECDH: перемножает свой эфемерный ключ из ClientHello с публичным ключом сервера (в конфиге клиента это поле password). Затем прогоняет результат через HKDF (соль — первые 20 байт ClientHello.Random, метка "REALITY"). Сервер вычисляет ровно тот же секрет из своего приватного ключа — классический Диффи-Хеллман.
  2. Метаданные в SessionId. Клиент кладёт в 32 байта версию Xray, текущее время (для защиты от повторов) и свой ShortId (8-байтный идентификатор, различающий клиентов).
  3. Шифрование метки. Всё это шифруется AES-GCM на AuthKey, причём в качестве дополнительных данных (AAD) берётся весь ClientHello. Результат кладётся обратно в SessionId. Без AuthKey метку нельзя ни прочитать, ни подделать.

Сервер делает всё зеркально: вычисляет тот же AuthKey, расшифровывает SessionId, достаёт версию/время/ShortId и проверяет:

  • версия клиента в допустимом окне (MinClientVer/MaxClientVer);
  • время в пределах MaxTimeDiff (защита от воспроизведения старого перехваченного ClientHello);
  • ShortId есть в списке разрешённых (ShortIds).

Если всё сошлось — клиент признан «своим», и сервер перехватывает рукопожатие. Если нет — соединение остаётся проброшенным на реальный сайт.

Временный сертификат «на лету»

Когда клиент опознан как свой, сервер должен отдать ему сертификат — но не может использовать настоящий сертификат example.com (у него нет приватного ключа этого домена). Решение хитрое: сервер берёт заранее заготовленный самоподписанный ed25519-сертификат и подменяет его подпись на HMAC-SHA512, вычисленный на AuthKey.

Обычной проверки цепочки сертификатов у клиента нет (она отключена). Вместо неё клиент повторяет тот же HMAC на своём AuthKey и сверяет с подписью в сертификате. Совпало — значит сертификат выдан именно REALITY-сервером, знающим общий секрет, а не реальным сайтом и не MITM-посредником. Только владелец правильного ключа способен воспроизвести этот HMAC.

REALITY НЕ «ворует сертификаты» — распространённое заблуждение

Частый миф: будто REALITY «крадёт» или «подменяет» настоящий сертификат целевого сайта. Это неверно, и официальное руководство XTLS это прямо опровергает. В TLS 1.3 сертификат сервера зашифрован (идёт после ServerHello), поэтому посредник его в принципе не видит, а сервер REALITY не может его ни прочитать, ни переслать. Что происходит на самом деле: REALITY реализует полноценный TLS 1.3, перехватывает у целевого сайта только открытый ServerHello (его длину, тайминги, внешний вид) и заменяет key_share, а «своему» клиенту генерирует собственный временный доверенный сертификат (подписанный HMAC на общем секрете, см. выше). Настоящий сертификат целевого сайта легитимный клиент в нормальных условиях не получает вообще — он достаётся только чужому клиенту/зонду, которому идёт честный проброс на реальный сайт.

Три исхода для клиента

Клиент REALITY различает три случая по полученному сертификату: временный доверенный (HMAC сошёлся) → прокси работает; настоящий сертификат реального сайта (значит сервер не опознал клиента или это редирект) → клиент уходит в режим «паука» — эмулирует обычный браузинг по сайту, чтобы даже при неудаче выглядеть как реальный посетитель; невалидный сертификат → TLS-ошибка и разрыв. Стартовый путь для режима паука задаётся параметром spiderX.

Защита от active probing

Active probing — это когда цензор сам отправляет запрос на подозрительный сервер, чтобы проверить, прокси это или нет. REALITY здесь практически неуязвим, и вот почему.

Любой ClientHello без валидной REALITY-метки (не тот SNI, неизвестный ShortId, неверный тег AES-GCM, устаревшее время) обрабатывается как чистый проброс на реальный сайт: зонд отправил ClientHello — тот уже физически ушёл на example.com через MirrorConn, и ответ реального сайта вернулся зонду байт-в-байт. Зонд общается с настоящим example.com: настоящий сертификат, настоящий TLS 1.3, настоящее поведение сайта.

Ключевой момент: сервер REALITY не генерирует поддельный ServerHello — он использует тот, что реально пришёл от целевого сайта. Поэтому нет никакого «своего» ответа, который зонд мог бы спровоцировать и опознать. А сама метка спрятана в SessionId и неотличима от случайного session id обычного TLS 1.3-клиента.

Рекомендация из документации REALITY: форвардить на целевой сайт ещё и TCP/80 и UDP/443, чтобы сервер выглядел как обычный порт-форвард на этот сайт со всех сторон.

uTLS: правдоподобный ClientHello

Чтобы сам ClientHello «своего» клиента не выделялся, REALITY использует библиотеку uTLS (refraction-networking/utls) — она собирает ClientHello с точным набором расширений, шифров, GREASE и порядком полей как у настоящего браузера. Браузер задаётся параметром fingerprint (по умолчанию chrome). REALITY встраивает свою метку поверх уже собранного uTLS-рукопожатия, правя только SessionId и не ломая отпечаток.

Обязательное условие: выбранный fingerprint должен поддерживать TLS 1.3 — иначе не будет эфемерного ECDHE-ключа, из которого выводится AuthKey, и рукопожатие невозможно.

Почему REALITY работает только по TCP (не по QUIC)

Для REALITY рекомендуется единственный надёжный транспорт — RAW (TCP). Причина техническая: REALITY прячет свою метку в поле SessionId внутри ClientHello. В обычном TCP-TLS это поле почти всегда заполняется случайными 32 байтами (для совместимости), поэтому REALITY-метка в нём неотличима от случайного мусора — идеальное место, чтобы спрятаться.

А в QUIC (на котором работает HTTP/3) поле SessionId в TLS имеет нулевую длину и модифицировать его нельзя. Некуда встроить метку — значит REALITY поверх QUIC не работает в принципе. Поэтому связка VLESS+REALITY живёт по TCP, и именно поэтому Vision тоже TCP-only.

Как выбрать target и serverNames

Выбор целевого сайта — самая ответственная часть настройки, от неё зависит и скрытность, и скорость. Требования к сайту-камуфляжу:

  • Зарубежный сайт (с точки зрения вашей страны) с поддержкой TLS 1.3 и HTTP/2.
  • Домен, который не редиректит куда-то ещё (редирект domainwww.domain допустим).
  • Желательно IP-адрес, близкий к вашему серверу по сетевому расстоянию (меньше задержка, менее подозрительно).

Не используйте крупные сайты (Google, CDN-гиганты) как target

Многие крупные сайты имеют CDN-серверы внутри страны. Если у сайта есть локальные серверы, то с точки зрения цензора вы в норме должны обращаться к внутренним адресам, а не выходить за границу — обращение за рубеж к «домашнему» сайту само становится аномалией. dl.google.com в примерах официального репозитория приведён только для иллюстрации — реально Google в качестве target не берут. Плюс: если target за CDN (например Cloudflare), непрошедший трафик пробрасывается туда, и ваш сервер превращается в бесплатный порт-форвард к чужому CDN.

Практично: если своего домена нет — просканируйте соседние по ASN домены инструментом RealiTLScanner. Если свой домен есть — используйте его (лучшая практика — сертификат из той же ASN, что и ваш сервер). Дополнительно можно фильтровать нежелательные SNI через Nginx.

Пост-квантовая защита

REALITY готов к эпохе квантовых компьютеров на двух уровнях:

  • Обмен ключами X25519MLKEM768. Если целевой сайт поддерживает этот гибридный пост-квантовый алгоритм, клиент REALITY применяет его автоматически. Проверить поддержку у target: xray tls ping cloudflare.com (подставьте свой адрес).
  • Подпись ML-DSA-65 (опционально). Задаётся парой mldsa65Seed (сервер) / mldsa65Verify (клиент), генерируется командой xray mldsa65. Добавляет к временному сертификату пост-квантовую подпись: даже если в будущем взломают X25519 и утечёт password, MITM-атака останется невозможной. На старые клиенты без поддержки не влияет.

Условие для ML-DSA-65: крупный сертификат target

Пост-квантовая подпись увеличивает размер временного сертификата REALITY. Чтобы это не создавало отличительную особенность, сертификат самого целевого сайта тоже должен быть большим — свыше 3500 байт. Для этого выбирайте target с RSA-сертификатом (не ECDSA — у него длина меньше) и поддержкой X25519MLKEM768. Размер проверяется командой xray tls ping example.com.

Минимальный конфиг

Пример боевого минимума (имена/ключи здесь условные, домен example.com — плейсхолдер вашего реального target). Обратите внимание: у сервера — target/privateKey/shortIds, у клиента — serverName/password/shortId, и с обеих сторон flow: xtls-rprx-vision.

Сервер (inbound):

{
  "protocol": "vless",
  "settings": {
    "clients": [{ "id": "<UUID>", "flow": "xtls-rprx-vision" }],
    "decryption": "none"
  },
  "streamSettings": {
    "network": "raw",
    "security": "reality",
    "realitySettings": {
      "target": "example.com:443",
      "serverNames": ["example.com", "www.example.com"],
      "privateKey": "<приватный ключ из xray x25519>",
      "shortIds": ["6ba85179e30d4fc2"]
    }
  }
}

Клиент (outbound):

{
  "protocol": "vless",
  "settings": {
    "vnext": [{
      "address": "<IP вашего сервера>",
      "port": 443,
      "users": [{ "id": "<тот же UUID>", "encryption": "none", "flow": "xtls-rprx-vision" }]
    }]
  },
  "streamSettings": {
    "network": "tcp",
    "security": "reality",
    "realitySettings": {
      "fingerprint": "chrome",
      "serverName": "www.example.com",
      "password": "<публичный ключ сервера>",
      "shortId": "6ba85179e30d4fc2"
    }
  }
}

target как fallback

target — это по сути точка fallback: весь трафик, не прошедший REALITY-проверку (чужой клиент, зонд), Xray напрямую пересылает туда. Можно указать не только внешний сайт, но и локальный фейковый веб-сервер — например "target": "localhost:10433", где на порту 10433 крутится подставной сайт. Тогда зонд увидит именно его. Если же оставить target на реальный внешний сайт — непрошедший трафик пойдёт туда.

Ключевые параметры конфига

ПараметрСторонаЧто это
targetсерверАдрес реального сайта для маскировки и проброса (example.com:443). По наличию этого поля ядро отличает серверный конфиг от клиентского. Старое имя — dest (сейчас оба поля — алиасы)
serverNamesсерверСписок разрешённых SNI; клиентский SNI должен точно совпасть (wildcard не поддерживается)
privateKeyсерверПриватный ключ X25519 (генерируется xray x25519)
passwordклиентПубличный ключ X25519 сервера. Раньше поле называлось publicKey, переименовано во избежание путаницы
shortIds / shortIdсервер / клиентСписок разрешённых 8-байтных идентификаторов клиентов (openssl rand -hex 8); пустое значение допускает пустой shortId
maxTimeDiffсерверДопустимый разброс времени клиента в миллисекундах (защита от replay); 0 — выключено
minClientVer / maxClientVerсерверОкно допустимых версий Xray у клиента (например 1.8.6)
fingerprintклиентuTLS-отпечаток браузера (по умолчанию chrome)
serverNameклиентSNI, который клиент отправляет (из серверного serverNames)
spiderXклиентСтартовый путь режима «паука» при неудаче (например /blog)
xverсерверВерсия PROXY protocol (0/1/2) для передачи реального IP клиента на target; по умолчанию 0
mldsa65Seed / mldsa65Verifyсервер / клиентОпциональная пост-квантовая подпись ML-DSA-65 (см. ниже)

Про лимит fallback-трафика ( limitFallbackUpload / limitFallbackDownload)

Эти параметры троттлят скорость проброса непрошедшего проверку трафика на реальный сайт (алгоритм «маркерная корзина»: afterBytes — после скольки байт включается лимит, bytesPerSec — базовая скорость, burstBytesPerSec — пиковая). Нужны в основном чтобы сервер не превратился в бесплатный порт-форвард к чужому CDN. Но официальная дока прямо предупреждает: включённый лимит создаёт детектируемый паттерн. Если и включать — рандомизируйте значения (особенно в GUI/панелях/скриптах установки), иначе фиксированный лимит сам станет сигнатурой. По умолчанию лучше не трогать, а вместо этого использовать сертификат из той же ASN или свой домен.

Отличие от обычного TLS и от ShadowTLS

От обычного TLS: не нужен свой домен и сертификат; настоящий ServerHello генерирует целевой сайт, а не сервер (нет серверного отпечатка); обычная валидация цепочки заменена на HMAC-проверку на общем секрете.

От ShadowTLS: оба проксируют рукопожатие к реальному сайту, но ShadowTLS перехватывает соединение после рукопожатия и продолжает своим потоком (характерный признак — hijack после ServerHello). REALITY же зеркалит ClientHello на настоящий сайт, берёт от него открытый ServerHello (с его длинами и таймингами) и генерирует своему клиенту собственный временный сертификат; для чужого клиента соединение остаётся буквально сквозным TLS реального сайта.

Что REALITY НЕ защищает

Криптографически REALITY на июль 2026 не вскрыт — метка неотличима без ключа, active probing бессилен. Но это не значит, что связку нельзя заблокировать: современный DPI перешёл на поведенческий анализ (тайминги, объёмы, эвристики соединения), который не трогает саму криптографию REALITY, но ловит прокси по косвенным признакам. Подробный разбор — в заметке про схему ограничений июня 2026.

📚 См. также


🤖 Эти статьи открыты — можно обучать на них ИИ

При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: исходник этой заметки · весь репозиторий.