Mieru и NaïveProxy: каскадные туннели под управлением панели s-ui-x

Материал из wolfram
Перейти к навигации Перейти к поиску

mieru и NaïveProxy: каскадные туннели под управлением панели s-ui-x

Эта статья объединяет два ранее раздельных руководства — по mieru и по NaïveProxy — в одно описание реально работающей инфраструктуры. Оба протокола развёрнуты параллельно, используют общее выходное плечо и управляются из одной панели на входном сервере.

Статья описывает схему в том виде, в каком она работает сейчас. Прежний способ (голые Docker-контейнеры с правкой JSON и Caddyfile руками) не выброшен — он вынесен в Приложение А, поскольку остаётся актуальным для выходного плеча и как запасной путь.

Что мы построим

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

                    ВХОДНОЕ ПЛЕЧО (РФ)              ВЫХОДНОЕ ПЛЕЧО (EU)
                 +------------------------+      +----------------------+
 клиент --mieru->| inbound mieru          |      |                      |
                 |      |                 |      |  mita  (mieru)       |
                 |      v  маршрутизация  |      |                      |
                 | outbound mieru --------+----->|                      |
                 |                        |      |                      |
 клиент --naive->| inbound naive          |      | Caddy + forwardproxy |
                 |      |                 |      |                      |
                 |      v                 |      |                      |
                 | outbound naive --------+----->|                      |
                 |                        |      |          |           |
                 |  панель s-ui-x         |      |          v           |
                 |  подписки, учёт, лимиты|      |  egress (WARP/direct)|
                 +------------------------+      +----------------------+

Ключевая идея: входной сервер одновременно является и сервером для клиентов, и клиентом для выходного сервера. Оба протокола в панели заведены дважды — как inbound (принимает пользователей) и как outbound (уходит на выходное плечо), а связывают их правила маршрутизации. Это и есть каскад.

Два протокола: чем отличаются и зачем нужны оба

Смысл держать два канала одновременно в том, что они прячутся принципиально по-разному и ломаются от разных причин. Если один начнут давить — второй, скорее всего, устоит.

Признак mieru NaïveProxy
Транспорт TCP (или UDP), без TLS TCP, HTTPS + HTTP/2 CONNECT
Как прячется Поток выглядит как случайный шум либо текстоподобный трафик Неотличим от браузера Chrome, идущего на обычный сайт
Нужен домен и сертификат Нет — только пара логин/пароль Да — валидный домен и TLS-сертификат
Реакция на активное зондирование Молчит — фингерпринтить нечего Отвечает как обычный сайт (probe_resistance)
Учёт трафика по пользователям Есть на уровне протокола Нет — считается панелью на входе
Клиенты Ставится штатно (mihomo, Karing, NekoBox) На Android нужен отдельный плагин (Cronet)

Практический вывод из эксплуатации: на мобильных основным каналом удобнее делать mieru — он импортируется без плясок. NaïveProxy держать на десктопе и как независимый резерв.

Сравнение с другими методами туннелирования

Метод Транспорт Маскировка Требует домен и TLS Устойчивость к ML-классификации
WireGuard UDP нет (явный WG) нет низкая
OpenVPN UDP/TCP нет опционально низкая
Shadowsocks-AEAD-2022 TCP случайный шум нет средняя
VLESS + REALITY TCP (TLS) TLS-handshake чужого сайта да высокая (внутри пула TLS)
Hysteria2 UDP (QUIC) QUIC с маскировкой под HTTPS да высокая (внутри пула QUIC)
NaïveProxy TCP (HTTP/2) стек Chromium, неотличим от HTTPS да высокая (внутри пула HTTPS)
mieru TCP / UDP, без TLS случайный шум либо текстоподобный поток нет высокая (подгонка энтропии и ASCII)

Как работают протоколы

mieru: шифрование без TLS

Программный пакет состоит из двух самостоятельных бинарников:

Бинарник Роль
mita Серверная часть. Принимает зашифрованные соединения, аутентифицирует пользователей, переправляет полезную нагрузку наружу
mieru Клиентская часть. Поднимает локальный SOCKS5-прокси; всё, что в него пришло, шифруется и уходит на mita

В отличие от большинства современных туннелей, mieru не использует TLS и не маскируется под легитимный сайт. Вместо этого он подаёт на провод поток, который при пассивном анализе выглядит как случайные байты или произвольный текстовый протокол — без узнаваемых заголовков, фиксированных длин и характерных handshake-паттернов.

Поток данных одиночного mieru: клиент → SOCKS5 → mieru → шифр → mita → интернет
Поток данных одиночного mieru: клиент → SOCKS5 → mieru → шифр → mita → интернет
Параметр Значение
Алгоритм XChaCha20-Poly1305 (AEAD) с 24-байтным nonce
Длина ключа 32 байта
Выработка ключа PBKDF2-SHA256, 64 итерации, соль = SHA-256 от текущего времени, округлённого до 2 минут
Идентификатор пользователя Последние 4 байта nonce заменены на префикс SHA-256 от username + nonce[0:16]; имя пользователя на провод не передаётся
Защита от повтора Проверка nonce-кэша на сервере; повторный пакет отбрасывается

ВСЕ структурные поля сегмента (тип, размер, sequence number, ID сессии) находятся внутри зашифрованного поля enc-meta. Снаружи невозможно определить ни длину полезной нагрузки, ни фазу сессии — видны только зоны паддинга и шифр-блобы.

Формат сегмента mieru: padding, nonce, шифр-метаданные, шифр-полезная нагрузка
Формат сегмента mieru: padding, nonce, шифр-метаданные, шифр-полезная нагрузка

Требование к синхронизации часов

Ключ шифрования не передаётся по каналу и нигде не хранится: он каждый раз вырабатывается из пары логин+пароль и текущего времени. Отсюда неочевидное требование:

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

Traffic pattern: подгонка статистики под «обычный» трафик

mieru умеет подменять статистические признаки шифр-потока:

Опция Что делает Накладные расходы
tcpFragment Дробит TCP-пакеты на мелкие куски и вставляет случайную задержку. Ломает узнаваемость «один большой шифр-стрим»
латентность до maxSleepMs мс на фрагмент
nonce.NONCE_TYPE_PRINTABLE Подменяет первые байты nonce печатными ASCII. Уводит классификатор из «высокая энтропия» в «это текстовый протокол» почти ноль
nonce.NONCE_TYPE_FIXED Подменяет первые байты фиксированными hex-строками. Имитирует конкретный заголовок ноль

Включать traffic pattern имеет смысл только на видимом анализатору плече (клиент → входной сервер). На внутреннем плече каскада это лишний overhead.

NaïveProxy: маскировка под Chrome

DPI «смотрит» не только на адреса, но и на характер соединения: сколько байт летит в каждую сторону, с какими интервалами, как выглядит TLS-рукопожатие, какой у соединения отпечаток. Обычный HTTPS-прокси выявляется именно потому, что программа-клиент общается иначе, чем настоящий браузер.

NaïveProxy решает это принципиально: клиентская часть использует встроенный сетевой стек Chromium (Cronet), поэтому трафик неотличим от реального Chrome не только по содержимому, но и по поведению на уровне TCP/TLS — те же задержки, те же размеры пакетов, тот же TLS-fingerprint. Серверная часть притворяется обычным веб-сервером.

Механика:

  1. Клиент открывает HTTPS-соединение с сервером — точь-в-точь как Chrome.
  2. Внутри соединения по HTTP/2 CONNECT прокидывается туннель.
  3. Сервер обрабатывает обычные запросы как веб-сервер, а туннельные — переправляет дальше.
  4. Если кто-то «постучится» без верных учётных данных, сервер ответит как обычный сайт — это probe_resistance.

Почему каскад из двух серверов

Поток данных каскадного mieru: входной сервер → выходной сервер → egress
Поток данных каскадного mieru: входной сервер → выходной сервер → egress
  • Адрес выходного сервера никогда не виден клиентскому ПО. Если входной сервер заблокируют — выходной остаётся неизвестным и переиспользуется со следующим входным.
  • Входной сервер хранит только учётки клиентов и не имеет финального egress. Компрометация его конфигурации не раскрывает финальный IP.
  • Двойное шифрование на каждом плече: сегмент дважды проходит AEAD с разными ключами.
  • Подключение к близкому серверу менее заметно, чем прямое соединение с зарубежным.

Требования

Серверы

Требование Зачем
1 Два Linux-сервера (Ubuntu 22.04+, Debian 12+ или сравнимый) Входное и выходное плечо каскада
2 SSH с правами root либо sudo без пароля Установка пакетов и управление сервисами
3 Docker Engine 24.0+ и Docker Compose v2.20+ на выходном сервере Контейнеры mita и Caddy. На входном сервере Docker не нужен — там всё нативно
4 NTP активен (chrony или systemd-timesyncd) на обоих Синхронизация часов в пределах 4 минут (см. mieru)
5 Открытый наружу TCP-диапазон 2012-2022 на обоих серверах Принимающие порты mieru
6 Открытые TCP 80 и 443 на обоих серверах NaïveProxy и выпуск сертификатов

DNS-записи

Создать до начала установки — сертификаты выпускаются по ним:

Запись Указывает на Для чего
YOUR_ENTRY_DOMAIN (например np.example.com) IP входного сервера NaïveProxy-вход для клиентов
YOUR_PANEL_DOMAIN (например panel.example.com) IP входного сервера Панель и подписки по HTTPS
YOUR_EXIT_DOMAIN (например npeu.example.com) IP выходного сервера NaïveProxy-приём от входного сервера

Панель и NaïveProxy-вход могут жить на одном домене — они разведены по портам. Разные домены удобнее: вход выглядит как обычный сайт, а панель не светится рядом.

Карта портов

Порт Плечо Кто слушает Наружу
2012-2022/tcp оба mieru (inbound на входе, mita на выходе) да
443/tcp оба NaïveProxy (inbound на входе, Caddy на выходе) да
80/tcp оба ACME http-01 (выпуск сертификатов) да, обязательно свободен
2095/tcp вход веб-панель s-ui-x ограничить своим IP
2097/tcp вход подписки да (клиенты забирают конфиги)

Порт 80 держать свободным. Он нужен ACME для автопродления. Именно из-за занятого веб-сервером 80-го порта продление сертификатов может молча срываться — разбор этой ошибки в разделе Диагностика.

