Runbook · Remnawave

Нода XHTTP SelfSteal

От чистого VPS до работающей ноды с VLESS + XHTTP + Reality, где Reality маскируется под собственный сайт на том же сервере. Порядок фаз обязателен: каждая заканчивается проверкой, без которой следующая не имеет смысла.

Xray 25.x Caddy 2 Let's Encrypt Docker

← Все руководства · Хост с автовыбором · Нода-мост

Значения этой ноды

Заполни один раз — команды и конфиги ниже подставят их автоматически. Сохраняется в браузере, для каждой ноды свой набор.

Ключи и shortId сюда не вводи. Приватный ключ Reality — секрет, которому нечего делать в браузерной странице. Он подставляется вручную в шаблон панели, на своём месте в фазе 09.

01

Сервер

Нода и панель — разные машины. В панели Remnawave нет Xray, весь трафик обрабатывает нода, поэтому под неё нужен отдельный VPS.

ПараметрМинимумКомментарий
CPU1 vCPUXray почти не грузит процессор; упор идёт в сеть
RAM1 ГБс 1 ГБ нужен swap, с 2 ГБ спокойнее
Диск10 ГБобразы Docker плюс логи
СетьIPv4обязателен — под A-запись и ACME
ОСUbuntu 24.04 / Debian 12чистая установка, без панелей вроде aaPanel

На что смотреть при выборе хостера

Заведи DNS-запись прямо сейчас, не дожидаясь фазы 04. Распространение занимает от минут до часа, и к моменту, когда она понадобится для сертификата, запись будет уже готова.

02

Базовая настройка

Доступ по ключу

Выполняется на своей машине, не на сервере:

локально, если ключа ещё нет
ssh-keygen -t ed25519 -C "remnawave-node"
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@IP_СЕРВЕРА

Убедись, что вход по ключу работает, не закрывая текущую сессию. Только после этого отключай пароли — на сервере:

на сервере
sed -i 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config
grep -rn "PasswordAuthentication" /etc/ssh/sshd_config.d/ 2>/dev/null
systemctl restart ssh
Почему вторая команда важна

Облачные образы часто кладут PasswordAuthentication yes в отдельный файл внутри /etc/ssh/sshd_config.d/, и он переопределяет основной конфиг. Правка одного sshd_config тогда ничего не даёт, а ты будешь считать, что пароли отключены. Если grep что-то нашёл — правь и там.

Фаервол

на сервере
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
ufw status numbered

Порт ноды для связи с панелью откроем в фазе 03 — точечно, не для всех.

Swap, если памяти 1 ГБ

на сервере
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
free -h

Docker

на сервере
curl -fsSL https://get.docker.com | sh
docker --version
docker compose version
Шлюз фазы
  • вход по ключу работает, пароли отключены и в основном конфиге, и в drop-in
  • ufw status показывает SSH, 80 и 443
  • docker compose version отвечает без ошибок
03

Нода Remnawave

Панель сама генерирует compose-файл для ноды вместе с сертификатом — вручную ничего собирать не нужно.

1 · Создать запись в панели

Панель → Nodes → Management → кнопка добавления. Заполни:

Проверь, не занят ли 2222

Часть гайдов советует переносить SSH на порт 2222. Если ты так сделал — возьми для NODE_PORT другой номер, иначе нода и SSH схлестнутся за один порт.

2 · Развернуть на сервере

Панель покажет готовый docker-compose.yml. Скопируй его целиком и вставь в редактор на сервере:

на сервере
mkdir -p /opt/remnanode && cd /opt/remnanode
nano docker-compose.yml

Вставь сгенерированное панелью, сохрани Ctrl+O → Enter → Ctrl+X, затем:

на сервере
docker compose config
docker compose up -d
docker compose logs -f -t

docker compose config перед запуском — привычка, которая экономит время: если при вставке в файл попала лишняя строка, ошибка вылезет сразу, а не в виде молчащего контейнера.

