Marzban-node - сервер-нода панели Marzban. Нужен для упрощения процесса настройки множества серверов в рамках одной VPN подписки. Поставляется с ядром XRay-core.

Здесь рассмотрим установку ноды с использованием механизма self-steal.

Установка

Перед установкой ноды необходимо установить Docker. Инструкции по установке Docker Engine с плагином Docker Compose есть на их официальном сайте. Также, необходимо иметь уже настроенную основную панель Marzban.

Установка ноды на сервер осуществляется скриптом, предоставленным разработчиком панели:

sudo bash -c "$(curl -sL https://github.com/Gozargah/Marzban-scripts/raw/master/marzban-node.sh)" @ install

После загрузки всех необходимых пакетов скрипт запросит сертификат ноды. Получить его можно в интерфейсе основной панели: открываем меню в правом верхнем углу → Node settings → Add New Marzban Node → нажимаем на иконку глаза и тем самым раскрываем окно с сертификатом ноды. Копируем и передаем скрипту. Окно с добавлением ноды при этом не закрываем.

После утверждения сертификата в скрипте подтверждаем порты для XRay ядра и API (или меняем, если это требуется). Скрипт установит docker compose с нодой. Заполняем данные в форме на основной панели, нажимаем Add Node, ждем подключения основной панели к ноде.

Tip

Если на раскрывающемся элементе ноды в списке отображается зеленая плашка Connected и версия XRay, значит установка прошла успешно.

Дополнительная настройка

После успешного добавления ноды к основной панели приступим к настройке ноды. Для начала переходим на сервере переходим в директорию /opt/marzban-node и выключаем компоуз командой docker compose down. Далее нам нужно будет отредактировать docker-compose.yml.

services:
  caddy: # добавляем в компоуз контейнер caddy
    image: caddy:2.9
    restart: always
    network_mode: host
    volumes:
      - ./caddy/data:/data
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile
      - ./caddy/index.html:/www/index.html
  marzban-node:
    container_name: marzban-node
    image: gozargah/marzban-node:latest
    restart: always
    network_mode: host
    environment:
      SSL_CLIENT_CERT_FILE: "/var/lib/marzban-node/cert.pem"
      SERVICE_PORT: "62050"
      XRAY_API_PORT: "62051"
      SERVICE_PROTOCOL: "rest"
    volumes:
      - /var/lib/marzban-node:/var/lib/marzban
      - /var/lib/marzban-node:/var/lib/marzban-node

