Mieru и NaïveProxy: каскадные туннели под управлением панели s-ui-x
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-паттернов.

| Параметр | Значение |
|---|---|
| Алгоритм | 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. Снаружи невозможно определить ни длину полезной нагрузки, ни фазу сессии — видны только зоны паддинга и шифр-блобы.

Требование к синхронизации часов
Ключ шифрования не передаётся по каналу и нигде не хранится: он каждый раз вырабатывается из пары логин+пароль и текущего времени. Отсюда неочевидное требование:
Расхождение системного времени клиента и сервера не должно превышать 4 минуты. При большем расхождении ключи не сойдутся, и подключения будут молча падать — без внятной ошибки в логах. На серверах и на клиентском устройстве обязан работать NTP.
Traffic pattern: подгонка статистики под «обычный» трафик
mieru умеет подменять статистические признаки шифр-потока:
| Опция | Что делает | Накладные расходы |
|---|---|---|
tcpFragment
|
Дробит TCP-пакеты на мелкие куски и вставляет случайную задержку. Ломает узнаваемость «один большой шифр-стрим» | |
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. Серверная часть притворяется обычным веб-сервером.
Механика:
- Клиент открывает HTTPS-соединение с сервером — точь-в-точь как Chrome.
- Внутри соединения по HTTP/2 CONNECT прокидывается туннель.
- Сервер обрабатывает обычные запросы как веб-сервер, а туннельные — переправляет дальше.
- Если кто-то «постучится» без верных учётных данных, сервер ответит как обычный сайт — это probe_resistance.
Почему каскад из двух серверов

- Адрес выходного сервера никогда не виден клиентскому ПО. Если входной сервер заблокируют — выходной остаётся неизвестным и переиспользуется со следующим входным.
- Входной сервер хранит только учётки клиентов и не имеет финального 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. В разделе Config → route.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 builddocker compose -f /opt/mieru-frontend/docker-compose.yml up -dsleep 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 сервере поднять два компонента:
- Caddy (Docker) — принимает соединения от клиентов (роутеров), пробрасывает через локальный naive daemon
- 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 version2.2 Структура каталогов и файлы Docker
mkdir -p /opt/caddy-naive/www
cd /opt/caddy-naiveDockerfile идентичен 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"]
EOFcat > /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:
EOFcat > /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>
EOF2.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-static2.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 --version2.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 Исходный код
#!/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