Содержимое этого файла — секрет

В сгенерированном compose лежит SSL_CERT — учётные данные, которыми панель авторизуется на этой ноде. Не выкладывай файл и не пересылай его целиком, когда просишь помощи с конфигом.

3 · Привязать профиль

Вернись к карточке создания ноды в панели, нажми Next, выбери Config Profile и заверши созданием. После этого панель начнёт отдавать ноде конфиг Xray — тот самый, который мы заменим в фазе 09.

4 · Закрыть порт API

Порт ноды должен быть доступен только панели:

на сервере · подставь IP панели
ufw allow from IP_ПАНЕЛИ to any port 2222 proto tcp
ufw status numbered
Не открывай порт всем

ufw allow 2222 без from выставляет API ноды в интернет. Правило обязано быть с указанием IP панели.

Шлюз фазы
  • docker ps показывает remnanode в статусе Up
  • в панели нода отмечена как подключённая
  • ss -tlnp | grep :443 показывает rw-core — значит конфиг доехал и Xray слушает
  • в ufw status порт API открыт только для IP панели
04

Домен и DNS

Если завёл запись ещё на фазе 01 — здесь остаётся только проверить. Каждой ноде нужен свой поддомен — A-запись указывает на конкретный IP, поэтому один домен на несколько нод не подойдёт.

проверка
dig +short мой.домен
Шлюз фазы

Возвращается ровно IP этой ноды и ничего больше. Если пусто — запись не разошлась, подожди. Если IP чужой — включено проксирование Cloudflare.

05

Порты

ПортКто занимаетДоступЗачем
22sshdснаружидоступ по ключу, пароли отключены в фазе 02
80CaddyснаружиACME-челлендж и редирект на HTTPS
443XrayснаружиReality, основной вход
2222Нодатолько IP панеливнутренний API, закрыт правилом из фазы 03
9443Caddyтолько 127.0.0.1сайт-прикрытие, наружу не торчит
проверка занятости
ss -tlnp | grep -E ':(80|443|9443)'
ufw status

На 443 должен быть rw-core — это Xray внутри контейнера remnanode. Порты 80 и 9443 обязаны быть свободны. Если 80 закрыт фаерволом:

при активном ufw
ufw allow 80/tcp
Частая причина провала

У многих хостеров есть второй фаервол в панели управления, независимый от ufw. Если ACME не проходит при открытом ufw — смотри туда.

06

Сайт-прикрытие

Это то, что увидит проверяющий. Требование одно, но жёсткое: страница должна выглядеть как настоящий маленький сайт.

минимальная заглушка, если шаблона ещё нет
mkdir -p /var/www/site
echo '<!doctype html><html><head><title>Studio</title></head><body><h1>Studio</h1></body></html>' \
  > /var/www/site/index.html

Заглушка годится только чтобы довести сборку до конца. Перед тем как пускать пользователей — замени на настоящий шаблон и положи favicon.ico: без него браузер проверяющего получит 404 на первом же запросе.

07

Caddy

Caddy раздаёт сайт по HTTPS на 127.0.0.1:9443 и сам получает сертификат Let's Encrypt через порт 80. Xray будет отправлять сюда всё, что не прошло авторизацию Reality.

Caddyfile

Создавай через редактор, не через heredoc: незакрытый heredoc молча съедает следующие команды как текст.

открыть редактор
mkdir -p /opt/selfsteal && nano /opt/selfsteal/Caddyfile
/opt/selfsteal/Caddyfile — вставить, сохранить Ctrl+O → Enter → Ctrl+X
{
	admin off
	https_port 9443
	auto_https disable_redirects
	servers {
		protocols h1 h2
	}
}

http://мой.домен {
	redir https://{host}{uri} permanent
}

https://мой.домен {
	bind 127.0.0.1
	root * /var/www/site
	file_server
	encode gzip

	tls {
		issuer acme {
			disable_tlsalpn_challenge
		}
	}
}

