Reality или XHTTP: разные уровни атаки
Запрос «xray reality xhttp» почти всегда приходит с неверной посылкой: что это два способа спрятать VPN и надо выбрать лучший. Они закрывают разные уровни.
Reality работает на уровне TLS-хендшейка. Нода подставляет сертификат чужого живого сайта (target/dest), и активный пробинг со стороны ТСПУ упирается в настоящий сайт: без валидного shortId и ключа Xray молча проксирует проверяющего на донора. По SNI, сертификату, JA3 и реакции на пробинг инбаунд неотличим от обычного HTTPS. Настройка донора, serverNames, flow: xtls-rprx-vision — тема отдельного материала про Reality, здесь не повторяем.
XHTTP (в старых сборках SplitHTTP) работает на уровне транспорта: укладывает VLESS-поток в обычные HTTP-запросы — даунлинк длинным стриминговым ответом, аплинк серией запросов. Сам по себе против DPI он даёт немного. Ценность появляется, когда перед ним стоит CDN: клиент подключается к edge-узлу, и вся видимая фильтру часть соединения — TLS до белого домена на белом IP.
Отсюда формулировка, которую стоит запомнить: Reality скрывает, что внутри соединения; CDN скрывает, куда идёт соединение. Блокировка по IP срабатывает до того, как дело дойдёт до содержимого, — значит, в этом сценарии Reality не помогает не из-за плохой настройки, а по построению. Общая картина «что вообще выживает под whitelist-режимом», включая Hysteria2, разбиралась отдельно; здесь — только выбор между двумя транспортами и гибрид.
Диагностика за пять минут: IP или протокол
Прежде чем менять что-то на проде, надо понять, что именно сломалось. Проверка делается с двух сторон одновременно: с клиента в зоне ограничений (телефон в режиме модема на мобильном операторе — самый показательный стенд) и с самой ноды.
С клиента:
ping -c 4 <IP_ноды>
nc -vz <IP_ноды> 443
curl -vk --connect-timeout 5 https://<IP_ноды>:443
mtr -rwc 20 <IP_ноды>
Одновременно на ноде:
sudo tcpdump -ni any 'tcp port 443 and tcp[tcpflags] & tcp-syn != 0'
Читаем результат:
| Что видим | Диагноз | Что делать |
|---|---|---|
SYN до ноды не долетает, mtr обрывается на транзите оператора | Режут по IP/подсети | Reality не поможет. Нужен белый фронт или смена IP |
| SYN приходит, SYN-ACK уходит, у клиента таймаут | Фильтр на обратном пути, тот же IP-блок | То же самое |
| TLS-хендшейк проходит, сессия живёт 5–30 сек и рвётся | DPI по протоколу/поведению | Reality, смена донора, flow: xtls-rprx-vision |
| Работает с зарубежного VPS, не работает с РФ-оператора | Фильтр на стороне РФ, нода исправна | Смотрим первые две строки |
| Не работает у одного оператора, работает у трёх | Локальный режим ТСПУ | Белый фронт для пострадавших |
Частый пропускаемый маркер: IP отвечает на пинг, но 443 глухой — это не «сложный DPI», а фильтр по паре IP+порт. Перевесить Reality на 8443 иногда даёт ремиссию на несколько дней, но это отсрочка, а не решение.
Честная оговорка: по наблюдениям, при жёстких шатдаунах и whitelist-режимах на «сером» IP не выживает ни один транспорт — ни Reality, ни Shadowsocks-2022, ни голый WireGuard. Вопрос не в маскировке: пакет физически не выпускают за пределы разрешённого списка адресов.
Что реально меняется в конфиге при переходе на XHTTP за CDN
Полное руководство по XHTTP за CDN — в отдельной статье; здесь только дельта относительно привычного Reality-инбаунда, потому что именно на ней ломаются.
Путь пакета: клиент → TLS до cdn.example.ru (белый IP edge) → CDN проксирует на origin → nginx на ноде → Xray. TLS терминируется на edge, поэтому инбаунд слушает локально и без своего TLS:
{
"tag": "vless-xhttp",
"listen": "127.0.0.1",
"port": 2087,
"protocol": "vless",
"settings": { "clients": [{ "id": "UUID" }], "decryption": "none" },
"streamSettings": {
"network": "xhttp",
"security": "none",
"xhttpSettings": {
"host": "cdn.example.ru",
"path": "/xh-braid",
"mode": "packet-up"
}
}
}
Ссылка клиенту (одной строкой):
vless://[email protected]:443?encryption=none&security=tls&sni=cdn.example.ru&type=xhttp&host=cdn.example.ru&path=%2Fxh-braid&mode=packet-up#node-cdn
Четыре пункта, которые дают 90% инцидентов:
modeзадаётся явно, с обеих сторон. У Yandex CDN нет проксирования POST, поэтому аплинк уходит GET-запросами, а GET в качестве аплинка допустим только в режимеpacket-up. Оставленныйmode: autoдаёт классику «в одном приложении работает, в другом нет»: клиент, выбравший stream-up, получает живой коннект и ноль трафика.security: noneна локальном порту. Оставленныйrealityв xhttp-инбаунде несовместим с терминацией TLS на edge.extraидентичен на ноде и в клиенте — не «похож», а посимвольно совпадает. И чем он короче, тем лучше: экзотические obfuscation-поля переименовываются между версиями Xray-core, и конфиг под 25.x у клиента на 24.x просто не поднимется. Размещение uplink-данных (тело запроса против кастомного заголовка) разбиралось отдельно — это ровно тот случай, когда CDN режет заголовки и туннель рвётся.- Буферизация на origin выключена. Без этого даунлинк-стрим копится в nginx и соединение выглядит как «висит и не отдаёт»:
location /xh-braid {
proxy_pass http://127.0.0.1:2087;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
На стороне CDN для этого path кэширование отключается обязательно: иначе edge закэширует первый ответ стрима и раздаст его всем следующим.
Сравнение и цена вопроса
| Параметр | Reality | XHTTP через белый CDN |
|---|---|---|
| От чего спасает | DPI по TLS, активный пробинг, SNI-фильтр | Блокировка по IP, whitelist-режим ТСПУ |
| Спасает при блоке IP | Нет | Да |
| Latency | +0 к сети до ноды | +20–60 мс на плечо через edge (по наблюдениям) |
| Стоимость трафика | 0, только канал VPS | ~0.6–3 ₽/ГБ в зависимости от CDN |
| Overhead | Минимальный | ~5–15% на паддинг и HTTP-заголовки |
| CPU ноды и origin | Низкий | Выше: поток мелких запросов, TLS дважды |
| Эксплуатация | Одна нода, один конфиг | Нода + origin-TLS + настройки CDN + учёт трафика |
Про деньги стоит считать заранее, а не по факту счёта. XHTTP в режиме packet-up — это не «один поток», а поток запросов: на гигабайт трафика их приходится на порядки больше, чем у обычной статики, ради которой CDN и строились. У CDN, где запросы тарифицируются отдельно, это заметная часть чека, и она же объясняет рост CPU на origin.
Порядок величин на живой ноде: 600 активных подписок, по наблюдениям 20–40 ГБ в месяц на активного пользователя — это 12–24 ТБ. Полный перевод такого объёма на CDN по 1.5 ₽/ГБ — это 18–36 тысяч рублей в месяц, включая ютуб ваших пользователей. Отсюда здоровая схема не «переехать на CDN», а держать CDN как второй маршрут для тех регионов и операторов, где прямой IP уже не проходит.
И ограничение, которое экономит часы экспериментов: Reality за CDN не существует. CDN терминирует TLS у себя, Reality требует сквозного TLS до вашей ноды — вещи взаимоисключающие. Любая инструкция вида «поставьте Reality за Cloudflare» технически невозможна.
Что выбрать: четыре сценария и гибридная схема
Практический выбор «что выбрать reality xhttp» сводится к четырём ситуациям.
- IP живой, жалобы единичные, деградация по вечерам. Reality,
flow: xtls-rprx-vision, порт 443. XHTTP тут добавит только задержек и счетов. - Часть операторов отвалилась, часть работает. Локальный фильтр. Reality остаётся основным, XHTTP-через-CDN добавляется вторым хостом в подписку для пострадавших.
- Нода мертва у всех РФ-операторов, из-за рубежа работает. IP в блоке. Только CDN-фронт. Переезд на новый «серый» IP по наблюдениям даёт недели, а не месяцы.
- Регион в whitelist-режиме. Только CDN на подтверждённо белом домене, причём проверять надо каждую
/24: у одного хостера часть подсетей в списке, часть нет, и состав меняется.
Гибрид собирают заранее, а не в момент шатдауна. Два инбаунда на одной ноде:
"inbounds": [
{ "tag": "vless-reality", "port": 443, /*... reality... */ },
{ "tag": "vless-xhttp", "listen": "127.0.0.1", "port": 2087 /*... xhttp... */ }
]
В подписке отдаются оба хоста с говорящими именами (RU-fast, RU-backup-cdn), Reality первым: пользователь переключится сам, когда основной перестанет ходить. В Remnawave, Marzban и 3x-ui это два хоста на профиль — механика описана в материалах про панели за CDN.
Проверка перед раздачей клиентам:
ss -lntp | grep -E '443|2087'
journalctl -u xray -n 50 --no-pager | grep -iE 'failed|rejected'
curl -s -o /dev/null -w '%{http_code} %{time_total}\n' https://cdn.example.ru/xh-braid
Последняя команда на живом XHTTP-пути не должна отдавать быстрый 404: мгновенный 404 означает, что CDN не пробрасывает этот path на origin, а нормальная реакция — соединение, которое держится до таймаута.
Частые ошибки при переезде с Reality на XHTTP
Из типовых разборов «всё сделал по гайду, но не работает»:
mode: autoвместо явногоpacket-up. Работает у части клиентов, у остальных коннект без трафика.extraразошёлся между нодой и клиентом. Хендшейк проходит, данные не идут или рвутся на больших ответах.- CDN кэширует
path. Первый пользователь работает, остальные получают пустое тело или чужой ответ. - Origin отдаёт HTTP там, где CDN требует валидный TLS до источника. Проверка цепочки:
openssl s_client -connect <IP_ноды>:8443 -servername cdn.example.ru </dev/null | head -20. - Разные версии Xray-core на ноде и в приложении. XHTTP меняется от версии к версии; при разборе инцидента первым делом сверяйте
xray versionи версию ядра в клиенте. - Подписка закэширована на клиенте. Особенно на iOS: конфиг обновили, приложение показывает старый. Отдавайте подписку с
Cache-Control: no-store, в тяжёлых случаях — переподключение подписки заново.
И обязательное для прода: копия конфига до правки инбаундов и готовый откат.
cp /usr/local/etc/xray/config.json /root/xray-config.$(date +%F-%H%M).json
xray -test -config /usr/local/etc/xray/config.json && systemctl restart xray
# откат:
# cp /root/xray-config.<метка>.json /usr/local/etc/xray/config.json && systemctl restart xray