Создаем директорию для конфигурации Caddy командой mkdir caddy и создаем в ней два файла:

  • Caddyfile

    {
    	https_port 4123
    	default_bind 127.0.0.1
    	servers {
    		listener_wrappers {
    			proxy_protocol {
    				allow 127.0.0.1/32
    			}
    			tls
    		}
    	}
    	auto_https disable_redirects
    }
    https://node.domain { # заменить домен на тот, который вы планируете использовать для ноды
            root * /www
            try_files {path} {path}.html {path}/ =404
            file_server
            encode gzip
    }
    http://node.domain { # заменить домен на тот, который вы планируете использовать для ноды
    	bind 0.0.0.0
    	redir https://node.domain{uri} permanent # заменить домен на тот, который вы планируете использовать для ноды
    }
    :4123 {
    	tls internal
    	respond 204
    }
    :80 {
    	bind 0.0.0.0
    	respond 204
    }
  • index.html

    <!DOCTYPE html>
    <html lang="ru">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Вход в Confluence</title>
        <style>
          body {
            font-family:
              -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
              sans-serif;
            background-color: #f4f5f7;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
          }
     
          .login-container {
            background-color: white;
            padding: 40px;
            border-radius: 8px;
            box-shadow:
              0 1px 3px rgba(0, 0, 0, 0.12),
              0 1px 2px rgba(0, 0, 0, 0.24);
            width: 350px;
            text-align: center;
          }
     
          .logo {
            margin-bottom: 20px;
          }
     
          .logo img {
            width: 120px;
          }
     
          h2 {
            margin-bottom: 20px;
            font-size: 24px;
            color: #0052cc;
          }
     
          input[type="text"],
          input[type="password"] {
            width: 100%;
            padding: 10px;
            margin: 10px 0;
            border: 1px solid #dfe1e6;
            border-radius: 4px;
            box-sizing: border-box;
            font-size: 16px;
          }
     
          .error {
            border-color: red;
          }
     
          .error-message {
            color: red;
            font-size: 14px;
            display: none;
            margin-top: 10px;
          }
     
          button {
            width: 100%;
            padding: 10px;
            background-color: #0052cc;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            margin-top: 20px;
          }
     
          button:hover {
            background-color: #0747a6;
          }
     
          .help-links {
            margin-top: 20px;
            font-size: 14px;
          }
     
          .help-links a {
            color: #0052cc;
            text-decoration: none;
          }
     
          .help-links a:hover {
            text-decoration: underline;
          }
          /* Стили для модального окна */
          .modal {
            display: none;
            position: fixed;
            z-index: 1;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: auto;
            background-color: rgba(0, 0, 0, 0.4);
            padding-top: 60px;
          }
     
          .modal-content {
            background-color: white;
            margin: 5% auto;
            padding: 20px;
            border: 1px solid #888;
            width: 80%;
            max-width: 400px;
            border-radius: 8px;
            text-align: center;
          }
     
          .close {
            color: #aaa;
            float: right;
            font-size: 28px;
            font-weight: bold;
            cursor: pointer;
          }
     
          .close:hover,
          .close:focus {
            color: black;
            text-decoration: none;
            cursor: pointer;
          }
        </style>
      </head>
      <body>
        <div class="login-container">
          <div class="logo">
            <img
              src="https://cdn.icon-icons.com/icons2/2429/PNG/512/confluence_logo_icon_147305.png"
              alt="Confluence"
            />
          </div>
          <h2 id="login-title">Войти в Confluence</h2>
          <form id="login-form">
            <input type="text" id="username" name="username" placeholder="Адрес электронной почты" />
            <input type="password" id="password" name="password" placeholder="Введите пароль" />
            <button type="submit" id="login-button">Войти</button>
          </form>
          <div id="error-message" class="error-message">
            Неправильное имя пользователя или пароль.
          </div>
          <div class="help-links">
            <a href="#" id="forgot-link">Не удается войти?</a>
            <a href="#" id="create-link">Создать аккаунт</a>
          </div>
        </div>
     
        <div id="myModal" class="modal">
          <div class="modal-content">
            <span class="close">&times;</span>
            <p id="modal-text">Для создания аккаунта обратитесь к администратору.</p>
          </div>
        </div>
     
        <script>
          function setLanguage(lang) {
            const elements = {
              ru: {
                loginTitle: "Войти в Confluence",
                usernamePlaceholder: "Адрес электронной почты",
                passwordPlaceholder: "Введите пароль",
                loginButton: "Войти",
                forgotLink: "Не удается войти?",
                createLink: "Создать аккаунт",
                createAccountText: "Для создания аккаунта обратитесь к администратору.",
                forgotPasswordText: "Для восстановления доступа обратитесь к администратору.",
                errorMessage: "Неправильное имя пользователя или пароль.",
              },
              en: {
                loginTitle: "Login to Confluence",
                usernamePlaceholder: "Email address",
                passwordPlaceholder: "Enter password",
                loginButton: "Login",
                forgotLink: "Can't log in?",
                createLink: "Create an account",
                createAccountText: "To create an account, please contact your administrator.",
                forgotPasswordText: "To recover access, please contact your administrator.",
                errorMessage: "Incorrect username or password.",
              },
            }
     
            document.getElementById("login-title").innerText = elements[lang].loginTitle
            document.getElementById("username").placeholder = elements[lang].usernamePlaceholder
            document.getElementById("password").placeholder = elements[lang].passwordPlaceholder
            document.getElementById("login-button").innerText = elements[lang].loginButton
            document.getElementById("forgot-link").innerText = elements[lang].forgotLink
            document.getElementById("create-link").innerText = elements[lang].createLink
     
            // Устанавливаем тексты для модальных окон и сообщений об ошибках
            document.getElementById("create-link").dataset.modalText =
              elements[lang].createAccountText
            document.getElementById("forgot-link").dataset.modalText =
              elements[lang].forgotPasswordText
            document.getElementById("error-message").innerText = elements[lang].errorMessage
          }
     
          function detectLanguage() {
            const userLang = navigator.language || navigator.userLanguage
            if (userLang.startsWith("ru")) {
              setLanguage("ru")
            } else {
              setLanguage("en")
            }
          }
     
          document.addEventListener("DOMContentLoaded", detectLanguage)
     
          var modal = document.getElementById("myModal")
     
          var span = document.getElementsByClassName("close")[0]
     
          function openModal(text) {
            document.getElementById("modal-text").innerText = text
            modal.style.display = "block"
          }
     
          document.getElementById("create-link").onclick = function (event) {
            event.preventDefault()
            openModal(this.dataset.modalText)
          }
     
          document.getElementById("forgot-link").onclick = function (event) {
            event.preventDefault()
            openModal(this.dataset.modalText)
          }
     
          span.onclick = function () {
            modal.style.display = "none"
          }
     
          window.onclick = function (event) {
            if (event.target == modal) {
              modal.style.display = "none"
            }
          }
     
          document.getElementById("login-form").onsubmit = function (event) {
            event.preventDefault()
            var username = document.getElementById("username")
            var password = document.getElementById("password")
            var errorMessage = document.getElementById("error-message")
     
            username.classList.remove("error")
            password.classList.remove("error")
            errorMessage.style.display = "none"
     
            var hasError = false
            if (username.value.trim() === "") {
              username.classList.add("error")
              hasError = true
            }
            if (password.value.trim() === "") {
              password.classList.add("error")
              hasError = true
            }
     
            if (hasError) {
              return
            }
     
            errorMessage.style.display = "block"
          }
     
          window.onclick = function (event) {
            if (event.target == modal) {
              modal.style.display = "none"
            }
          }
        </script>
      </body>
    </html>

Далее поднимаем компоуз командой docker compose up -d.

В основной панели открываем конфигурацию xray и добавляем в поле serverName домен ноды

Warning

В случае, если у вас используется несколько нод, их домены должны быть записаны в массив serverNames вместе с доменом основной панели.

Наконец, переходим в настройки хостов: открываем меню в правом верхнем углу → Host settings

Раскрываем панель с нужным инбаундом (скорее всего он у вас только один) и добавляем нового хоста, заполняя поля настроек следующим образом:

Success

При успешной настройке по этой инструкции на домене, использованном для ноды, будет открываться темплейт логина в Confluence, который, само собой, ничего не делает. Данный ход нужен для обеспечения легитности трафика и защиты сервера.