Параметры, которые нужно подготовить заранее

Заполните таблицу своими значениями — дальше по тексту встречаются эти плейсхолдеры.

Плейсхолдер Что это Пример
YOUR_ENTRY_HOST Адрес входного сервера для SSH 198.51.100.10
YOUR_EXIT_HOST Адрес выходного сервера для SSH 203.0.113.20
YOUR_EXIT_IP Публичный IP выходного сервера — к нему подключается входной 203.0.113.20
YOUR_ENTRY_DOMAIN Домен NaïveProxy-входа np.example.com
YOUR_EXIT_DOMAIN Домен NaïveProxy-выхода npeu.example.com
YOUR_PANEL_DOMAIN Домен панели panel.example.com
YOUR_BRIDGE_PASSWORD Пароль служебной mieru-учётки, которой вход ходит на выход openssl rand -base64 24
YOUR_CHAIN_PASSWORD Пароль служебной naive-учётки (то же назначение) openssl rand -hex 16
YOUR_EGRESS_PORT Порт SOCKS5-egress на выходном сервере (если используется) 24366

Служебные учётки (bridge и chain) клиентам не раздаются никогда — это внутренние перемычки каскада.


Этап 1. Выходное плечо

Выходной сервер — «немой» конец каскада: он не знает о клиентах, у него всего две учётные записи, по одной на протокол. Управляется файлами, панель здесь не нужна.

Все команды этого этапа выполняются на выходном сервере.

1.1. Установка Docker

ssh root@YOUR_EXIT_HOST
curl -fsSL https://get.docker.com | sh
docker --version && docker compose version

1.2. mieru: образ

Рабочий каталог:

mkdir -p /opt/mieru-exit && cd /opt/mieru-exit

Официальных образов у mieru нет, поэтому собираем свой — бинарники берутся из релизов upstream:

nano /opt/mieru-exit/Dockerfile
# Образ с mita и mieru, бинарники качаются из релизов upstream
# Версия задаётся через --build-arg MIERU_VERSION
ARG MIERU_VERSION=3.32.0
ARG TARGETARCH=amd64

FROM alpine:3.19 AS downloader
ARG MIERU_VERSION
ARG TARGETARCH
RUN apk add --no-cache ca-certificates wget
WORKDIR /tmp/mita
RUN wget -qO- "https://github.com/enfein/mieru/releases/download/v${MIERU_VERSION}/mita_${MIERU_VERSION}_linux_${TARGETARCH}.tar.gz" | tar xz
WORKDIR /tmp/mieru
RUN wget -qO- "https://github.com/enfein/mieru/releases/download/v${MIERU_VERSION}/mieru_${MIERU_VERSION}_linux_${TARGETARCH}.tar.gz" | tar xz

FROM alpine:3.19
RUN apk add --no-cache ca-certificates tini && \
    adduser -H -D -g "" mita && \
    adduser -H -D -g "" mieru
COPY --from=downloader /tmp/mita/mita /usr/local/bin/mita
COPY --from=downloader /tmp/mieru/mieru /usr/local/bin/mieru
RUN chmod +x /usr/local/bin/mita /usr/local/bin/mieru && \
    mkdir -p /etc/mita /var/lib/mita /var/run/mita \
             /etc/mieru /var/lib/mieru /var/run/mieru /srv && \
    chown -R mita:mita /etc/mita /var/lib/mita /var/run/mita && \
    chown -R mieru:mieru /etc/mieru /var/lib/mieru /var/run/mieru
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["/sbin/tini","--","/usr/local/bin/docker-entrypoint.sh"]

Две строки adduser здесь обязательны. Без системных пользователей mita и mieru не создаётся Unix-сокет для RPC, и контейнер падает с FATAL: getUid("mita") failed.

1.3. mieru: entrypoint

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

nano /opt/mieru-exit/docker-entrypoint.sh
#!/bin/sh
# Запускает mita или mieru в foreground, в фоне применяет JSON-конфиг
# через RPC (mita/mieru apply config). RPC становится доступен
# через 1-2 секунды после старта daemon — отсюда retry-цикл.

set -eu

MODE="${MIERU_MODE:-mita}"
CONFIG_FILE="${MIERU_CONFIG:-/srv/config.json}"

case "$MODE" in
    mita)  BIN=mita ;;
    mieru) BIN=mieru ;;
    *) echo "[entrypoint] FATAL: MIERU_MODE='$MODE' (ожидается mita или mieru)"; exit 1 ;;
esac

apply_config_and_start() {
    if [ ! -f "$CONFIG_FILE" ]; then
        echo "[entrypoint] WARN: $CONFIG_FILE не найден, конфиг не применяется"
        return 0
    fi
    i=0
    while [ "$i" -lt 30 ]; do
        i=$((i+1))
        sleep 1
        if "$BIN" apply config "$CONFIG_FILE" >/tmp/apply.out 2>&1; then
            echo "[entrypoint] config применён ($CONFIG_FILE)"
            # после apply config daemon в IDLE — нужен start, чтобы открыть сокеты
            if "$BIN" start >/tmp/start.out 2>&1; then
                echo "[entrypoint] $BIN start: OK"
            else
                echo "[entrypoint] WARN: $BIN start failed:"
                cat /tmp/start.out 2>/dev/null || true
            fi
            return 0
        fi
    done
    echo "[entrypoint] WARN: не удалось применить config за 30s, последний вывод:"
    cat /tmp/apply.out 2>/dev/null || true
}

apply_config_and_start &

echo "[entrypoint] запуск $BIN run"
exec "$BIN" run

Обратите внимание на "$BIN" start после apply config. mita run сам по себе не открывает прокси-порты — демон висит в состоянии IDLE до явной команды start. Это одна из самых частых причин «сервис работает, но порты не слушаются».

1.4. mieru: конфигурация выходного узла

Сгенерируйте пароль служебной учётки:

openssl rand -base64 24
nano /opt/mieru-exit/server.json
{
    "portBindings": [
        { "portRange": "2012-2022", "protocol": "TCP" }
    ],
    "users": [
        { "name": "ru-bridge", "password": "YOUR_BRIDGE_PASSWORD" }
    ],
    "loggingLevel": "INFO",
    "mtu": 1380,
    "egress": {
        "proxies": [
            {
                "name": "warp-via-xui",
                "protocol": "SOCKS5_PROXY_PROTOCOL",
                "host": "172.17.0.1",
                "port": 24366
            }
        ],
        "rules": [
            {
                "ipRanges": ["*"],
                "domainNames": ["*"],
                "action": "PROXY",
                "proxyNames": ["warp-via-xui"]
            }
        ]
    }
}

Учётка одна — та самая перемычка каскада. Клиентских учёток здесь нет и быть не должно: клиенты живут на входном сервере.

Блок egress нужен, если выходной трафик должен идти не напрямую, а через сторонний прокси (например, SOCKS5-inbound панели 3x-ui, ведущий на WARP). Адрес 172.17.0.1 — это шлюз docker0: контейнер с network_mode: host видит локальный SOCKS-inbound именно по этому адресу, а не по 127.0.0.1. Если egress не нужен — удалите блок целиком, тогда выход будет с публичного IP сервера.

Проверить egress-цепочку (подставьте свой порт):

docker run --rm curlimages/curl:latest --max-time 12 --socks5 172.17.0.1:24366 https://icanhazip.com

Ожидается IP вашего egress-узла. Если вернулся публичный IP самой машины — egress не работает, смотрите маршрутизацию в панели.

1.5. mieru: запуск

nano /opt/mieru-exit/docker-compose.yml
services:
  mita-exit:
    build:
      context: .
      args:
        MIERU_VERSION: "3.32.0"
    image: mieru-bundle:3.32.0
    container_name: mita-exit
    restart: unless-stopped
    network_mode: host
    environment:
      MIERU_MODE: mita
      MIERU_CONFIG: /srv/server.json
    volumes:
      - ./server.json:/srv/server.json:ro
      - mita-exit-state:/etc/mita
      - mita-exit-runtime:/var/run/mita
      - mita-exit-data:/var/lib/mita
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  mita-exit-state:
  mita-exit-runtime:
  mita-exit-data:
cd /opt/mieru-exit && docker compose up -d --build
docker exec mita-exit mita status

Ожидается mita server status is "RUNNING". Если IDLE — выполните docker exec mita-exit mita start.

1.6. NaïveProxy: образ Caddy с forwardproxy

Штатный Caddy не умеет NaïveProxy — нужен форк плагина от автора naiveproxy, вкомпилированный через xcaddy.

mkdir -p /opt/caddy-naive/www && cd /opt/caddy-naive
nano /opt/caddy-naive/Dockerfile
FROM golang:1.25-alpine AS builder

RUN apk add --no-cache git

RUN go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

RUN xcaddy build \
    --with github.com/caddyserver/forwardproxy=github.com/klzgrad/forwardproxy@naive

FROM debian:bookworm-slim