Две строки здесь неочевидны и обе обязательны:

Проверка синтаксиса и запуск

валидация
head -3 /opt/selfsteal/Caddyfile
docker run --rm -v /opt/selfsteal/Caddyfile:/etc/caddy/Caddyfile:ro \
  caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile

Первая строка файла — ровно {. Валидатор отвечает Valid configuration.

запуск
docker run -d \
  --name selfsteal-caddy \
  --restart always \
  --network host \
  -v /opt/selfsteal/Caddyfile:/etc/caddy/Caddyfile:ro \
  -v /var/www/site:/var/www/site:ro \
  -v caddy_data:/data \
  -v caddy_config:/config \
  caddy:2-alpine

docker logs -f selfsteal-caddy

Пути на хосте и в контейнере совпадают намеренно — в Caddyfile указан тот же root, путать нечего. Жди в логах certificate obtained successfully, затем Ctrl+C.

проверка
ss -tlnp | grep -E ':(80|9443)'
curl -sI --resolve мой.домен:9443:127.0.0.1 \
  https://мой.домен:9443
Шлюз фазы
  • caddy слушает *:80 и 127.0.0.1:9443
  • curl отвечает HTTP/2 200
  • заголовка alt-svc в ответе нет
08

Ключи

пара ключей Reality
docker run --rm ghcr.io/xtls/xray-core:latest x25519
два shortId и path
openssl rand -hex 8
openssl rand -hex 8
openssl rand -hex 8
ЗначениеКуда идёт
Private keyшаблон Xray, поле privateKey
Public keyхост в панели, поле Public Key
shortId ×2шаблон Xray; в хосте указывается один любой из них
pathшаблон Xray и хост — должны совпадать точно
Не оставляй пустой shortId

shortIds: [""] принимает любого, кто знает публичный ключ. Генерируй настоящие.

Path в шаблоне стоит заглушкой /Pathобязательно замени на свой и пиши со слешем. Оставишь как есть — путь будет одинаковым у всех, кто брал этот runbook, а это ровно тот признак, от которого мы тут избавляемся.

09

Шаблон Xray

На новой ноде здесь стоит профиль, который панель привязала в фазе 03 — его и заменяем. Если нода уже работала с пользователями, сначала сохрани текущий шаблон в файл: это твой откат.

Про тег инбаунда. На новой ноде поставь любое понятное имя — например VLESS_XHTTP_Reality. На работающей оставь тот тег, что уже используется, иначе привязки хостов в панели слетят и инбаунд придётся выбирать заново в каждом хосте. На работу имя не влияет ни в том, ни в другом случае.

шаблон Xray — заменить плейсхолдеры в верхнем регистре
{
  "log": { "loglevel": "warning" },
  "inbounds": [
    {
      "tag": "ТЕКУЩИЙ_ТЕГ_ИНБАУНДА",
      "listen": "0.0.0.0",
      "port": 443,
      "protocol": "vless",
      "settings": {
        "clients": [],
        "decryption": "none"
      },
      "streamSettings": {
        "network": "xhttp",
        "security": "reality",
        "realitySettings": {
          "show": false,
          "target": "127.0.0.1:9443",
          "xver": 0,
          "serverNames": ["мой.домен"],
          "privateKey": "ПРИВАТНЫЙ_КЛЮЧ",
          "shortIds": ["SHORTID_1", "SHORTID_2"]
        },
        "xhttpSettings": {
          "host": "мой.домен",
          "path": "/Path",
          "mode": "auto"
        }
      },
      "sniffing": {
        "enabled": true,
        "destOverride": ["http", "tls", "quic"]
      }
    }
  ],
  "outbounds": [
    {
      "tag": "DIRECT",
      "protocol": "freedom",
      "settings": { "domainStrategy": "UseIPv4" }
    },
    {
      "tag": "BLOCK",
      "protocol": "blackhole"
    }
  ],
  "dns": {
    "servers": ["94.140.14.14", "94.140.15.15", "1.1.1.1"],
    "queryStrategy": "UseIPv4",
    "disableCache": false
  },
  "routing": {
    "domainStrategy": "IPIfNonMatch",
    "rules": [
      { "ip": ["0.0.0.0/8", "::/128"], "outboundTag": "BLOCK" },
      { "ip": ["geoip:private"], "outboundTag": "BLOCK" },
      { "domain": ["geosite:private"], "outboundTag": "BLOCK" },
      { "protocol": ["bittorrent"], "outboundTag": "BLOCK" }
    ]
  }
}

Что здесь делает каждый нестандартный кусок

ПолеНазначение
target: 127.0.0.1:9443SelfSteal — непрошедшие авторизацию уходят на локальный Caddy, а не на чужой сайт
network: xhttpтранспорт внутри туннеля; с realitySettings не конфликтует, это разные слои
mode: autoс Reality клиент сам выберет stream-one; сервер принимает все режимы
xver: 0Caddy видит соединения с 127.0.0.1. Значение 1 требует приёма PROXY-протокола в Caddyfile — рассинхрон ломает всё
domainStrategy: IPIfNonMatchрезолвит домен вторым проходом, чтобы поймать 0.0.0.0 от AdGuard; заодно закрывает SSRF в приватные сети
0.0.0.0/8 → BLOCKAdGuard отвечает 0.0.0.0 на рекламу, а connect() к нему на Linux уходит на 127.0.0.1 — то есть обратно в свой же инбаунд
freedom: UseIPv4заставляет исходящий резолвить через внутренний DNS; без этого блок dns не работает

Про блокировку рекламы. Фильтрация целиком на стороне резолвера. AdGuard отвечает 0.0.0.0 на рекламные домены, IPIfNonMatch вторым проходом резолвит адрес, правило 0.0.0.0/8 отправляет соединение в blackhole. Локальных списков нет — обновлять и поддерживать нечего, а база AdGuard свежее любого geosite.

Цена такого решения — единственная точка отказа. 1.1.1.1 третьим в списке страхует от недоступности AdGuard, но в момент переключения на него фильтрация выключается целиком и молча. Раз в месяц проверяй, что фильтр вообще жив:

dig +short @94.140.14.14 doubleclick.net — ожидается 0.0.0.0. Пусто или таймаут означают, что реклама прямо сейчас не режется. Альтернатива — убрать 1.1.1.1 совсем: тогда отказ AdGuard уронит интернет всем пользователям, зато незаметной потери фильтрации не будет.

10

Хост в панели

ПолеЗначение
Network / Transportxhttp
Pathтот же, что в шаблоне
Hostдомен ноды
Securityreality
SNIдомен ноды
Public Keyиз вывода x25519
Short IDодин из двух
Fingerprintchrome
Flowпусто
Ошибка номер один при переходе на XHTTP

xtls-rprx-vision работает только поверх raw/TCP. Оставишь его в поле Flow — соединение не поднимется, а сообщение об ошибке будет невразумительным и уведёт диагностику не туда. Поле нужно именно очистить.

Второе: mux.cool с XHTTP не включать. XHTTP мультиплексирует сам через xmux, а свежие версии Xray на сервере такие соединения отвергают.

11

Приёмка

Три проверки, каждая закрывает свой слой.

1 — нода поднялась
docker logs --tail 50 remnanode
ss -tlnp | grep :443
2 — SelfSteal работает (главный тест)
curl -sI https://мой.домен

Без --resolve — обычный запрос снаружи на 443. Он проходит через Xray, не проходит авторизацию Reality и проваливается в Caddy. Именно это увидит проверяющий.

3 — сертификат подменился
echo | openssl s_client -connect мой.домен:443 \
  -servername мой.домен 2>/dev/null \
  | openssl x509 -noout -subject -issuer
Приёмка пройдена
  • в логах ноды нет ошибок разбора конфига, на 443 — rw-core
  • curl отдаёт HTTP/2 200 и твою страницу
  • сертификат — Let's Encrypt на домен ноды
  • клиент подключается и трафик идёт

Если встало

Диагностика послойно, не наугад. Сначала определи, какой слой сломан, потом чини.

СимптомСлойЧто смотреть
В панели нода «не подключена» Нода Порт API закрыт или открыт не тому IP, либо в Address указан не тот адрес. Смотри docker logs remnanode и ufw status numbered
Контейнер remnanode перезапускается по кругу Нода Обычно испорченный SSL_CERT — при копировании из панели потерялась часть строки. Сгенерируй карточку ноды заново и вставь compose целиком
ss не показывает rw-core на 443 Профиль Config Profile не привязан к ноде, панель не отдала конфиг Xray. Вернись к фазе 03, шаг 3
SSH пускает по паролю после запрета SSH Drop-in файл в /etc/ssh/sshd_config.d/ переопределяет основной конфиг. Правь и его, затем systemctl restart ssh
Caddy зациклился на выпуске сертификата ACME Закрыт порт 80 либо включено проксирование Cloudflare. Проверь оба фаервола — ufw и панель хостера
В ответе есть alt-svc Caddy Не применился protocols h1 h2. Проверь глобальный блок Caddyfile и перезапусти контейнер
Открывается чужой сайт или редирект на guce. Xray Шаблон не применился, на ноде ещё старый target. Consent-страница Yahoo — типичный признак
curl отдаёт сайт, но клиент не подключается Хост Почти всегда непустой Flow. Дальше — расхождение path или публичного ключа
Клиент подключается, трафика нет Роутинг docker logs remnanode при loglevel: warning
Connection refused на 443 снаружи Xray Нода не стартовала — ошибка в JSON. Логи контейнера покажут строку
YAML или heredoc ведут себя странно Шелл Приглашение > вместо # означает незакрытый heredoc — Ctrl+C. Затем head -3 на файле: в него могла попасть лишняя строка

Откат: вернуть сохранённый шаблон Xray в панель и применить к ноде. Caddy при этом можно не трогать — он никому не мешает и пригодится со следующей попытки.

Чек-лист ноды

нода: — укажи домен выше —

По всему парку

Что у каждой ноды своё

Клиенты

XHTTP требует свежих версий: v2rayNG 1.9+, v2rayN 7+, sing-box 1.11+, Hiddify, Streisand. Старые клиенты отвалятся молча. Предупреди пользователей до переключения, иначе получишь волну обращений.

Балансировщик

Если на клиенте настроен балансировщик, его конфиг править не нужно — Remnawave подставляет транспорт из настроек хоста. После миграции дай наблюдателю несколько минут на пересбор статистики: скользящее окно выборки наполняется не мгновенно, и первые пробы могут увести балансировщик не туда.

Порядок работ

Новая нода. Иди по фазам подряд, от 01 до 11. Пользователей на ней ещё нет, ломать нечего.

Миграция работающей ноды. Фазы 01–03 уже пройдены, начинай с 04. Фазы 04–07 ничего не ломают: старый инбаунд продолжает работать, пока ты заводишь домен, поднимаешь Caddy и получаешь сертификат. Единственный момент простоя наступает в фазе 09, при перезапуске ноды с новым шаблоном.

При нескольких нодах это даёт удобную схему: доведи все ноды до конца фазы 07 заранее, проверь у каждой сайт-прикрытие, и только потом пройди переключением по фазам 08–11. Тогда простой сводится к одному рестарту на ноду вместо растянутой на вечер возни.


Собрано из рабочей сессии развёртывания. Проверено на Xray 25.x, Caddy 2, Remnawave с нодой в Docker. Значения в полях наверху хранятся только в этом браузере.