Перейти к содержанию

Порт-форвардер поневоле: почему выбор target в REALITY важнее, чем кажется

Опубликовано
  • Админы

Инструкции по REALITY обычно проскакивают выбор сайта-донора одной строчкой: «укажите любой зарубежный домен». Строчка дешёвая, последствия — нет. От неё зависит, что увидит на вашем 443-м порту случайный сканер и какой объём чужого трафика ваш VPS согласится возить бесплатно.

Разложим это поле по-настоящему: сначала механика фолбэка, потом четыре группы критериев с готовыми командами, затем полная таблица параметров realitySettings и скрипт, который прогоняет весь список за один заход.

Проверка сайта-донора для REALITY
Что проверяется у кандидата в доноры и какими строками вывода это читается

Куда уходит соединение, которое не прошло проверку

Формулировка короткая: если аутентификация REALITY не удалась, сервер отдаёт соединение напрямую на target. Никакой имитации — ClientHello уходит целиком, а клиенту возвращается настоящий ответ настоящего сайта.

Развилка обработки ClientHello в REALITY
Развилка: аутентификация прошла — временный сертификат; не прошла — прямой форвард на донора

Приятная сторона очевидна: постучавшийся без ключа получит чужой валидный TLS с чужим сертификатом. Неприятная описана в документации Project X буквально — когда у донора «особенный» адрес, скажем он живёт за Cloudflare CDN, ваша машина по факту работает порт-форвардером для Cloudflare. Там же названы и способы это придавить: фильтрация средствами Nginx либо параметры limitFallbackUpload и limitFallbackDownload.

❗ Главное

Донор — это не декорация. Это адрес, куда ваш сервер ходит при каждом неудачном хендшейке, чей сертификат он показывает наблюдателю и чью полосу оплачивает из своего кармана.

Планка, заданная в README проекта REALITY, выглядит скромно: сайт зарубежный, есть TLSv1.3 и H2, домен не работает перевалочным пунктом для редиректов. Дополнительными плюсами названы близкий по географии адрес, зашифрованные сообщения хендшейка после Server Hello (в качестве образца упомянут dl.google.com) и поддержка OCSP Stapling. Каждый пункт измеряется руками — этим и займёмся.

Группа 1. То, что отсеивает кандидата сразу

Три быстрых отказа: нет TLSv1.3, нет h2, домен только редиректит. Первые два читаются одной командой:

openssl s_client -connect www.пример.tld:443 -servername www.пример.tld \
    -alpn h2 -status -tls1_3 </dev/null 2>/dev/null | grep -E "^New,|ALPN|Server Temp Key|OCSP"

Флаг -tls1_3 оставляет клиенту единственный вариант. Соединение не поднялось — кандидата вычёркиваем: конструкция REALITY заимствует свойства хендшейка версии 1.3, на 1.2 она не собирается. Строка ALPN protocol: h2 обязана присутствовать; http/1.1 или пустой ALPN означают, что донор ведёт себя не так, как ведут себя крупные сайты в браузере.

Разметка вывода openssl s_client
Строки вывода OpenSSL 3.x, в которых лежат ответы; значения зависят от домена

Заодно видна строка Server Temp Key: X25519, 253 bits — согласованная группа обмена ключами. Документация REALITY никаких требований к ней не выдвигает, но глянуть стоит: чем меньше профиль донора отличается от типичного «крупный сайт отвечает браузеру», тем лучше.

Третий отказ проверяется через коды ответа:

curl -sI -o /dev/null -w "%{http_code} %{redirect_url}\n" https://пример.tld/
curl -sI -o /dev/null -w "%{http_code} %{redirect_url}\n" https://www.пример.tld/

Наглядно это видно на общеизвестном домене: python.org выдаёт 301 https://www.python.org/, тогда как www.python.org отвечает честной двухсоткой. Ровно поэтому официальные примеры перечисляют в serverNames оба имени, а в target ставят то, за которым лежит содержимое.

⚠️ Осторожно

Апекс-домен, умеющий только перебрасывать на www, в доноры не годится. Ваш сервер начнёт раздавать сканерам редиректы там, где должен раздавать страницу, — а это как раз ситуация, когда «сайт» ведёт себя не как сайт.

Группа 2. То, из чего складывается сам конфиг

Список имён для serverNames берётся не с потолка, а из сертификата донора:

openssl s_client -connect www.пример.tld:443 -servername www.пример.tld </dev/null 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName

И здесь спрятана мина. Подстановочные символы в serverNames не поддерживаются — об этом сказано и в README, и в описании поля. Сертификат выписан на *.пример.tld? Значит, перечисляйте конкретные имена, которые резолвятся и реально отдают страницу.

⛔ Так делать нельзя

"serverNames": ["*.пример.tld"] — это не краткая запись, а неработающий конфиг: звёздочку REALITY не раскрывает.

Полезная тонкость того же поля: среди значений допустима пустая строка "", и она разрешает подключения совсем без SNI. Со стороны клиента есть парная возможность — вписать в serverName произвольный IP-адрес, и тогда ClientHello уйдёт без SNI.

Группа 3. То, во что вам обойдётся содержание донора

dig +short www.пример.tld

Полученный адрес пробивается по владельцу — whois, RIPE, любой определитель ASN. Обнаружился крупный CDN? Возвращаемся к цитате из начала: весь неаутентифицированный поток пойдёт туда за ваш счёт. Отдельная беда в том, что открытый форвардер находится без труда, а трафик принимает от кого угодно.

Штатный тормоз — токен-бакет на фолбэке:

"limitFallbackUpload": {
  "afterBytes": 0,
  "bytesPerSec": 0,
  "burstBytesPerSec": 0
},
"limitFallbackDownload": {
  "afterBytes": 0,
  "bytesPerSec": 0,
  "burstBytesPerSec": 0
}

Параметр afterBytes задаёт объём, пропускаемый без ограничений (именно эти первые килобайты и есть настоящий ответ донора, который обязан выглядеть штатно), bytesPerSec — скорость после порога, burstBytesPerSec — разрешённый всплеск. Ноль в bytesPerSec означает, что ограничение не работает.

🔒 Безопасность

Есть и обратная сторона: чересчур одинаковые лимиты сами превращаются в признак. README REALITY прямо советует рандомизировать подобные параметры при массовом автоматическом развёртывании, чтобы конфиги не совпадали байт в байт.

Вторая статья расходов — задержка:

curl -o /dev/null -s -w "connect=%{time_connect}s tls=%{time_appconnect}s\n" https://www.пример.tld/

Упомянутая в README географическая близость считается от сервера, а не от вас. Каждое неудачное соединение — очередной поход VPS к донору, и его задержка суммируется с вашей. Донор в соседнем дата-центре и донор за океаном дают разный профиль отклика.

Группа 4. То, что важно только при ML-DSA-65

Пункт касается исключительно тех, кто включает mldsa65Seed. Требование сформулировано жёстко: с включённой постквантовой подписью сертификат, который отдаёт target, обязан превышать 3500 байт — иначе временный сертификат REALITY окажется крупнее оригинала и подмена станет заметна по размеру.

openssl s_client -connect www.пример.tld:443 -servername www.пример.tld -showcerts </dev/null 2>/dev/null \
  | awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' | grep -v CERTIFICATE \
  | tr -d '\n' | base64 -d | wc -c

На выходе — суммарный размер цепочки в байтах DER. Не дотянули до 3500? Либо другой донор, либо отказ от mldsa65Seed.

📌 Заметка

Порог без труда берут домены с длинными цепочками и RSA-сертификатами, а компактные ECDSA-цепочки из двух звеньев в него нередко не влезают. Тот редкий случай, когда более современный сертификат донора играет против вас.

Параметры realitySettings: что откуда берётся

Поле (сервер)ОбязательноеЧто писатьОткуда значение
targetдаwww.пример.tld:443, формат как у dest в VLESS fallbacksгруппы 1–4
xverнетверсия PROXY-протокола, формат как у xver в fallbacksсхема вашего фронта
serverNamesдаконкретные имена, без *; допустима пустая строкаSAN сертификата, группа 2
privateKeyдавывод xray x25519генерируется локально
shortIdsдадо 16 hex-знаков, чётное количество; пустое значение разрешает клиенту не слать shortIdпридумываете
minClientVerнетпо умолчанию 26.3.27документация
maxClientVerнетверхняя граница версии клиентадокументация
maxTimeDiffнетдопустимое расхождение часов, мсваш выбор
mldsa65Seedнетseed постквантовой подписи ML-DSA-65требует группу 4
limitFallbackUpload / limitFallbackDownloadнетafterBytes, bytesPerSec, burstBytesPerSecгруппа 3