RUN apt-get update && \
    apt-get install -y ca-certificates && \
    rm -rf /var/lib/apt/lists/*

COPY --from=builder /go/caddy /usr/local/bin/caddy

EXPOSE 80 443

CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

Ветка @naive в строке --with обязательна: это форк klzgrad, а не оригинальный forwardproxy.

1.7. NaïveProxy: Caddyfile

nano /opt/caddy-naive/Caddyfile
{
  order forward_proxy before file_server
}
:443, YOUR_EXIT_DOMAIN {
  tls your-email@example.com
  forward_proxy {
    basic_auth ru-chain YOUR_CHAIN_PASSWORD
    upstream socks5://127.0.0.1:24363
    hide_ip
    hide_via
    probe_resistance
  }
  file_server {
    root /var/www/html
  }
}

Разбор директив:

Директива Назначение
tls your-email@example.com Caddy сам выпустит и будет продлевать сертификат Let's Encrypt
basic_auth ru-chain ... Учётка перемычки. Строк basic_auth может быть несколько — по одной на пользователя
upstream socks5://... Куда отдавать расшифрованный трафик (SOCKS5-inbound панели, WARP и т.п.)
hide_ip, hide_via Вырезают заголовки, выдающие проксирование
probe_resistance При неверных учётных данных сервер ведёт себя как обычный сайт, а не как прокси
file_server Отдаёт статическую заглушку тем, кто зашёл «просто так»

Критично: плагин klzgrad/forwardproxy не понимает bcrypt-хэши — только plaintext. Если вписать хэш, сервер не выдаст ошибку авторизации, а вернёт 308 Redirect (сработает probe_resistance), и диагностика уведёт совсем не туда.

Заглушка, чтобы сайт выглядел живым:

echo '<!doctype html><title>It works</title><h1>It works</h1>' > /opt/caddy-naive/www/index.html

1.8. NaïveProxy: запуск

nano /opt/caddy-naive/docker-compose.yml
services:
  caddy-naive:
    build: .
    container_name: caddy-naive
    restart: unless-stopped
    network_mode: host
    environment:
      - XDG_DATA_HOME=/data
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./www:/var/www/html:ro
      - caddy_data:/data
      - caddy_logs:/var/log/caddy

volumes:
  caddy_data:
  caddy_logs:
cd /opt/caddy-naive && docker compose up -d --build

Сборка занимает несколько минут — компилируется Caddy. Проверка:

docker logs caddy-naive --tail 20
curl -sI https://YOUR_EXIT_DOMAIN | head -3

Должна вернуться заглушка с валидным сертификатом. Если сертификат не выпустился — проверьте, что порт 80 доступен снаружи и DNS-запись уже указывает на этот сервер.

1.9. Итог этапа

На выходном сервере работают два независимых приёмника:

Служба Порт Учётка Куда отдаёт трафик
mita-exit (mieru) 2012-2022/tcp ru-bridge egress SOCKS5 → WARP либо напрямую
caddy-naive 443/tcp ru-chain upstream socks5://…

Оба ждут подключений только от входного сервера. Клиенты сюда не ходят.


Этап 2. Входное плечо: панель s-ui-x

2.1. Почему панель, а не голые контейнеры

Исторически оба протокола на входном сервере жили так же, как сейчас живут на выходном: mita в Docker с правкой server.json и Caddy с учётками в Caddyfile. Для двух служебных перемычек это нормально, но как только появляются живые пользователи, схема начинает мешать:

  • добавление клиента — правка JSON или Caddyfile плюс перезапуск контейнера;
  • у NaïveProxy вообще нет учёта трафика: Caddy не считает, кто сколько выкачал;
  • лимиты по объёму и срокам приходится изобретать самому;
  • раздача конфигов — руками, никаких подписок;
  • два протокола = два несвязанных набора учёток.

Панель s-ui-x-extended закрывает это целиком. Она построена на форке ядра sing-box, в котором mieru и NaïveProxy реализованы нативно — и как inbound (приём клиентов), и как outbound (уход на выходное плечо). Каскад собирается правилами маршрутизации внутри панели.

Компонент Роль
s-ui-x-extended Веб-панель: пользователи, трафик, лимиты, подписки, правила
sing-box-extended Ядро. Встроено в бинарь панели — отдельного процесса sing-box нет

Важно для доверия к схеме: mieru-inbound в этом форке использует официальную библиотеку enfein/mieru/v3/apis/server — ту же, на которой работает mita. Протокол идентичен, ранее розданные клиентские конфиги остаются валидными.

Панель ставится рядом с уже существующими сервисами (например, 3x-ui с VLESS/Trojan/Hysteria2) и их не трогает — важно лишь развести порты.

2.2. Установка

Все команды этапа выполняются на входном сервере.

ssh root@YOUR_ENTRY_HOST

Перед установкой освободите порты 2012-2022 и 443, если их занимали прежние сервисы:

ss -tulpn | grep -E ":(443|80|201[2-9]|202[0-2]) "

Установщик:

SUI_LANG=ru bash <(curl -Ls https://raw.githubusercontent.com/deposist/s-ui-x-extended/main/install.sh)

Он задаст несколько вопросов. Отвечать так:

Вопрос Ответ Почему
Продолжить настройку? y Иначе порты останутся дефолтными
Порт панели 2095 Значение по умолчанию, обычно свободно
Путь панели оставить пустым Сгенерируется случайный — так лучше
Порт подписки 2097 Не 2096! См. врезку ниже
Путь подписки оставить пустым
Сменить логин/пароль y и задать свои Иначе будут случайные

Грабля, стоящая отдельного упоминания. По умолчанию панель занимает под подписки порт 2096 — ровно тот, который использует 3x-ui для своих подписок. Если 3x-ui уже стоит на этом сервере, панель уйдёт в бесконечный цикл перезапусков с ошибкой:

journalctl -u s-ui -n 20 --no-pager
web server run http on[::]:2095
listen tcp :2096: bind: address already in use
s-ui.service: Main process exited, code=exited, status=1/FAILURE

Лечится сменой порта:

systemctl stop s-ui && /usr/local/s-ui/sui setting -subPort 2097 && systemctl start s-ui

Проверка:

systemctl is-active s-ui && /usr/local/s-ui/sui setting -show

2.3. Учётные данные и первый вход

Задать логин и пароль администратора можно в любой момент:

/usr/local/s-ui/sui admin -username YOUR_ADMIN -password YOUR_ADMIN_PASSWORD

Установщик оставляет файл со стартовым паролем — удалите его после первого входа:

rm -f /usr/local/s-ui/db/initial-admin.txt

Полезные команды управления:

s-ui                      # интерактивное меню
systemctl status s-ui     # состояние сервиса
journalctl -u s-ui -f     # логи ядра в реальном времени

2.4. Сертификаты и автопродление

Панель и подписки обязаны работать по HTTPS: через них ходят пароли администратора и клиентские конфигурации. NaïveProxy-инбаунду сертификат нужен по определению.

Используем acme.sh в режиме standalone — он поднимает собственный HTTP-сервер на порту 80 на время проверки.

curl https://get.acme.sh | sh -s email=your-email@example.com

Сертификат для NaïveProxy-входа:

/root/.acme.sh/acme.sh --issue -d YOUR_ENTRY_DOMAIN --standalone --keylength ec-256 --server letsencrypt

Установка с хуком, который перезапустит панель после каждого продления:

/root/.acme.sh/acme.sh --install-cert -d YOUR_ENTRY_DOMAIN --ecc \
  --fullchain-file /root/cert/YOUR_ENTRY_DOMAIN/fullchain.pem \
  --key-file /root/cert/YOUR_ENTRY_DOMAIN/privkey.pem \
  --reloadcmd "systemctl restart s-ui"

То же самое для домена панели:

/root/.acme.sh/acme.sh --issue -d YOUR_PANEL_DOMAIN --standalone --keylength ec-256 --server letsencrypt
/root/.acme.sh/acme.sh --install-cert -d YOUR_PANEL_DOMAIN --ecc \
  --fullchain-file /root/cert/YOUR_PANEL_DOMAIN/fullchain.pem \
  --key-file /root/cert/YOUR_PANEL_DOMAIN/privkey.pem \
  --reloadcmd "systemctl restart s-ui"

Проверить, что автопродление зарегистрировано:

/root/.acme.sh/acme.sh --list
crontab -l | grep acme

Про порт 80 — не пропускайте. В standalone-режиме (Le_Webroot='no' в конфигурации домена) acme.sh поднимает свой сервер на 80-м порту. Если порт занят — например, тем же Caddy — продление молча провалится, и вы узнаете об этом, когда сертификат истечёт. Реальный случай из эксплуатации разобран в разделе Диагностика.

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

nano /root/.acme.sh/YOUR_DOMAIN_ecc/YOUR_DOMAIN.conf

Значение хранится в base64 внутри обёртки __ACME_BASE64__START_…__ACME_BASE64__END_. Такой способ хорош тем, что не запускает reload немедленно — в отличие от повторного --install-cert, который тут же дёрнет перезапуск и разорвёт активные соединения.

2.5. Включение HTTPS для панели и подписок

Через интерфейс: Settings → поля сертификата для панели и для подписки. Через API (см. 2.9) — объект settings с полями:

Поле Значение
webCertFile / webKeyFile Пути к сертификату панели
webDomain YOUR_PANEL_DOMAIN
subCertFile / subKeyFile Пути к сертификату подписок
subURI https://YOUR_PANEL_DOMAIN:2097/sub/

После перезапуска панель доступна только по HTTPS:

systemctl restart s-ui
curl -s -o /dev/null -w "%{http_code}\n" https://YOUR_PANEL_DOMAIN:2095/app/

Ответ 307 (редирект на форму входа) означает, что всё поднялось.

2.6. Outbound'ы: плечи каскада

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

Outbound mieru (раздел Outbounds → добавить, тип mieru):

Поле Значение
tag to-exit-mieru
server YOUR_EXIT_IP
server_ports 2012-2022
transport TCP
username / password ru-bridge / YOUR_BRIDGE_PASSWORD
multiplexing MULTIPLEXING_HIGH

Outbound naive (тип naive):

Поле Значение
tag to-exit-naive
server / server_port YOUR_EXIT_DOMAIN / 443
username / password ru-chain / YOUR_CHAIN_PASSWORD
tls включён, server_name = YOUR_EXIT_DOMAIN

После сохранения в логе должно появиться:

journalctl -u s-ui -n 20 --no-pager | grep -E "mieru|naive"
outbound/mieru[to-exit-mieru]mieru client is started
outbound/naive[to-exit-naive]NaiveProxy started, version: 148.0.7778.96

Строка с версией NaiveProxy подтверждает, что ядро подняло настоящий Cronet — сетевой стек Chromium, а не самодельную реализацию HTTP/2.

2.7. TLS-запись и инбаунды

NaïveProxy-инбаунду нужен сертификат. В разделе TLS создаётся запись:

Поле Значение
name YOUR_ENTRY_DOMAIN
server.certificate_path /root/cert/YOUR_ENTRY_DOMAIN/fullchain.pem
server.key_path /root/cert/YOUR_ENTRY_DOMAIN/privkey.pem
server.server_name YOUR_ENTRY_DOMAIN

Теперь инбаунды — точки входа для клиентов.

Inbound mieru:

Поле Значение
tag mieru-in
listen ::
listen_ports 2012-2022
transport TCP
traffic_pattern см. 2.8

Inbound naive:

Поле Значение
tag naive-in
listen / listen_port :: / 443
tls_id id созданной TLS-записи

Ядро не запустится, пока нет ни одного пользователя. Сразу после создания mieru-инбаунда в логе появится:

start sing-box err: initialize inbound[0] mieru-in failed to build mieru server config:
failed to validate mieru options: users is empty

Это нормально. Пользователи в этой панели хранятся отдельно от инбаундов, поэтому ядро поднимется только после создания первого клиента — см. Этап 3. Пугаться и переделывать инбаунд не нужно.

2.8. Traffic pattern для mieru

Плечо «клиент → входной сервер» — единственное, которое видит ТСПУ, поэтому здесь имеет смысл включить подгонку статистики (см. теорию выше). В панели traffic_pattern задаётся строкой base64, а не JSON.

Готовое значение для комбинации «дробление TCP + печатаемый nonce»:

EAAaBAgBEAoiBggBGAYgCA==

Оно соответствует такой конфигурации:

{
    "unlockAll": false,
    "tcpFragment": { "enable": true, "maxSleepMs": 10 },
    "nonce": { "type": "NONCE_TYPE_PRINTABLE", "minLen": 6, "maxLen": 8 }
}

Собственный вариант получается так: поднимите временный контейнер mita с нужным trafficPattern в server.json и выполните экспорт:

docker exec mita-exit mita export traffic-pattern

Проверить, что строка означает именно то, что задумано:

docker exec mita-exit mita explain traffic-pattern "EAAaBAgBEAoiBggBGAYgCA=="

Клиенту знать этот паттерн не нужно — режим диктует сервер.

2.9. Правила маршрутизации — собственно каскад

Без правил трафик клиентов уйдёт напрямую с входного сервера, и весь смысл каскада потеряется. Нужно связать каждый inbound со своим outbound. В разделе Configroute.rules:

{
  "route": {
    "rules": [
      { "action": "sniff" },
      { "protocol": ["dns"], "action": "hijack-dns" },
      { "inbound": ["mieru-in"], "action": "route", "outbound": "to-exit-mieru" },
      { "inbound": ["naive-in"], "action": "route", "outbound": "to-exit-naive" }
    ]
  }
}

Два последних правила и есть каскад: всё, что пришло на mieru-вход, уходит mieru-плечом; всё, что пришло на naive-вход, — naive-плечом. Протоколы не смешиваются, каждый идёт своей цепочкой до конца.

2.10. Настройка через API

Всё вышеописанное делается и без веб-интерфейса — полезно для автоматизации и повторного развёртывания.

Авторизация принимает только form-encoded поля user и pass (JSON и пары username/password не работают):

curl -sk -c /tmp/sui.jar -X POST "https://YOUR_PANEL_DOMAIN:2095/app/api/login" \
     -d "user=YOUR_ADMIN&pass=YOUR_ADMIN_PASSWORD"

Любой изменяющий запрос требует CSRF-токен:

CSRF=$(curl -sk -b /tmp/sui.jar -c /tmp/sui.jar "https://YOUR_PANEL_DOMAIN:2095/app/api/csrf" \
       | sed -E 's/.*"token":"([^"]+)".*/\1/')

