Шаг 0. Пре-флайт: ядро, часы, порт 443
Чистый VPS, root, свободный 443/tcp. Ставим официальным скриптом XTLS:
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
xray version
Reality есть в Xray-core с 1.8.0, но ориентируйтесь на ветку 25.x: в старых ядрах отличается вывод генератора ключей и часть имён полей. Что появилось после установки:
| Путь | Что это |
|---|---|
/usr/local/bin/xray | бинарь ядра |
/usr/local/etc/xray/config.json | основной конфиг (его и правим) |
/var/log/xray/access.log, error.log | логи |
/etc/systemd/system/xray.service | юнит, управление через systemctl |
Три вещи до конфига — иначе потом ловите «плавающие» симптомы, которые не лечатся правкой JSON:
# 1. Время. В аутентификацию Reality входит временная метка;
# при заметном расхождении часов часть клиентов получает отказ.
timedatectl set-ntp true && timedatectl status | grep -E "synchronized|NTP"
# 2. Файрвол: наружу только SSH и 443.
ufw allow 22/tcp && ufw allow 443/tcp && ufw --force enable
# 3. BBR — по наблюдениям ровнее скорость на дальних плечах.
printf 'net.core.default_qdisc=fq\nnet.ipv4.tcp_congestion_control=bbr\n' >> /etc/sysctl.conf
sysctl -p && sysctl net.ipv4.tcp_congestion_control
Отдельно проверьте исходящий доступ с ноды на 443 наружу: Reality проксирует каждый неопознанный хендшейк на донора, и если egress режет хостер, вы получите работающий туннель у «своих» и висящее соединение у всех остальных — то есть провал теста на пробинг.
Если нода живёт под панелью (Remnawave, 3x-ui, Marzban), править config.json руками бессмысленно: панель генерирует тот же JSON и перезаписывает файл при рестарте. Поля ниже вводятся в её UI один в один; про сами панели у нас отдельные материалы, здесь — чистый Xray.
Шаг 1. Три секрета: xray x25519, UUID, shortId
Пара ключей X25519 генерируется самим ядром:
xray x25519
Вывод отличается между версиями, и это регулярный источник путаницы:
# Xray-core до 25.x:
Private key: iJ2h... -> в конфиг сервера, поле privateKey
Public key: 6Dpm... -> клиенту, поле publicKey / pbk
# Xray-core 25.x:
PrivateKey: iJ2h...
Password: 6Dpm... <- это и есть публичный ключ, идёт клиенту в pbk
Слово Password пугает новичков — это не пароль пользователя, а тот же публичный ключ. Оба значения — base64url длиной 43 символа; если в поле pbk оказалась строка другой длины, вы почти наверняка скопировали не ту строку. Публичный ключ выводится из приватного (xray x25519 -i <PRIVATE_KEY>), но имя флага между сборками менялось — сверьтесь с xray help x25519, а не с чужим гайдом.
В свежих сборках есть и постквантовый вариант (xray mlkem768). Он работает, но по наблюдениям мобильные клиенты подтягивают поддержку неравномерно, и «на ПК работает, в телефоне нет» получается на ровном месте. Для прода берите классический x25519.
Остальные два секрета:
xray uuid # UUID клиента
openssl rand -hex 8 # shortId: 16 hex-символов (8 байт), максимум для поля
Таблица соответствия сервер ↔ клиент — по ней потом чинится большинство ошибок:
Сервер, realitySettings / clients | Клиент (JSON / параметр ссылки) | Формат |
|---|---|---|
privateKey | нигде, никогда не отдавать | base64url, 43 символа |
публичный ключ (Public key / Password) | publicKey / pbk | base64url, 43 символа |
serverNames[] | serverName / sni | домен донора |
dest (target) | — | host:443 |
shortIds[] | shortId / sid | hex, чётная длина, до 16 символов |
clients[].id | id | UUID |
clients[].flow | flow | xtls-rprx-vision |
| — | fingerprint / fp | chrome |
Сложите значения в файл, чтобы не выковыривать их потом из истории шелла:
cat > /root/reality.env <<'EOF'
UUID=00000000-0000-0000-0000-000000000000
PRIV=iJ2h...
PBK=6Dpm...
SID=a1b2c3d4e5f60718
SNI=www.samsung.com
EOF
chmod 600 /root/reality.env
Шаг 2. SNI-донор: процедура проверки за 10 секунд
Критерии выбора донора мы разбирали в материалах про Reality и SNI-маскировку, здесь — только процедура проверки перед боем. Худшее, что можно сделать, — взять домен из чужого туториала не глядя (включая этот: www.samsung.com ниже — плейсхолдер, а не рекомендация).
D=www.samsung.com
openssl s_client -connect $D:443 -servername $D -tls1_3 -alpn h2 </dev/null 2>/dev/null \
| grep -E "Protocol|Server Temp Key|ALPN protocol|issuer"
Годный кандидат выглядит так:
ALPN protocol: h2
Protocol: TLSv1.3
Server Temp Key: X25519, 253 bits
Три жёстких условия: TLS 1.3, X25519 в Server Temp Key (увидели ECDH, P-256 — кандидат не подходит) и h2 в ALPN. Прогнать список пачкой:
for D in www.samsung.com www.lovelive-anime.jp cdn.jsdelivr.net www.icloud.com; do
R=$(openssl s_client -connect $D:443 -servername $D -tls1_3 -alpn h2 </dev/null 2>/dev/null \
| grep -E "Protocol:|Server Temp Key|ALPN protocol" | tr '\n' ' ')
printf "%-28s %s\n" "$D" "${R:-FAIL}"
done
У Xray есть и собственная проверка: xray tls ping www.samsung.com — показывает цепочку сертификатов глазами ядра.
Два момента, о которых забывают. Первый: запускать проверку надо с самого сервера. Часть сайтов и часть хостеров режут доступ по географии — из Москвы всё зелёно, а с VPS во Франкфурте таймаут, и Reality на таком доноре будет молча ронять чужие соединения. Второй: это не «настроил и забыл». Сайты переезжают за общий CDN и меняют TLS-профиль без предупреждения, поэтому держите двух-трёх проверенных запасных и умейте переключить dest и serverNames за минуту — вместе с рассылкой новой подписки клиентам.
Шаг 3. Xray reality config: конфиг сервера целиком
Кладём в /usr/local/etc/xray/config.json. Это полный рабочий файл, а не фрагмент — подставьте свои значения из /root/reality.env.
{
"log": {
"loglevel": "warning",
"access": "/var/log/xray/access.log",
"error": "/var/log/xray/error.log"
},
"inbounds": [
{
"tag": "vless-reality",
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "00000000-0000-0000-0000-000000000000",
"email": "user1@node1",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"show": false,
"dest": "www.samsung.com:443",
"xver": 0,
"serverNames": ["www.samsung.com"],
"privateKey": "iJ2h...",
"shortIds": ["a1b2c3d4e5f60718"]
}
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"],
"routeOnly": true
}
}
],
"outbounds": [
{ "tag": "direct", "protocol": "freedom" },
{ "tag": "block", "protocol": "blackhole" }
],
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{ "type": "field", "ip": ["geoip:private"], "outboundTag": "block" },
{ "type": "field", "protocol": ["bittorrent"], "outboundTag": "block" }
]
}
}
Что здесь неочевидно:
emailвclients[]— не почта, а метка вaccess.log. Без неё вы не поймёте, чей аккаунт качает трафик и чей ключ разошёлся по чатам:shortIds— плоский список, он не привязан к конкретному клиенту, и по логу sid не читается.shortIdsбез пустой строки. Пустой элемент""разрешает подключение вообще безsid— удобно на отладке, вредно в проде.destв свежих сборках называетсяtarget, старое имя работает как алиас. Если конфиг переезжает между версиями ядра — оставляйтеdest, его понимают обе.geoip:privateвblockзакрывает клиентам путь во внутреннюю сеть хостера. Без этого правила из вашего же туннеля видно соседей по подсети и метадата-сервис облака.routeOnly: trueв sniffing: домен берётся только для маршрутизации и не подменяет адрес назначения — меньше сюрпризов с сайтами за CDN.xver: 0— PROXY protocol выключен; включать только если перед Xray стоит свой фронт-прокси.
Применяем и проверяем синтаксис, а не «рестарт и посмотрим»:
xray run -test -c /usr/local/etc/xray/config.json # ждём: Configuration OK
systemctl restart xray && systemctl --no-pager status xray
journalctl -u xray -n 30 --no-pager
address already in use означает, что на 443 уже сидит nginx или панель: разводите их по портам или по разным IP. Повесить Reality на нестандартный порт технически можно, но визит «на samsung.com:8443» выглядит аномально и первым попадает под эвристики.
Шаг 4. Клиент: полный JSON и ссылка vless://
Клиент повторяет серверные значения дословно. Конфиг для десктопного Xray — SOCKS на 10808, HTTP на 10809:
{
"log": { "loglevel": "warning" },
"inbounds": [
{
"tag": "socks",
"listen": "127.0.0.1",
"port": 10808,
"protocol": "socks",
"settings": { "udp": true },
"sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"] }
},
{
"tag": "http",
"listen": "127.0.0.1",
"port": 10809,
"protocol": "http"
}
],
"outbounds": [
{
"tag": "proxy",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "203.0.113.10",
"port": 443,
"users": [
{
"id": "00000000-0000-0000-0000-000000000000",
"encryption": "none",
"flow": "xtls-rprx-vision"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"serverName": "www.samsung.com",
"fingerprint": "chrome",
"publicKey": "6Dpm...",
"shortId": "a1b2c3d4e5f60718",
"spiderX": "/"
}
}
},
{ "tag": "direct", "protocol": "freedom" }
],
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{ "type": "field", "ip": ["geoip:private", "geoip:ru"], "outboundTag": "direct" }
]
}
}
Правило с geoip:ru пускает российские адреса мимо туннеля: меньше расход трафика на ноде и меньше жалоб на банковские приложения. Доменные списки (geosite:*) намеренно не добавлены — их состав зависит от того, чей geosite.dat лежит рядом с бинарём, универсального совета тут нет.
Та же конфигурация одной ссылкой — для Happ, v2rayNG, Streisand, Nekobox и подписок:
vless://[email protected]:443?type=tcp&security=reality&encryption=none&flow=xtls-rprx-vision&sni=www.samsung.com&fp=chrome&pbk=6Dpm...&sid=a1b2c3d4e5f60718&spx=%2F#reality-node1
Две детали: spx=%2F — слэш обязан быть URL-кодирован, иначе часть парсеров обрежет строку; текст после # — только метка в списке серверов, на подключение не влияет.
И предупреждение из практики: не досыпайте в ссылку поля «на всякий случай» — ручной alpn, экзотические параметры обфускации, хвосты от чужих конфигов. Набор полей Reality менялся между версиями ядра, и поле, безобидное у вас на ПК, у части пользователей превращается в «конфиг не импортируется» или молчаливый обрыв. Восьми параметров выше достаточно. Отдельно запомните: flow: xtls-rprx-vision действует только для TCP+TLS/Reality — если этот же пользователь получит второй профиль поверх XHTTP, там flow должен быть пустым.
Шаг 5. Проверка: пять тестов, включая активный пробинг
«В клиенте загорелось зелёное» — не проверка. Прогоните по порядку, каждый тест отсекает свой класс проблем.
| # | Что проверяем | Команда | Что должно быть | |
|---|---|---|---|---|
| 1 | Синтаксис конфига | xray run -test -c /usr/local/etc/xray/config.json | Configuration OK | |
| 2 | Порт слушается | `ss -lntp \ | grep ':443'` | строка с xray |
| 3 | Донор доступен с ноды | `curl -sI --max-time 5 https://www.samsung.com \ | head -1` | HTTP/2 200 или редирект |
| 4 | Маскировка (пробинг) | см. ниже | сертификат и ответ настоящего донора | |
| 5 | Туннель работает | curl -x socks5h://127.0.0.1:10808 -s https://api.ipify.org | IP вашего сервера |
Четвёртый тест — главный и самый недооценённый. Запускайте его не с сервера, а с посторонней машины: вы имитируете ровно то, что делает цензор, стучась на ваш IP.
IP=203.0.113.10; D=www.samsung.com
# Что увидит активный пробинг: должен вернуться реальный ответ донора
curl -sI --resolve $D:443:$IP https://$D/ | head -3
# Чей сертификат отдаёт ваш сервер
openssl s_client -connect $IP:443 -servername $D </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer
В выводе должны быть subject и issuer настоящего донора — CN=www.samsung.com и его удостоверяющий центр. Нет сертификата, соединение рвётся или висит — либо dest недоступен с сервера (egress-файрвол хостера, см. шаг 0), либо в serverNames нет того имени, что вы подставили в --resolve.
Пятый тест дополните измерением и негативной проверкой:
curl -x socks5h://127.0.0.1:10808 -s https://ifconfig.co/json
curl -x socks5h://127.0.0.1:10808 -o /dev/null -s -w "%{speed_download}\n" \
https://speed.cloudflare.com/__down?bytes=25000000
Теперь испортите один символ в publicKey на клиенте и переподключитесь: соединение подняться не должно. Если оно всё равно работает — вы правите файл, который ядро не читает (типовая ситуация на ноде под панелью), и все предыдущие тесты ничего не доказывают. На сервере в этот момент при "show": true в логе появится запись об отклонённом хендшейке; после отладки верните false, иначе лог забьётся мусором от чужих сканеров.
Частые ошибки, эксплуатация и граница Reality
Сводка по симптомам, по убыванию частоты в обращениях операторов:
| Симптом | Причина | Лечение |
|---|---|---|
| Клиент «подключен», сайты не открываются | flow: xtls-rprx-vision прописан только на одной стороне | выставить одинаково у клиента и в clients[] |
| Обрыв сразу после хендшейка | sni вне serverNames, sid не из shortIds или клиенту отдали privateKey вместо pbk | сверить четыре поля по таблице из шага 1 |
| Всё сломалось после смены донора | serverNames поменяли, у клиентов остался старый sni | менять на обеих сторонах, через перевыпуск подписки |
| Работает на ПК, не работает на телефоне | пустой fp, устаревший клиент, лишние поля в ссылке | fp=chrome, убрать всё сверх восьми полей, обновить клиент |
Xray не стартует: address already in use | на 443 висит nginx или панель | развести по портам или по IP |
| Плавающие разрывы у части клиентов | ушли часы сервера | timedatectl set-ntp true |
| Пробинг стал видеть ошибку TLS | донор уехал за CDN или отключил TLS 1.3/X25519 | перепроверить openssl s_client, переключиться на запасного |
Configuration OK, но снаружи порт закрыт | security group хостера, а не ufw | открыть 443/tcp в панели хостера |
| Правки в конфиге не применяются | нода под панелью, файл перезаписывается | править в UI панели |
Про эксплуатацию. Приватный ключ меняют только при компрометации: его ротация — это обновление pbk у всех клиентов сразу, то есть перевыпуск всей подписки. Точечный отзыв дешевле делать через UUID: удалили запись из clients[], перезапустили ядро — остальные не пострадали. С shortIds так не выйдет: список общий и не привязан к пользователю, удаление значения отрубит всех, кто его использует. Если хотите per-user sid — ведите сопоставление sid→пользователь у себя, ядро вам его не покажет. Смена донора ключей не касается вообще.
И честная граница. Есть класс симптомов, который не чинится ни одной строкой таблицы: конфиг верный, пробинг проходит, с домашнего интернета летает — а с мобильного оператора не подключается никто. Это не ошибка настройки. Reality маскирует что вы делаете, но не меняет откуда: клиент по-прежнему идёт напрямую на IP ноды, и если дата-центровый адрес режется по IP или не входит в белый список оператора, маскировать уже нечего.
Лечится это точкой входа, а не транспортом: перед нодой ставится фронт на «белом» IP (идея whitelist-CDN вроде Clearway), и клиент подключается к CDN. Транспорт там уже другой — XHTTP, и ломается он обычно в двух местах. Первое: у Yandex CDN нет POST, поэтому аплинк идёт через GET, а GET разрешён только при явно заданном mode: packet-up — без него получается классическое «в одном приложении работает, в другом нет». Второе: поле extra на ноде и в клиенте должно совпадать символ в символ. Подробности — в материалах про XHTTP и панели за CDN. Практичная схема для оператора: Reality основным профилем, CDN-фронт резервным, оба в одной подписке.