Формат shortIds стоит запомнить отдельно: восемь байт — это шестнадцать шестнадцатеричных знаков, количество знаков обязано быть чётным, короткая запись добивается нулями. Пример из документации: "aa1234" разворачивается в "aa12340000000000".

У клиента без вариантов нужны fingerprint (режим unsafe, выключающий uTLS, здесь недоступен) и password. Второе — переименованный publicKey; в документации отдельно оговорено, что это действительно публичный ключ x25519, однако в схеме REALITY он лежит у клиента, и выкладывать его в открытый доступ не следует. Есть также spiderX — стартовый путь «краулера», который у разных клиентов должен различаться.

ℹ️ Справка

Названия полей разъехались, и с этим придётся жить: свежая документация оперирует именами target и password, а официальный репозиторий Xray-examples по-прежнему показывает конфиг с dest и publicKey. Генераторы ссылок и панели используют оба варианта, так что старые имена в чужом конфиге — обычное дело.

То же самое в sing-box

Донор описывается блоком tls.reality.handshake — поля server и server_port. Пара ключей создаётся командой sing-box generate reality-keypair, short_id представляет собой hex-строку длиной от нуля до восьми знаков, а пустой max_time_difference попросту выключает контроль расхождения часов. Критерии к сайту-донору не меняются — всё описанное выше переносится один в один.

Один скрипт вместо десяти команд

#!/bin/bash
# ./check-target.sh www.пример.tld
h="$1"
o=$(openssl s_client -connect "$h:443" -servername "$h" -alpn h2 -status -tls1_3 </dev/null 2>/dev/null)
[ -z "$o" ] && { echo "$h: TLSv1.3 не согласован — отбой"; exit 1; }
echo "$o" | grep -E "^New,|^ALPN protocol|Server Temp Key|OCSP"
echo "$h" | xargs -I{} curl -sI -o /dev/null -w "HTTP: %{http_code} %{redirect_url}\n" "https://{}/"
echo "$o" | openssl x509 -noout -ext subjectAltName 2>/dev/null | tail -n +2
echo "IP: $(dig +short "$h" | tr '\n' ' ')"
curl -o /dev/null -s -w "connect=%{time_connect}s tls=%{time_appconnect}s\n" "https://$h/"
chain=$(openssl s_client -connect "$h:443" -servername "$h" -showcerts </dev/null 2>/dev/null \
  | awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' | grep -v CERTIFICATE | tr -d '\n' | base64 -d | wc -c)
echo "Цепочка: $chain байт (для mldsa65Seed нужно > 3500)"

Запускать обязательно на самом VPS. С ноутбука третья и вторая половина проверок теряют смысл, а результат первой может разойтись с тем, что увидит сервер.

Слепые зоны этого стенда

Скрипт закрывает техническую часть и ничего не говорит про остальное, а остальное порой весит больше.

Он не подскажет, сколько соседних серверов на том же хостинге и в той же подсети уже назначили донором тот же домен, — а одинаковый SNI на десятке смежных адресов бросается в глаза сильнее любой мелочи в хендшейке. Не предскажет поведение донора через месяц: смена CDN, новый сертификат покороче, внезапный редирект. Не спасёт от активного зондирования: неаутентифицированным REALITY честно отдаёт чужой сайт, но профиль задержек вашей машины всё равно не совпадает с профилем донора.

✅ Как правильно

Гоняйте стенд не однократно при установке, а раз в пару месяцев по расписанию. И держите про запас второго проверенного кандидата, чтобы замена сводилась к правке одной строки в конфиге.

Общий трезвый итог: грамотно подобранный донор снимает несколько конкретных признаков, но невидимым соединение не делает.


Источники

💬 Вопрос к сообществу

Что у вас прописано в target прямо сейчас и как этот домен туда попал — считали задержку, смотрели сертификат или взяли из первой попавшейся инструкции? Случалось ли, что донор «протухал» и его приходилось экстренно менять?

Featured Replies

No posts to show

Последние посетители 0

  • Ни одного зарегистрированного пользователя не просматривает данную страницу
Яндекс.Метрика

Account

Navigation

Поиск

Поиск

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.