Сохранение объектов — единый эндпоинт /app/api/save:

curl -sk -b /tmp/sui.jar -X POST "https://YOUR_PANEL_DOMAIN:2095/app/api/save" \
  -H "X-CSRF-Token: $CSRF" \
  --data-urlencode "object=outbounds" \
  --data-urlencode "action=new" \
  --data-urlencode 'data={"type":"mieru","tag":"to-exit-mieru","server":"YOUR_EXIT_IP","server_ports":["2012-2022"],"transport":"TCP","username":"ru-bridge","password":"YOUR_BRIDGE_PASSWORD","multiplexing":"MULTIPLEXING_HIGH"}'
Параметр Значения
object inbounds, outbounds, clients, tls, config, settings
action new, edit, del
data JSON объекта; при edit обязательно поле id

Чтение — GET-эндпоинты /app/api/{load,inbounds,outbounds,clients,tls,config,settings,stats,onlines}.


Этап 3. Пользователи и раздача

3.1. Модель: один клиент — оба протокола

В панели пользователь и инбаунд разделены. Клиент — это самостоятельная запись с набором учётных данных по протоколам, привязанная к одному или нескольким инбаундам. Отсюда удобное свойство: одна запись выдаёт человеку сразу оба канала — mieru и NaïveProxy — с общим лимитом трафика, общим сроком и одной подпиской.

Поле клиента Назначение
name Имя (оно же логин mieru)
config.mieru {"name": …, "password": …}
config.naive {"username": …, "password": …}
inbounds Список id инбаундов, к которым привязан клиент
volume Лимит трафика в байтах, 0 — без лимита
expiry Срок действия (unix-время), 0 — бессрочно
subSecret UUID подписки, генерируется автоматически
Многопользовательский режим: один сервер обслуживает несколько учёток одновременно
Многопользовательский режим: один сервер обслуживает несколько учёток одновременно

Один сервер обслуживает неограниченное число учёток — отдельный контейнер на каждого пользователя, как в схемах «один сервер — один клиент», здесь не нужен.

Обратите внимание на разные имена полей: у mieru — name, у naive — username. Если ошибиться, пользователь просто не попадёт в конфигурацию соответствующего инбаунда: панель пропустит запись без нужного поля, а протокол останется без учёток.

3.2. Создание клиента

В интерфейсе: Clients → добавить, задать имя, отметить оба инбаунда, при необходимости выставить лимит и срок.

Через API:

curl -sk -b /tmp/sui.jar -X POST "https://YOUR_PANEL_DOMAIN:2095/app/api/save" \
  -H "X-CSRF-Token: $CSRF" \
  --data-urlencode "object=clients" \
  --data-urlencode "action=new" \
  --data-urlencode 'data={"enable":true,"name":"user01","config":{"mieru":{"name":"user01","password":"MIERU_PASSWORD"},"naive":{"username":"user01","password":"NAIVE_PASSWORD"}},"inbounds":[1,2],"volume":0,"expiry":0,"desc":"первый клиент"}'

Сразу после создания первого клиента ядро наконец поднимется полностью:

journalctl -u s-ui -n 15 --no-pager | grep started
inbound/mieru[mieru-in]mieru server is started
inbound/naive[naive-in]tcp server started at [::]:443
inbound/naive[naive-in]udp server started at [::]:443
sing-box started

Проверить занятые порты:

ss -tlnp | grep -E ":(443|201[2-9]|202[0-2]) "

3.3. Подписка

У каждого клиента свой адрес подписки:

https://YOUR_PANEL_DOMAIN:2097/sub/<subSecret>

Форматы отдачи:

Запрос Что отдаёт Кому годится
без параметров base64 со списком ссылок (mierus://, http2://) NekoBox, Karing и подобные
?format=json Готовый конфиг sing-box с обоими outbound'ами, группами auto и proxy Клиенты на sing-box
?format=clash Бесполезен

Про Clash-формат. Он отдаёт пустой proxies: [], потому что Clash и Mihomo не знают ни mieru, ни naive в том виде, в каком их отдаёт панель. Не тратьте на него время — раздавайте base64-ссылки или JSON.

Проверка, что подписка жива:

curl -s -o /dev/null -w "%{http_code}\n" "https://YOUR_PANEL_DOMAIN:2097/sub/SUBSECRET"

Ответ 200 — всё в порядке.

Отдельно про кнопку проверки в интерфейсе. В окне выдачи конфигов («Delivery») кнопка проверки подписки может показывать Subscription URL test failed, хотя подписка полностью работоспособна. Причина — политика браузера: панель живёт на порту 2095, подписки на 2097, для браузера это разные источники, а сервер подписок не отдаёт заголовок Access-Control-Allow-Origin. Браузер отправляет запрос, но не даёт скрипту прочитать ответ — панель честно пишет об этом мелким шрифтом («HTTP success cannot be confirmed»). Клиентов это не касается: они ходят за подпиской напрямую, без ограничений CORS. Проверять доступность надо командой curl выше, а не кнопкой.

3.4. Какие ссылки отдавать клиенту

mieru — из подписки берётся штатно:

mierus://user01:MIERU_PASSWORD@YOUR_ENTRY_DOMAIN?port=2012-2022&profile=mieru-in&protocol=TCP

NaïveProxy — здесь есть нюанс. Панель генерирует ссылку в схеме http2:// с base64-частью, и NekoBox её не импортирует (схема жёстко зашита в генераторе, настройки нет). Рабочий формат для NekoBox и NekoRay нужно собрать вручную:

naive+https://user01:NAIVE_PASSWORD@YOUR_ENTRY_DOMAIN:443#naive

Сама панель такую ссылку понимает — можно проверить её конвертером:

curl -sk -b /tmp/sui.jar -X POST "https://YOUR_PANEL_DOMAIN:2095/app/api/linkConvert" \
  -H "X-CSRF-Token: $CSRF" \
  --data-urlencode "link=naive+https://user01:NAIVE_PASSWORD@YOUR_ENTRY_DOMAIN:443#naive"

Ответ — валидный naive-outbound с полями server, username, password, tls.


Этап 4. Клиентские приложения

4.1. Десктоп

Для mieru — Clash Verge Rev, Mihomo Party, NekoBox. Профиль дополняется блоком:

proxies:
  - name: mieru-entry
    type: mieru
    server: YOUR_ENTRY_DOMAIN
    port: 2022
    username: user01
    password: MIERU_PASSWORD
    multiplexing: MULTIPLEXING_HIGH

port: 2022одно число из диапазона 2012-2022, не диапазон: сервер слушает все 11 портов параллельно, клиент занимает один. Многие сборки mihomo не разбирают диапазон и молча ломаются.

Для NaïveProxy — официальный бинарь naive. Конфигурация:

{
  "listen": "socks://127.0.0.1:1080",
  "proxy": "https://user01:NAIVE_PASSWORD@YOUR_ENTRY_DOMAIN"
}
naive ./config.json

После запуска на 127.0.0.1:1080 появляется SOCKS5, который и указывается в браузере или системе.

4.2. Android

Протокол Как подключается
mieru Импортируется штатно из подписки или ссылкой mierus://. Работает в NekoBox, Karing, husi
NaïveProxy Требует отдельный плагин (naive-plugin): протоколу нужен Cronet — часть Chromium, которую не кладут в основной APK

Если плагин не установлен, ссылка не импортируется вовсе — и это не проблема конфигурации сервера. Порядок: сначала плагин, затем импорт ссылки в формате naive+https:// (не http2://).

Практический вывод: на телефонах основным каналом делайте mieru.

4.3. OpenWrt-роутер

Смысл: роутер держит постоянное подключение, а трафик-менеджер (например, podkop) заворачивает в него нужные домены — тогда туннель работает для всей домашней сети.

Вариант с mieru

ssh root@YOUR_ROUTER_HOST
cd /tmp && wget -q https://github.com/enfein/mieru/releases/download/v3.32.0/mieru_3.32.0_linux_arm64.tar.gz -O mieru.tar.gz
tar xzf mieru.tar.gz && mv mieru /usr/bin/mieru && chmod +x /usr/bin/mieru && rm -f /tmp/mieru.tar.gz /tmp/LICENSE /tmp/README.md

Архитектура большинства современных моделей — aarch64; для других (mipsel и т.п.) возьмите соответствующий артефакт из релизов.

Конфигурация /etc/mieru_client_config.json:

{
    "profiles": [
        {
            "profileName": "default",
            "user": { "name": "user01", "password": "MIERU_PASSWORD" },
            "servers": [
                {
                    "ipAddress": "YOUR_ENTRY_IP",
                    "portBindings": [ { "portRange": "2012-2022", "protocol": "TCP" } ]
                }
            ],
            "mtu": 1380,
            "multiplexing": { "level": "MULTIPLEXING_HIGH" }
        }
    ],
    "activeProfile": "default",
    "rpcPort": 8964,
    "socks5Port": 1090,
    "loggingLevel": "INFO"
}

На роутере не используйте mieru apply config. Клиент читает JSON напрямую через переменную MIERU_CONFIG_JSON_FILE, выставленную в init-скрипте. Если применить конфигурацию через RPC, состояние разъедется между JSON-файлом и persistent-хранилищем.

Вариант с NaïveProxy

Конфигурация /etc/naive/config.json:

{
  "listen": "socks://127.0.0.1:1080",
  "proxy": "https://user01:NAIVE_PASSWORD@YOUR_ENTRY_DOMAIN",
  "log": ""
}

Init-скрипт procd /etc/init.d/naive:

#!/bin/sh /etc/rc.common

START=95
STOP=01
USE_PROCD=1

start_service() {
    procd_open_instance
    procd_set_param command /usr/bin/naive /etc/naive/config.json
    procd_set_param respawn
    procd_set_param stdout 1
    procd_set_param stderr 1
    procd_close_instance
}
chmod +x /etc/init.d/naive && /etc/init.d/naive enable && /etc/init.d/naive start

START=95 — запуск после поднятия сети.

Подключение podkop

podkop не принимает ни mierus://, ни naive+https:// напрямую — он понимает только vless/ss/trojan/socks/hy2. Поэтому на роутере поднимается standalone-клиент с локальным SOCKS5, а в podkop указывается уже он:

socks5://127.0.0.1:1090     — для mieru
socks5://127.0.0.1:1080     — для naive

Проверка с роутера:

curl -s --max-time 20 -x socks5h://127.0.0.1:1090 https://ifconfig.me

Должен вернуться IP выходного сервера (или его egress), а не роутера и не входного сервера.


Финальная архитектура

  КЛИЕНТЫ                ВХОДНОЙ СЕРВЕР                     ВЫХОДНОЙ СЕРВЕР
                    (панель s-ui-x, нативно)              (Docker, без панели)

  mieru ──2012-2022──▶ inbound mieru ──┐
  (mierus://)          traffic_pattern │
                                       ├─▶ outbound mieru ──2012-2022──▶ mita
                                       │   (ru-bridge)                    │
  naive ─── :443 ────▶ inbound naive ──┤                                  ▼
  (naive+https://)     TLS + padding   │                            egress SOCKS5
                                       └─▶ outbound naive ─── :443 ──▶ Caddy
                                           (ru-chain)                     │
                       панель  :2095                                      ▼
                       подписки :2097                              WARP / прямой выход

Та же цепочка для NaïveProxy в виде схемы:

Участок Протокол Что видит наблюдатель
Клиент → входной сервер (mieru) mieru поверх TCP, без TLS Случайный либо текстоподобный поток без опознаваемых признаков
Клиент → входной сервер (naive) HTTPS + HTTP/2 CONNECT Chrome, открывающий обычный сайт
Входной → выходной (mieru) mieru поверх TCP Трафик между двумя серверами
Входной → выходной (naive) HTTPS + HTTP/2 CONNECT То же
Выходной → интернет через egress (WARP) либо напрямую IP выходного узла или Cloudflare

Клиентское ПО никогда не видит адрес выходного сервера: в конфигурации клиента фигурирует только входной.


Эксплуатация

Добавление пользователя

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

Отзыв доступа

Снять галочку enable или удалить клиента. Учётка исчезает из конфигурации инбаундов, активные сессии рвутся при следующем применении.

Лимиты и сроки

Поля volume (байты) и expiry (unix-время) у клиента. По исчерпании панель сама отключает доступ. Учёт ведётся на входе, поэтому работает и для NaïveProxy, который сам по себе трафик не считает.

Просмотр трафика

curl -sk -b /tmp/sui.jar "https://YOUR_PANEL_DOMAIN:2095/app/api/clients"

Поля up и down — байты. В интерфейсе то же самое видно в списке клиентов.

Обновление компонентов

s-ui update

На выходном сервере (смена версии mieru — в docker-compose.yml):

cd /opt/mieru-exit && docker compose up -d --build
cd /opt/caddy-naive && docker compose up -d --build

Логи

journalctl -u s-ui -f                      # входной сервер: панель и ядро
docker logs mita-exit --tail 50            # выходной: mieru
docker logs caddy-naive --tail 50          # выходной: naive

Резервная копия

tar czf /root/sui-backup-$(date +%F).tar.gz /usr/local/s-ui/db /root/cert

База панели — SQLite в /usr/local/s-ui/db/. В ней клиенты, инбаунды, правила; вместе с сертификатами этого достаточно для восстановления.


Диагностика

Проверка цепочек

Единственная надёжная проверка — пропустить реальный трафик и посмотреть, какой IP видит внешний сервис. Ожидается IP выходного сервера либо его egress.

NaïveProxy (запустить локальный клиент, затем):

curl -s --max-time 25 -x socks5h://127.0.0.1:1080 https://ifconfig.me

mieru:

curl -s --max-time 30 -x socks5h://127.0.0.1:1090 https://ifconfig.me

Частые проблемы

Панель циклически перезапускается: bind: address already in use

Порт подписок (по умолчанию 2096) занят другим сервисом — чаще всего подписками 3x-ui. Сменить:

systemctl stop s-ui && /usr/local/s-ui/sui setting -subPort 2097 && systemctl start s-ui

Ядро не стартует: failed to validate mieru options: users is empty

У mieru-инбаунда нет ни одного пользователя. Пользователи в панели хранятся отдельно, поэтому создайте клиента и привяжите его к инбаунду — ядро поднимется само. Инбаунд переделывать не нужно.

NaïveProxy: обрыв соединения, в логе missing naive padding

Так и должно быть. Инбаунд требует настоящий протокол NaïveProxy с padding-заголовком, поэтому обычная проверка вида

curl -x https://user:pass@YOUR_ENTRY_DOMAIN:443 https://ifconfig.me

будет отброшена: curl шлёт обычный HTTP/1.1 CONNECT. Это не поломка, а строгое соответствие протоколу (Caddy в такой ситуации был мягче и принимал обычный CONNECT). Проверять только настоящим клиентом naive.

Клиент mieru подключается, но трафика нет

Проверьте время на клиенте:

date -u

Расхождение более 4 минут с сервером ломает выработку ключа, и соединения падают молча. Настройте NTP.

Тест mieru показывает пустой ответ

mieru-клиенту нужно около 15 секунд на старт. Если проверять раньше, соединение ещё не установлено и curl вернёт пустоту — легко принять за поломку. Дайте клиенту подняться и повторите; заодно убедитесь, что SOCKS слушает:

ss -tlnp | grep 1090

Трафик уходит, но выходной IP — входного сервера

Не работают правила маршрутизации: трафик уходит direct вместо каскадного outbound. Проверьте секцию route.rules — правила inbound → outbound должны существовать и стоять после sniff и hijack-dns.

NaïveProxy: 308 Redirect вместо авторизации

Пароль не совпал, а probe_resistance маскирует это под обычный редирект. Частая причина — bcrypt-хэш в basic_auth на выходном сервере: плагин klzgrad/forwardproxy понимает только plaintext и сравнивает строку буквально.

mita в состоянии IDLE, порты не слушаются

mita run не открывает сокеты сам по себе:

docker exec mita-exit mita start

FATAL: getUid("mita") failed

В образе нет системных пользователей. Проверьте наличие строк adduser -H -D -g "" mita и adduser -H -D -g "" mieru в Dockerfile и пересоберите образ.

Сертификат не продлился, хотя acme.sh в cron

Самая коварная из проблем — она молчаливая. В standalone-режиме acme.sh поднимает собственный сервер на порту 80; если порт занят (веб-сервером, Caddy, чем угодно), проверка не проходит, и продление тихо срывается. Обнаруживается уже по истёкшему сертификату.

Проверить режим:

grep Le_Webroot /root/.acme.sh/YOUR_DOMAIN_ecc/YOUR_DOMAIN.conf

Le_Webroot='no' означает standalone. Убедитесь, что порт свободен:

ss -tlnp | grep ":80 "

Ручной прогон, показывающий реальную причину:

/root/.acme.sh/acme.sh --cron --home /root/.acme.sh

Даты и расписание:

/root/.acme.sh/acme.sh --list

Кнопка проверки подписки показывает ошибку

Ограничение CORS в браузере, а не поломка. Подробности — в разделе 3.3. Проверяйте curl-ом.


Шпаргалка

Входной сервер

systemctl status s-ui                    # состояние панели и ядра
systemctl restart s-ui                   # перезапуск
journalctl -u s-ui -f                    # логи
s-ui                                     # интерактивное меню
/usr/local/s-ui/sui setting -show        # порты и пути
/usr/local/s-ui/sui admin -username U -password P   # смена учётных данных
ss -tlnp | grep -E ":(443|209[57]|201[2-9]|202[0-2]) "   # занятые порты

Выходной сервер

docker ps                                        # что запущено
docker exec mita-exit mita status                # состояние mieru
docker exec mita-exit mita start                 # поднять из IDLE
docker exec mita-exit mita get users             # пользователи и трафик
docker logs caddy-naive --tail 50                # логи naive
cd /opt/mieru-exit && docker compose up -d --build
cd /opt/caddy-naive && docker compose up -d --build

Сертификаты

/root/.acme.sh/acme.sh --list                    # что ведётся и когда продление
/root/.acme.sh/acme.sh --cron --home /root/.acme.sh   # ручной прогон
openssl x509 -in /root/cert/DOMAIN/fullchain.pem -noout -dates
ss -tlnp | grep ":80 "                           # порт 80 обязан быть свободен

Проверка цепочек

curl -s --max-time 30 -x socks5h://127.0.0.1:1090 https://ifconfig.me   # mieru
curl -s --max-time 25 -x socks5h://127.0.0.1:1080 https://ifconfig.me   # naive
curl -s -o /dev/null -w "%{http_code}\n" "https://YOUR_PANEL_DOMAIN:2097/sub/SUBSECRET"

API панели

# вход (только form-encoded user/pass!)
curl -sk -c /tmp/sui.jar -X POST "https://PANEL:2095/app/api/login" -d "user=U&pass=P"
# CSRF-токен для любых изменений
CSRF=$(curl -sk -b /tmp/sui.jar -c /tmp/sui.jar "https://PANEL:2095/app/api/csrf" \
       | sed -E 's/.*"token":"([^"]+)".*/\1/')
# чтение
curl -sk -b /tmp/sui.jar "https://PANEL:2095/app/api/load"
curl -sk -b /tmp/sui.jar "https://PANEL:2095/app/api/clients"

Приложение А. Ручная установка без панели

Ниже — прежний способ развернуть входное плечо: mieru и NaïveProxy отдельными контейнерами, с правкой конфигураций руками. Материал перенесён из исходных руководств без изменений и остаётся полезным в трёх случаях:

  • нужно поднять входное плечо без панели (минимум зависимостей, всё в файлах);
  • нужно понять, что именно панель делает под капотом;
  • панель по каким-то причинам недоступна, а канал нужен здесь и сейчас.

Терминология исходных текстов: frontend — входной сервер (в основной части статьи), backend — выходной. Учётки клиентов в этом варианте живут в server.json и Caddyfile, учёта трафика у NaïveProxy нет, подписок нет — всё это как раз и появляется вместе с панелью.

А.1. mieru: ручная установка входного узла

Установка frontend-сервера (точка входа)

Все команды этого раздела выполняются на frontend-сервере по SSH.

Шаг 1. Подключение и подготовка

ssh root@YOUR_FRONTEND_HOST
mkdir -p /opt/mieru-frontend && cd /opt/mieru-frontend

Шаг 2. Dockerfile и entrypoint (одинаковые с backend)

Создайте оба файла так же, как на backend-сервере (см. шаги 4–5 раздела «Установка backend-сервера»). Содержимое идентично — образ универсальный, режим работы выбирается переменной окружения.

nano /opt/mieru-frontend/Dockerfile

(вставьте тот же Dockerfile, что на backend)

nano /opt/mieru-frontend/docker-entrypoint.sh

(вставьте тот же entrypoint)

chmod +x /opt/mieru-frontend/docker-entrypoint.sh

Шаг 3. Генерация пароля первой клиентской учётки

openssl rand -base64 24 | tr -d '=/+' | head -c 31

Скопируйте результат и впишите в таблицу как YOUR_CLIENT_PASSWORD. Это пароль учётки client01, который вы потом отдадите своему первому клиентскому устройству.

Шаг 4. Серверный конфиг (mita-frontend)

nano /opt/mieru-frontend/server.json

Замените YOUR_CLIENT_PASSWORD на значение из шага 3.

{
    "portBindings": [
        {"portRange": "2012-2022", "protocol": "TCP"}
    ],
    "users": [
        {"name": "client01", "password": "YOUR_CLIENT_PASSWORD"}
    ],
    "loggingLevel": "INFO",
    "mtu": 1380,
    "trafficPattern": {
        "unlockAll": false,
        "tcpFragment": {"enable": true, "maxSleepMs": 10},
        "nonce": {
            "type": "NONCE_TYPE_PRINTABLE",
            "minLen": 6,
            "maxLen": 8
        }
    },
    "egress": {
        "proxies": [
            {
                "name": "to-backend",
                "protocol": "SOCKS5_PROXY_PROTOCOL",
                "host": "127.0.0.1",
                "port": 1080
            }
        ],
        "rules": [
            {
                "ipRanges": ["*"],
                "domainNames": ["*"],
                "action": "PROXY",
                "proxyNames": ["to-backend"]
            }
        ]
    }
}

Сохраните и защитите файл.

chmod 600 /opt/mieru-frontend/server.json

Шаг 5. Клиентский конфиг (mieru-client)

nano /opt/mieru-frontend/client.json

Замените YOUR_BACKEND_PUBLIC_IP на публичный IP backend-сервера, YOUR_BRIDGE_PASSWORD — на пароль bridge-учётки, который вы создавали на backend в шаге 6.

{
    "profiles": [
        {
            "profileName": "default",
            "user": {
                "name": "frontend-bridge",
                "password": "YOUR_BRIDGE_PASSWORD"
            },
            "servers": [
                {
                    "ipAddress": "YOUR_BACKEND_PUBLIC_IP",
                    "portBindings": [
                        {"portRange": "2012-2022", "protocol": "TCP"}
                    ]
                }
            ],
            "mtu": 1380,
            "multiplexing": {"level": "MULTIPLEXING_HIGH"},
            "handshakeMode": "HANDSHAKE_NO_WAIT"
        }
    ],
    "activeProfile": "default",
    "rpcPort": 8964,
    "socks5Port": 1080,
    "loggingLevel": "INFO",
    "socks5ListenLAN": false,
    "httpProxyListenLAN": false
}

Сохраните и защитите файл.

chmod 600 /opt/mieru-frontend/client.json

Шаг 6. docker-compose.yml на frontend

nano /opt/mieru-frontend/docker-compose.yml

Вставьте.

services:
  mieru-client:
    build:
      context: .
      args:
        MIERU_VERSION: "3.32.0"
    image: mieru-bundle:3.32.0
    container_name: mieru-client
    restart: unless-stopped
    network_mode: host
    environment:
      MIERU_MODE: mieru
      MIERU_CONFIG: /srv/client.json
    volumes:
      - ./client.json:/srv/client.json:ro
      - mieru-client-state:/etc/mieru
      - mieru-client-runtime:/var/run/mieru
      - mieru-client-data:/var/lib/mieru
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  mita:
    image: mieru-bundle:3.32.0
    container_name: mita-frontend
    restart: unless-stopped
    network_mode: host
    depends_on:
      - mieru-client
    environment:
      MIERU_MODE: mita
      MIERU_CONFIG: /srv/server.json
    volumes:
      - ./server.json:/srv/server.json:ro
      - mita-state:/etc/mita
      - mita-runtime:/var/run/mita
      - mita-data:/var/lib/mita
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  mieru-client-state:
  mieru-client-runtime:
  mieru-client-data:
  mita-state:
  mita-runtime:
  mita-data:

Сохраните.

Шаг 7. Сборка и запуск

docker compose -f /opt/mieru-frontend/docker-compose.yml build
docker compose -f /opt/mieru-frontend/docker-compose.yml up -d
sleep 12

Шаг 8. Проверка

docker logs mieru-client --tail 10

Должна быть строка mieru: config применён и затем запуск mieru run.

docker logs mita-frontend --tail 10

Должно быть RUNNING и mita start: OK.

docker exec mieru-client mieru status

Ожидание: mieru client is running.

docker exec mita-frontend mita status

Ожидание: mita server status is "RUNNING".

Шаг 9. Сквозной тест каскада

С frontend-сервера обратитесь во внешний интернет через локальный SOCKS5 mieru-client. Это эквивалент того, как пойдёт реальный клиентский запрос.

curl -sS --max-time 30 --proxy socks5h://127.0.0.1:1080 https://icanhazip.com

Ожидание: IP вашего egress-узла. Например, IP Cloudflare WARP, если на backend настроен такой outbound. Команда не должна возвращать публичный IP frontend-сервера. Контрольный замер прямого IP frontend для сравнения.

curl -sS --max-time 5 https://icanhazip.com

Если первый IP отличается от второго — каскад работает.



А.2. NaïveProxy: ручная установка входного узла

Здесь входной сервер описан как «RU сервер (Entry Node)»: Caddy принимает клиентов, а отдельный демон naive держит второе плечо каскада до выходного сервера.

Часть 2: Настройка RU сервера (Entry Node)

Цель: на RU сервере поднять два компонента:

  1. Caddy (Docker) — принимает соединения от клиентов (роутеров), пробрасывает через локальный naive daemon
  2. naive daemon (systemd) — клиент, который соединяется с EU Caddy по протоколу NaïveProxy

2.1 Установка Docker

Те же команды, что и для EU сервера:

apt-get update && apt-get upgrade -y
apt-get install -y ca-certificates curl gnupg lsb-release

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) \
  signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  > /etc/apt/sources.list.d/docker.list

apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

docker --version && docker compose version

2.2 Структура каталогов и файлы Docker

mkdir -p /opt/caddy-naive/www
cd /opt/caddy-naive

Dockerfile идентичен EU серверу:

cat > /opt/caddy-naive/Dockerfile << 'EOF'
FROM golang:1.25-alpine AS builder
RUN apk add --no-cache git
RUN go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
RUN xcaddy build \
    --with github.com/caddyserver/forwardproxy=github.com/klzgrad/forwardproxy@naive

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /go/caddy /usr/local/bin/caddy
EXPOSE 80 443
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
EOF
cat > /opt/caddy-naive/docker-compose.yml << 'EOF'
services:
  caddy-naive:
    build: .
    container_name: caddy-naive
    restart: unless-stopped
    network_mode: host
    environment:
      - XDG_DATA_HOME=/data
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./www:/var/www/html:ro
      - caddy_data:/data
      - caddy_logs:/var/log/caddy

volumes:
  caddy_data:
  caddy_logs:
EOF
cat > /opt/caddy-naive/www/index.html << 'EOF'
<!DOCTYPE html>
<html><head><title>Welcome</title></head>
<body><h1>Welcome</h1><p>Nothing to see here.</p></body>
</html>
EOF

2.3 Создание Caddyfile RU сервера

Ключевое отличие от EU: директива upstream socks5://127.0.0.1:10808, которая перенаправляет входящие CONNECT-запросы в локальный naive daemon.

cat > /opt/caddy-naive/Caddyfile << 'EOF'
{
  order forward_proxy before file_server
}

:443, your-naive-ru-domain.example.com {
  tls YOUR_EMAIL@example.com
  forward_proxy {
    basic_auth RU_PROXY_USER RU_PROXY_PASSWORD
    upstream socks5://127.0.0.1:10808
    hide_ip
    hide_via
    probe_resistance
  }
  file_server {
    root /var/www/html
  }
}
EOF
Почему upstream socks5, а не https?
Вариант upstream https://EU_CADDY не работает в данной схеме — Caddy при таком подходе "протекает" заголовки HTTP-ответа в TLS-туннель, что ломает рукопожатие. Правильное решение — цепочка через SOCKS5 к бинарнику naive, который уже корректно реализует протокол NaïveProxy.

2.4 Сборка и запуск Caddy

cd /opt/caddy-naive
docker compose build
docker compose up -d
docker compose logs -f

Ожидаемый вывод:

{"level":"info","msg":"certificate obtained successfully","identifier":"your-naive-ru-domain.example.com"}

2.5 Установка naive daemon (клиент для EU)

2.5.1 Определение архитектуры сервера
uname -m
# x86_64 → нужен пакет для linux-x86_64-static
2.5.2 Скачивание бинарника naive

Перейди на страницу релизов: https://github.com/klzgrad/naiveproxy/releases

Найди архив для своей архитектуры. Для Ubuntu x86_64:

naiveproxy-vXXX-linux-x86_64.tar.xz
# Скачать (замени URL на актуальный из Releases)
cd /tmp
wget https://github.com/klzgrad/naiveproxy/releases/download/vXXX/naiveproxy-vXXX-linux-x86_64.tar.xz

# Распаковать (xzcat нужен если tar не поддерживает .xz напрямую)
tar -xf naiveproxy-vXXX-linux-x86_64.tar.xz
# или
xzcat naiveproxy-vXXX-linux-x86_64.tar.xz | tar -xf -

# Установить бинарник
cp naiveproxy-vXXX-linux-x86_64/naive /usr/bin/naive
chmod +x /usr/bin/naive

# Проверить
naive --version
2.5.3 Создание конфигурации naive daemon
mkdir -p /etc/naive

cat > /etc/naive/config.json << 'EOF'
{
  "listen": "socks://127.0.0.1:10808",
  "proxy": "https://EU_PROXY_USER:EU_PROXY_PASSWORD@your-naive-eu-domain.example.com",
  "log": ""
}
EOF
Что происходит
  • listen — naive слушает SOCKS5 на localhost:10808 (именно сюда смотрит upstream в Caddyfile RU)
  • proxy — подключается к EU Caddy по протоколу NaïveProxy (HTTP/2 CONNECT over TLS, трафик неотличим от обычного Chrome HTTPS)
2.5.4 Создание systemd-сервиса
cat > /etc/systemd/system/naive.service << 'EOF'
[Unit]
Description=NaïveProxy client daemon
After=network.target

[Service]
ExecStart=/usr/bin/naive /etc/naive/config.json
Restart=always
RestartSec=5
User=nobody

[Install]
WantedBy=multi-user.target
EOF
# Активировать и запустить
systemctl daemon-reload
systemctl enable naive
systemctl start naive

# Проверить статус
systemctl status naive

Ожидаемый вывод:

● naive.service - NaïveProxy client daemon
     Loaded: loaded (/etc/systemd/system/naive.service; enabled)
     Active: active (running)

2.6 Проверка цепочки с RU сервера

# Тест HTTP через naive daemon → EU → WARP
curl -s -x socks5h://127.0.0.1:10808 http://ifconfig.me
# Должен вернуть IP EU сервера или Cloudflare WARP IP

# Тест HTTPS
curl -s -x socks5h://127.0.0.1:10808 https://ifconfig.me
# Должен вернуть тот же результат


Приложение Б. Скрипты управления пользователями

Скрипты из прежней схемы, когда учётки правились в файлах. С панелью они не нужны — управление пользователями, лимитами и подписками делается через интерфейс или API. Оставлены для варианта из Приложения А и для тех, кто автоматизирует выходное плечо.

Б.1. mieru-users.sh

Скрипт добавляет, удаляет и показывает учётки на mita, попутно генерируя клиентские конфигурации и QR-коды.

Важная деталь реализации: при удалении вызывается mita delete user, а не только правка JSON. Если убрать пользователя лишь из файла, его активные сессии продолжат работать до истечения тайм-аута.

Исходный код mieru-users.sh

Полный текст скрипта приведён ниже. Скопируйте его целиком в /usr/local/bin/mieru-users.sh на frontend-сервере по инструкции выше.

#!/bin/bash
# mieru-users — управление пользователями mita
# Источник истины: /opt/mieru-frontend/server.json
# Применение через mita apply config (RPC к работающему daemon).

CONFIG_FILE="/opt/mieru-frontend/server.json"
COMPOSE_DIR="/opt/mieru-frontend"
COMPOSE_SVC="mita"
BACKUP_DIR="/opt/mieru-frontend/backups"
BACKUP_KEEP=10
SERVER_IP="YOUR_FRONTEND_PUBLIC_IP"

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'

check_root() {
    if [[ $EUID -ne 0 ]]; then
        echo -e "${RED}Запусти от root: sudo bash mieru-users.sh${NC}"; exit 1
    fi
}

check_deps() {
    for c in jq docker openssl; do
        if ! command -v "$c" >/dev/null 2>&1; then
            echo -e "${RED}Не найдено: $c${NC}"; exit 1
        fi
    done
    if [[ ! -f "$CONFIG_FILE" ]]; then
        echo -e "${RED}Не найден $CONFIG_FILE${NC}"; exit 1
    fi
}

mita_cli() {
    docker compose -f "$COMPOSE_DIR/docker-compose.yml" exec -T "$COMPOSE_SVC" mita "$@"
}

backup_config() {
    mkdir -p "$BACKUP_DIR"
    local stamp; stamp=$(date +%Y%m%d-%H%M%S)
    cp "$CONFIG_FILE" "$BACKUP_DIR/server-$stamp.json"
    ls -1t "$BACKUP_DIR"/server-*.json 2>/dev/null | tail -n +$((BACKUP_KEEP+1)) | xargs -r rm -f
}

apply_config() {
    if ! mita_cli apply config /srv/server.json 2>&1; then
        echo -e "${RED}mita apply config упал. Откатываюсь к последнему бэкапу.${NC}"
        local last; last=$(ls -1t "$BACKUP_DIR"/server-*.json 2>/dev/null | head -1)
        if [[ -n "$last" ]]; then
            cp "$last" "$CONFIG_FILE"
            mita_cli apply config /srv/server.json >/dev/null 2>&1 || true
        fi
        return 1
    fi
    return 0
}

press_enter() { echo ""; read -rp "Enter для возврата..."; }

generate_password() {
    openssl rand -base64 24 | tr -d '=/+' | head -c 31
}

generate_uri() {
    echo "mierus://${1}:${2}@${SERVER_IP}?port=2022&protocol=TCP&mtu=1380&multiplexing=MULTIPLEXING_HIGH&handshake-mode=HANDSHAKE_NO_WAIT"
}

generate_yaml() {
    cat <<EOF
proxies:
  - name: mieru-${1}
    type: mieru
    server: ${SERVER_IP}
    port: 2022
    username: ${1}
    password: ${2}
    multiplexing: MULTIPLEXING_HIGH
EOF
}

print_uri_for() {
    echo ""
    echo -e "${CYAN}=== Вариант 1: Clash / Mihomo YAML ===${NC}"
    echo -e "${BOLD}$(generate_yaml "$1" "$2")${NC}"
    echo ""
    echo -e "${CYAN}=== Вариант 2: mierus:// URI ===${NC}"
    echo -e "${BOLD}  $(generate_uri "$1" "$2")${NC}"
    echo ""
    echo -e "${CYAN}user=${BOLD}${1}${NC}, pwd=${BOLD}${2}${NC}, host=${BOLD}${SERVER_IP}${NC}, port=${BOLD}2022${NC}"
}

list_users() {
    echo ""
    local n; n=$(jq '.users | length' "$CONFIG_FILE")
    echo -e "${BOLD}${CYAN}=== Пользователей: $n ===${NC}"
    jq -r '.users[] | "  \(.name)\t\(.password[0:6])…"' "$CONFIG_FILE" | nl -w2 -s'. '
    echo ""
    echo -e "${BOLD}${CYAN}=== Активные сессии ===${NC}"
    mita_cli get connections 2>&1 | head -20
    press_enter
}

add_user() {
    echo ""; read -rp "Имя пользователя: " name
    name=$(echo "$name" | tr -d ' ')
    [[ -z "$name" ]] && { echo "Пусто."; press_enter; return; }
    if jq -e --arg n "$name" '.users[] | select(.name == $n)' "$CONFIG_FILE" >/dev/null; then
        echo "Уже есть."; press_enter; return
    fi
    read -rp "Пароль (Enter — сгенерировать): " pwd
    [[ -z "$pwd" ]] && pwd=$(generate_password)

    backup_config
    local tmp; tmp=$(mktemp)
    jq --arg n "$name" --arg p "$pwd" '.users += [{"name":$n,"password":$p}]' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE"

    if apply_config; then
        echo -e "${GREEN}Пользователь $name добавлен.${NC}"
        print_uri_for "$name" "$pwd"
    fi
    press_enter
}

delete_user() {
    mapfile -t users < <(jq -r '.users[].name' "$CONFIG_FILE")
    [[ ${#users[@]} -eq 0 ]] && { echo "Пусто."; press_enter; return; }
    for i in "${!users[@]}"; do echo "  $((i+1))) ${users[$i]}"; done
    echo "  0) Отмена"; read -rp "Номер: " n
    [[ "$n" == "0" || -z "$n" ]] && return
    local target="${users[$((n-1))]}"
    read -rp "Удалить '$target'? (y/N): " yn
    [[ ! "$yn" =~ ^[Yy]$ ]] && return

    mita_cli delete user "$target" 2>&1
    backup_config
    local tmp; tmp=$(mktemp)
    jq --arg n "$target" 'del(.users[] | select(.name == $n))' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE"
    apply_config && echo -e "${GREEN}Удалён.${NC}"
    press_enter
}

show_uri() {
    mapfile -t users < <(jq -r '.users[].name' "$CONFIG_FILE")
    [[ ${#users[@]} -eq 0 ]] && { echo "Пусто."; press_enter; return; }
    for i in "${!users[@]}"; do echo "  $((i+1))) ${users[$i]}"; done
    read -rp "Номер: " n
    [[ -z "$n" ]] && return
    local user="${users[$((n-1))]}"
    local pwd; pwd=$(jq -r --arg n "$user" '.users[] | select(.name == $n) | .password' "$CONFIG_FILE")
    print_uri_for "$user" "$pwd"
    press_enter
}

server_status() {
    mita_cli status 2>&1
    echo
    mita_cli get connections 2>&1
    echo
    mita_cli get metrics 2>&1 | head -30
    press_enter
}

main_menu() {
    check_root; check_deps
    while true; do
        clear
        echo "╔══════════════════════════════════════╗"
        echo "║   mieru User Manager                 ║"
        echo "║   IP: $SERVER_IP"
        echo "╚══════════════════════════════════════╝"
        echo
        echo "  1) Список пользователей"
        echo "  2) Добавить"
        echo "  3) Удалить"
        echo "  4) URI клиента"
        echo "  5) Статус сервера"
        echo "  0) Выход"
        read -rp "Выбор: " opt
        case "$opt" in
            1) list_users ;; 2) add_user ;;
            3) delete_user ;; 4) show_uri ;;
            5) server_status ;; 0) exit 0 ;;
        esac
    done
}

main_menu


Б.2. naive-users

Аналог для NaïveProxy: правит basic_auth-строки в Caddyfile и перезапускает контейнер. Учёта трафика здесь нет и быть не может — Caddy его не ведёт.

4.5 Исходный код

▶ naive-users.sh — показать исходный код
#!/bin/bash
# =============================================================
# naive-users — управление пользователями NaïveProxy на RU сервере
# Caddyfile: /opt/caddy-naive/Caddyfile
# =============================================================

CADDYFILE="/opt/caddy-naive/Caddyfile"
CADDY_DIR="/opt/caddy-naive"
# Подставьте свой домен (не коммитьте реальные значения в публичный репозиторий)
DOMAIN="your-naive-ru-domain.example.com"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

# --- Вспомогательные функции ---

check_root() {
    if [[ $EUID -ne 0 ]]; then
        echo -e "${RED}Ошибка: запусти скрипт от root (sudo bash naive-users.sh)${NC}"
        exit 1
    fi
}

check_caddyfile() {
    if [[ ! -f "$CADDYFILE" ]]; then
        echo -e "${RED}Ошибка: Caddyfile не найден: $CADDYFILE${NC}"
        exit 1
    fi
}

# Возвращает массив пользователей из Caddyfile
get_users() {
    grep -oP '(?<=basic_auth )\S+' "$CADDYFILE"
}

# Возвращает пароль пользователя
get_password() {
    local user="$1"
    grep "basic_auth $user " "$CADDYFILE" | awk '{print $3}'
}

restart_caddy() {
    echo -e "${YELLOW}Перезапуск Caddy...${NC}"
    cd "$CADDY_DIR" && docker compose restart caddy-naive > /dev/null 2>&1
    if [[ $? -eq 0 ]]; then
        echo -e "${GREEN}Caddy перезапущен успешно.${NC}"
    else
        echo -e "${RED}Ошибка при перезапуске Caddy. Проверь: cd $CADDY_DIR && docker compose logs${NC}"
    fi
}

press_enter() {
    echo ""
    read -rp "Нажми Enter для возврата в меню..."
}

# --- Функции меню ---

list_users() {
    echo ""
    echo -e "${BOLD}${CYAN}=== Список клиентов ===${NC}"
    mapfile -t users < <(get_users)
    if [[ ${#users[@]} -eq 0 ]]; then
        echo -e "${YELLOW}Клиентов нет.${NC}"
    else
        for i in "${!users[@]}"; do
            echo -e "  ${BOLD}$((i+1)).${NC} ${users[$i]}"
        done
        echo ""
        echo -e "  Всего: ${#users[@]} клиент(ов)"
    fi
    press_enter
}

add_user() {
    echo ""
    echo -e "${BOLD}${CYAN}=== Добавить клиента ===${NC}"

    read -rp "Имя пользователя: " username
    username=$(echo "$username" | tr -d ' ')

    if [[ -z "$username" ]]; then
        echo -e "${RED}Имя не может быть пустым.${NC}"
        press_enter
        return
    fi

    # Проверка — уже существует?
    if grep -q "basic_auth $username " "$CADDYFILE"; then
        echo -e "${RED}Пользователь '$username' уже существует.${NC}"
        press_enter
        return
    fi

    read -rp "Пароль (Enter = сгенерировать автоматически): " password
    if [[ -z "$password" ]]; then
        password=$(openssl rand -base64 16 | tr -d '=/+' | head -c 20)
        echo -e "  Сгенерирован пароль: ${BOLD}${password}${NC}"
    fi

    # Вставить строку basic_auth перед первой строкой upstream или probe_resistance
    sed -i "/probe_resistance/i\\    basic_auth $username $password" "$CADDYFILE"

    echo -e "${GREEN}Пользователь '${username}' добавлен.${NC}"
    restart_caddy

    echo ""
    echo -e "${BOLD}Конфиг для клиента:${NC}"
    print_config_for "$username" "$password"
    press_enter
}

delete_user() {
    echo ""
    echo -e "${BOLD}${CYAN}=== Удалить клиента ===${NC}"
    mapfile -t users < <(get_users)

    if [[ ${#users[@]} -eq 0 ]]; then
        echo -e "${YELLOW}Нет клиентов для удаления.${NC}"
        press_enter
        return
    fi

    for i in "${!users[@]}"; do
        echo -e "  ${BOLD}$((i+1)).${NC} ${users[$i]}"
    done
    echo "  0. Отмена"
    echo ""
    read -rp "Выбери номер для удаления: " choice

    if [[ "$choice" == "0" || -z "$choice" ]]; then
        return
    fi

    if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#users[@]} )); then
        echo -e "${RED}Неверный номер.${NC}"
        press_enter
        return
    fi

    target="${users[$((choice-1))]}"
    read -rp "Удалить пользователя '${target}'? (y/N): " confirm
    if [[ "$confirm" =~ ^[Yy]$ ]]; then
        sed -i "/basic_auth $target /d" "$CADDYFILE"
        echo -e "${GREEN}Пользователь '${target}' удалён.${NC}"
        restart_caddy
    else
        echo "Отменено."
    fi
    press_enter
}

print_config_for() {
    local user="$1"
    local pass="$2"
    echo ""
    echo -e "${BOLD}┌─────────────────────────────────────────────┐${NC}"
    echo -e "${BOLD}│  config.json для naive (ПК / роутер)        │${NC}"
    echo -e "${BOLD}└─────────────────────────────────────────────┘${NC}"
    echo '{
  "listen": "socks://127.0.0.1:1080",
  "proxy": "https://'"$user"':'"$pass"'@'"$DOMAIN"'",
  "log": ""
}'
    echo ""
    echo -e "${CYAN}Путь на роутере OpenWrt:${NC}  /etc/naive/config.json"
    echo -e "${CYAN}Путь на Windows ПК:${NC}       рядом с naive.exe"
    echo -e "${CYAN}SOCKS5 адрес для podkop:${NC}  socks5://127.0.0.1:1080"
}

show_config() {
    echo ""
    echo -e "${BOLD}${CYAN}=== Показать конфиг клиента ===${NC}"
    mapfile -t users < <(get_users)

    if [[ ${#users[@]} -eq 0 ]]; then
        echo -e "${YELLOW}Нет клиентов.${NC}"
        press_enter
        return
    fi

    for i in "${!users[@]}"; do
        echo -e "  ${BOLD}$((i+1)).${NC} ${users[$i]}"
    done
    echo "  0. Отмена"
    echo ""
    read -rp "Выбери номер клиента: " choice

    if [[ "$choice" == "0" || -z "$choice" ]]; then
        return
    fi

    if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#users[@]} )); then
        echo -e "${RED}Неверный номер.${NC}"
        press_enter
        return
    fi

    local user="${users[$((choice-1))]}"
    local pass
    pass=$(get_password "$user")
    echo -e "Клиент: ${BOLD}${user}${NC}"
    print_config_for "$user" "$pass"
    press_enter
}

# --- Главное меню ---

main_menu() {
    check_root
    check_caddyfile

    while true; do
        clear
        echo -e "${BOLD}${CYAN}"
        echo "╔══════════════════════════════════════╗"
        echo "║   NaïveProxy User Manager (RU)       ║"
        echo "║   Домен: $DOMAIN   ║"
        echo "╚══════════════════════════════════════╝"
        echo -e "${NC}"

        mapfile -t users < <(get_users)
        echo -e "  Активных клиентов: ${BOLD}${#users[@]}${NC}"
        echo ""
        echo -e "  ${BOLD}1.${NC} Список клиентов"
        echo -e "  ${BOLD}2.${NC} Добавить клиента"
        echo -e "  ${BOLD}3.${NC} Удалить клиента"
        echo -e "  ${BOLD}4.${NC} Показать конфиг клиента"
        echo -e "  ${BOLD}0.${NC} Выход"
        echo ""
        read -rp "Выбор: " option

        case "$option" in
            1) list_users ;;
            2) add_user ;;
            3) delete_user ;;
            4) show_config ;;
            0) echo "Выход."; exit 0 ;;
            *) echo -e "${RED}Неверный выбор.${NC}"; sleep 1 ;;
        esac
    done
}

main_menu



См. также