Skip to main content

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)

Модель B — shared edge-нода: DE.
Там public 443 уже занят вебом, поэтому спереди стоит HAProxy: только SNI de-srv-t1.quick-fly-space.uk уходит в Xray на 127.0.0.1:12000, а весь остальной 443 трафик остаётся на NPMplus 127.0.0.1:11000. Это отдельный паттерн, его не надо тащить на FIN/USA без необходимости. HAProxy умеет это штатно через req.ssl_sni в mode tcp. (xtls.github.io)

Remnawave-панель — не Xray-ядро, и сама по себе пользователей не обслуживает; ей нужен подключённый Remnawave Node. У ноды в UI поле Port — это порт, на котором уже запущен Remnawave Node, а не пользовательский inbound. В твоём кейсе это 62223. (docs.rw)


Жёсткие правила, которые не надо нарушать

  1. Панель не публикуй напрямую наружу. У Remnawave это прямое требование: сервисы панели должны сидеть на loopback/внутренней сети и отдаваться через reverse proxy. (docs.rw)

  2. Subscription page не вешай на sub-path. Не /sub, не /subscription, не /remnawave. Только root домена/поддомена, либо отдельно включай CUSTOM_SUB_PREFIX. Иначе ловишь те самые 404 на /assets/.... (docs.rw)

  3. Для новых обычных нод делай домен, а не IP. В Remnawave Host адрес может быть и IP, и домен, но домен лучше, потому что потом меняешь DNS, а не заставляешь клиента обновлять подписку. Хост наследует настройки выбранного inbound, а SNI на хосте может переопределять serverNames из inbound.

  4. Внутренний сквад обязателен. Профиль на ноде и Host сами по себе не делают inbound доступным пользователю. Inbound ещё должен быть включён в Internal Squad пользователя.

  5. Если меняешь .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

  • :::443rw-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 на хосте

Сначала этап без риска:

  • весь 443127.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-хост ты добавляешь так:

  1. DNS xx-srv... -> IP, DNS only.

  2. На сервере: nginx + acme.sh + LE cert через Cloudflare DNS API.

  3. Cover-site на 127.0.0.1:10443 с этим же доменом и cert.

  4. remnanode в network_mode: host, NODE_PORT=62223.

  5. Сгенерировал x25519 + shortId.

  6. Создал Config Profile с:

    • listen 0.0.0.0

    • port 443

    • target 127.0.0.1:10443

    • serverNames [xx-srv...]

  7. Назначил профиль ноде и включил inbound.

  8. Создал Host:

    • address xx-srv...

    • port 443

    • SNI xx-srv...

    • fingerprint chrome

  9. Включил inbound в VPN-Main.

  10. Проверил тестовым юзером.

Если на ноде уже живёт веб на 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

Нужно видеть:

  • :443rw-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

Для shared-edge хоста, где уже живёт веб:

  • копируй схему DE

  • не переноси веб наружу на 8443

  • делай HAProxy front на 443

Если хочешь, следующим сообщением я соберу тебе это в ещё более прикладной вид: два готовых шаблона
node-bootstrap.sh для новой FIN/USA-подобной ноды и
Config Profile JSON template, который ты будешь только домен/ключи менять.