RUNBOOK: как добавить новую обычную ноду по шаблону FIN/USA
Ниже нормальный runbook под твою текущую схему. Без воды, но с нюансами, чтобы ты потом не наступал на те же грабли.
Что у тебя в итоге за архитектура
У тебя теперь две разные модели нод.
Модель A — обычная entry-нода: FIN и USA.
На них rw-core слушает public 443, а cover-site живёт локально на 127.0.0.1:10443 через nginx и LE-сертификат, выпущенный через acme.sh + DNS API. Это самая чистая схема для новых нод. Xray transport сейчас использует network: "raw" как актуальное имя TCP, а tcp оставлен как alias; для REALITY нужен target, и serverNames должны совпадать с тем, что этот target реально принимает. (xtls.github.io)
Remnawave-панель — не Xray-ядро, и сама по себе пользователей не обслуживает; ей нужен подключённый Remnawave Node. У ноды в UI поле Port — это порт, на котором уже запущен Remnawave Node, а не пользовательский inbound. В твоём кейсе это 62223. (docs.rw)
Жёсткие правила, которые не надо нарушать
-
Панель не публикуй напрямую наружу. У Remnawave это прямое требование: сервисы панели должны сидеть на loopback/внутренней сети и отдаваться через reverse proxy. (docs.rw)
-
Subscription page не вешай на sub-path. Не
/sub, не/subscription, не/remnawave. Только root домена/поддомена, либо отдельно включайCUSTOM_SUB_PREFIX. Иначе ловишь те самые 404 на/assets/.... (docs.rw) -
Для новых обычных нод делай домен, а не IP. В Remnawave Host адрес может быть и IP, и домен, но домен лучше, потому что потом меняешь DNS, а не заставляешь клиента обновлять подписку. Хост наследует настройки выбранного inbound, а SNI на хосте может переопределять
serverNamesиз inbound. -
Внутренний сквад обязателен. Профиль на ноде и Host сами по себе не делают inbound доступным пользователю. Inbound ещё должен быть включён в Internal Squad пользователя.
-
Если меняешь
.envу Remnawave, контейнеры надо пересоздавать, а не просто restart.docker compose down && docker compose up -d, иначе переменные не применятся. (docs.rw)
Это твой основной шаблон для следующих серверов.
1) Подготовка DNS
В Cloudflare:
-
создаёшь
A-запись видаxx-srv-t1.quick-fly-space.uk -> IP_НОДЫ -
для entry-домена ноды держишь DNS only, не orange cloud
Cloudflare API tokens лучше делать scoped на конкретную зону. Для acme.sh тебе фактически нужны права минимум уровня DNS Write на зону и Zone Read, если ты хочешь, чтобы клиент мог корректно работать с zone metadata. (Cloudflare Docs)
2) Подготовка сервера
На новой ноде:
apt update
apt install -y curl git nginx openssl
systemctl enable --now nginx
Если используешь CrowdSec — оставляй локальные сервисы как есть, но не забирай ими 443.
3) Установка acme.sh
git clone https://github.com/acmesh-official/acme.sh.git /opt/acme.sh-src
cd /opt/acme.sh-src
./acme.sh --install -m admin@quick-fly-space.uk
acme.sh для автоматического продления нужно использовать именно в DNS API mode, а не в manual TXT режиме. Сам проект прямо предупреждает, что manual DNS mode не renewится автоматически. (GitHub)
4) Cloudflare credentials
Создай файл:
install -d -m 700 /root/.secrets
cat >/root/.secrets/cf-acme.env <<'EOF'
export CF_Token='PASTE_TOKEN'
export CF_Zone_ID='PASTE_ZONE_ID'
EOF
chmod 600 /root/.secrets/cf-acme.env
set -a
. /root/.secrets/cf-acme.env
set +a
Не мешай сюда мусор вроде неверного ZoneID или лишних account-level данных, если они не нужны. Ты уже видел, как из-за кривого ZoneID acme.sh падает в invalid domain.
5) Выпуск сертификата для ноды
Пример для новой ноды xx-srv-t1.quick-fly-space.uk:
DOMAIN="xx-srv-t1.quick-fly-space.uk"
~/.acme.sh/acme.sh --issue \
--dns dns_cf \
--server letsencrypt \
--keylength ec-256 \
--dnssleep 30 \
-d "$DOMAIN"
6) Установка сертификата в production path nginx
Не используй cert/key напрямую из ~/.acme.sh/.... Правильный путь — --install-cert с --reloadcmd, чтобы renew автоматически копировал свежий cert и перезагружал nginx. Сам acme.sh это отдельно подчёркивает. (GitHub)
DOMAIN="xx-srv-t1.quick-fly-space.uk"
install -d -m 700 "/etc/nginx/ssl/$DOMAIN"
~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" --ecc \
--key-file "/etc/nginx/ssl/$DOMAIN/key.pem" \
--fullchain-file "/etc/nginx/ssl/$DOMAIN/fullchain.pem" \
--reloadcmd "nginx -t && systemctl reload nginx"
7) Cover-site nginx
Делаешь простую заглушку:
install -d -m 755 /var/www/reality-cover
cat >/var/www/reality-cover/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Welcome</title></head>
<body><h1>Welcome</h1><p>It works.</p></body>
</html>
EOF
Конфиг:
server {
listen 80;
listen [::]:80;
server_name xx-srv-t1.quick-fly-space.uk;
root /var/www/reality-cover;
index index.html;
}
server {
listen 127.0.0.1:10443 ssl;
http2 on;
server_name xx-srv-t1.quick-fly-space.uk;
ssl_certificate /etc/nginx/ssl/xx-srv-t1.quick-fly-space.uk/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/xx-srv-t1.quick-fly-space.uk/key.pem;
root /var/www/reality-cover;
index index.html;
}
Проверка:
nginx -t
systemctl reload nginx
ss -tulpn | egrep ':80 |:10443 |:443 '
openssl s_client -connect 127.0.0.1:10443 -servername xx-srv-t1.quick-fly-space.uk </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
Смысл cover-site простой: REALITY форвардит невалидный трафик в target, и target должен отвечать валидным HTTPS на тот же SNI, а не в пустоту. Это прямое следствие механики target/serverNames. (xtls.github.io)
8) Node compose
Для обычной ноды оставляешь стандартный шаблон Remnawave Node с network_mode: host и NODE_PORT=62223. Это нормально, publish пользовательских портов через Docker тут не нужен. (xtls.github.io)
Типовой skeleton:
services:
remnanode:
container_name: remnanode
hostname: remnanode
image: remnawave/node:latest
network_mode: host
restart: always
cap_add:
- NET_ADMIN
ulimits:
nofile:
soft: 1048576
hard: 1048576
environment:
- NODE_PORT=62223
- SECRET_KEY=...
9) Сгенерировать REALITY-ключи
На ноде:
docker exec -it remnanode xray x25519
openssl rand -hex 8
Нужны:
-
PrivateKey -
PublicKey -
shortId
10) Создать Config Profile в Remnawave
UI → Config Profiles → Create
Шаблон для обычной ноды:
{
"log": {
"loglevel": "warning"
},
"inbounds": [
{
"tag": "VLESS_REALITY_XX_443",
"port": 443,
"listen": "0.0.0.0",
"protocol": "vless",
"settings": {
"clients": [],
"decryption": "none"
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"]
},
"streamSettings": {
"network": "raw",
"security": "reality",
"realitySettings": {
"show": false,
"xver": 0,
"target": "127.0.0.1:10443",
"shortIds": ["REPLACE_SHORTID"],
"privateKey": "REPLACE_PRIVATE_KEY",
"serverNames": ["xx-srv-t1.quick-fly-space.uk"]
}
}
}
],
"outbounds": [
{ "tag": "DIRECT", "protocol": "freedom" },
{ "tag": "BLOCK", "protocol": "blackhole" }
],
"routing": { "rules": [] }
}
Для обычных нод не надо acceptProxyProtocol. Он нужен только там, где спереди HAProxy шлёт send-proxy-v2, как на DE. Для FIN/USA у тебя прямой 443 -> rw-core, без HAProxy.
11) Подключить профиль к ноде
UI → Nodes → Management → Create/Edit node
Заполняешь:
-
Country — как надо
-
Internal name — нормальное имя
-
Address — IP или домен ноды
-
Port —
62223 -
Select config profile — твой новый профиль
-
Включаешь inbound из профиля
-
Save
По логике панели один node использует один Config Profile, а после сохранения Remnawave пушит конфиг на ноду и поднимает Xray listener. Это как раз и видно по твоим тестам с 443/12000.
Проверка на ноде:
netstat -tulpn
Ищешь:
-
:::62223— node API -
:::443—rw-core -
127.0.0.1:10443— nginx
12) Создать Host
UI → Hosts → Create host
Основное:
-
Host visibility — on
-
Remark — например
FIN REALITY 443 -
Address —
fin-srv-t1.quick-fly-space.uk -
Port —
443 -
Node — только нужная нода
-
Tag — пусто
Расширенные:
-
SNI —
fin-srv-t1.quick-fly-space.uk -
Override SNI from address — off
-
Keep SNI blank — off
-
Security layer — default
-
Fingerprint —
chrome -
Остальное пусто
Почему так: Host строго привязан к одному inbound, наследует его параметры, а Host SNI может переопределять serverNames inbound. Visibility управляет тем, увидит ли хост пользователь в подписке.
13) Internal Squad
UI → Internal Squads
Создай или используй рабочий squad, например VPN-Main, и включи в нём новый inbound. Без этого пользователь хост не получит, даже если node и host уже готовы.
14) Тестовый пользователь
UI → Users → Create
Выбираешь:
-
username
-
active internal squad =
VPN-Main -
сохраняешь
-
берёшь subscription URL
-
импортируешь в клиент
-
тестируешь
Remnawave сам отдаёт хосты пользователю через подписку и подставляет нужный формат подписки в зависимости от клиента. Для Happ/V2RayN/V2RayNG это особенно удобно.
RUNBOOK: как добавить shared-edge ноду по шаблону DE
Это только для случая, когда на том же IP уже живёт веб и public 443 нельзя просто отдать rw-core.
1) Перенос HTTPS NPMplus на loopback
На DE ты уже сделал правильно:
-
80:80остаётся наружу -
127.0.0.1:11000:443— внутренний HTTPS NPMplus -
public
443освобождён под HAProxy
2) HAProxy на хосте
Сначала этап без риска:
-
весь
443→127.0.0.1:11000 -
проверяешь, что все существующие сайты живы
Только потом добавляешь отдельный rule для VPN SNI:
frontend https_in
mode tcp
bind *:443
option tcplog
tcp-request inspect-delay 5s
tcp-request content accept if { req.ssl_hello_type 1 }
use_backend bk_reality_de if { req.ssl_sni -i de-srv-t1.quick-fly-space.uk }
default_backend bk_npm_https
backend bk_reality_de
mode tcp
server xray_de 127.0.0.1:12000 send-proxy-v2
backend bk_npm_https
mode tcp
server npm_https 127.0.0.1:11000
3) DE Config Profile
На DE inbound локальный:
-
listen: "127.0.0.1" -
port: 12000 -
sockopt.acceptProxyProtocol: true -
target: "127.0.0.1:11000" -
serverNames: ["de-srv-t1.quick-fly-space.uk"]
acceptProxyProtocol нужен именно потому, что HAProxy шлёт send-proxy-v2. Если один конец его ждёт, а другой не шлёт — всё ломается. Это парный механизм. (xtls.github.io)
Subscription Page: как не сломать второй раз
Твоя рабочая схема сейчас правильная.
Что должно быть
На сервере панели
В /app/remnawave/.env:
SUB_PUBLIC_DOMAIN=sub.quick-fly-space.com
Потом:
cd /app/remnawave
docker compose down remnawave
docker compose up -d
SUB_PUBLIC_DOMAIN должен быть без http/https. Это прямо указано в доке bundled/separate subscription page. (docs.rw)
В subscription page .env
Минимум:
APP_PORT=3010
REMNAWAVE_PANEL_URL=https://remna-panel
REMNAWAVE_API_TOKEN=...
Если ходишь на панель по внутреннему HTTPS, как у тебя сейчас, NODE_EXTRA_CA_CERTS оставляешь — он нужен не для внешнего LE, а чтобы контейнер subscription-page доверял внутреннему сертификату панели. Bundled install и reverse-proxy docs это допускают через обычную внутреннюю схему panel URL. (docs.rw)
Reverse proxy
Подписку отдаёшь с корня отдельного поддомена, не с /sub на основном домене. Иначе опять словишь 404 по assets. (docs.rw)
SHM: что нужно подготовить заранее
Вот тут не тупи, потому что у тебя уже есть один важный нюанс.
1) Internal Squad name
В твоём текущем шаблоне SHM DEFAULT_INTERNAL_SQUAD_NAME резолвится по имени, а не по UUID. Значит имя рабочего squad должно быть стабильным и не меняться без причины. Сейчас у тебя это VPN-Main.
2) Storage prefix
В SHM docs/examples для Remnawave используется префикс vpn_remnawave_..., а в твоём текущем shm-remnawave.template.sh ещё остался старый vpn_mrzb_.... Это надо обязательно поправить, иначе потом будешь искать “почему бот/кабинет не читает ключи”.
То есть в шаблоне надо заменить:
-
vpn_mrzb_{{ us.id }}
на -
vpn_remnawave_{{ us.id }}
3) Что должно лежать в settings сервера для SHM
Минимум:
-
server.settings.remnawave.api -
server.settings.remnawave.token -
server.settings.remnawave.default_internal_squad_name -
опционально
server.settings.remnawave.shm_tz -
опционально
server.settings.remnawave.expire_safety_minutes
Это уже видно из твоего шаблона.
4) Категория услуги
В SHM должна быть категория услуги vpn-remnawave. Это прямо так указано в SHM PDF.
Мини-чеклист для каждой новой ноды
Если коротко, то любой следующий XX-хост ты добавляешь так:
-
DNS
xx-srv... -> IP, DNS only. -
На сервере: nginx +
acme.sh+ LE cert через Cloudflare DNS API. -
Cover-site на
127.0.0.1:10443с этим же доменом и cert. -
remnanodeвnetwork_mode: host,NODE_PORT=62223. -
Сгенерировал
x25519+shortId. -
Создал
Config Profileс:-
listen 0.0.0.0 -
port 443 -
target 127.0.0.1:10443 -
serverNames [xx-srv...]
-
-
Назначил профиль ноде и включил inbound.
-
Создал Host:
-
address
xx-srv... -
port
443 -
SNI
xx-srv... -
fingerprint
chrome
-
-
Включил inbound в
VPN-Main. -
Проверил тестовым юзером.
Если на ноде уже живёт веб на 443, не делай вид, что это “почти то же самое”. Это уже DE-паттерн с HAProxy.
Что смотреть, если новая нода не заводится
Если нода не работает, проверяй в таком порядке:
На сервере
netstat -tulpn
openssl s_client -connect 127.0.0.1:10443 -servername xx-srv-t1.quick-fly-space.uk </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
Нужно видеть:
-
:443—rw-core -
127.0.0.1:10443— nginx -
:62223— node API
В панели
-
profile назначен нужной ноде
-
inbound в ноде активирован
-
Host привязан к правильному inbound
-
inbound включён в
VPN-Main -
Host visibility = on
У пользователя
-
хост появился в подписке
-
в клиенте SNI правильный
-
клиент берёт fingerprint
chrome
Мой практический вердикт
Для всех следующих обычных entry-нод:
-
копируй схему FIN/USA
-
не тащи туда HAProxy
-
не городи NPMplus на нодах
-
делай cert через acme.sh + Cloudflare DNS API
-
держи cover-site локально на
127.0.0.1:10443
-
копируй схему DE
-
не переноси веб наружу на 8443
-
делай HAProxy front на 443
Если хочешь, следующим сообщением я соберу тебе это в ещё более прикладной вид: два готовых шаблона —node-bootstrap.sh для новой FIN/USA-подобной ноды иConfig Profile JSON template, который ты будешь только домен/ключи менять.
No comments to display
No comments to display