# Веб-панель qeli — установка и использование > **Документация описывает 0.7.13** — последний выпущенный релиз. Что именно установлено > у вас, покажет `qeli --version`. Встроенная админка демона: профили, пользователи/группы, живые клиенты, identity-ключи и выдача `qeli://`-ссылок/QR. Запускается **внутри** `qeli server` (процесс-супервизор), управляет данными через тот же конфиг/users-файл и control-сокет. - **Self-contained:** CSS/JS/шрифты встроены в бинарь и отдаются с `/assets/*` — **никаких CDN в рантайме**, панель работает на сервере без исходящего интернета. - **Стек:** `axum` + `alpine.js`; REST `/api/*`; сессия — HMAC-кука (ключ = хеш admin-пароля, сервер не хранит состояние сессий). - **Языки:** переключатель RU/EN внизу сайдбара (по умолчанию English; выбор запоминается в браузере). > Справочник всех ключей `[web]` — [CONFIG.md](CONFIG.md#веб-панель-web). Здесь — > как поднять и как пользоваться. --- ## 1. Установка / включение Панель настраивается секцией `[web]` серверного конфига. Минимум для доступа **по внешнему IP** (безопасно): ```ini [web] enabled = true # или конкретный внешний IP (для self-signed SAN лучше IP) bind = 0.0.0.0 port = 8080 username = admin # ОБЯЗАТЕЛЕН на ЛЮБОМ bind, включая loopback — пусто = панель не стартует password_hash = $argon2id$v=19$m=...$... # встроенный HTTPS (rustls); cert self-signed авто. Заходить по https://, не http:// tls = true # (опц.) белый список источников. Отсутствует, пусто или "" = любой источник. # allowed_ips = 203.0.113.4, 10.0.0.0/8 # (опц.) хост для share-ссылок; ещё и разрешённый CSRF-origin public_host = vpn.example.com # (опц.) доп. браузерные origin'ы для доступа по LAN/домену/прокси без него панель грузится, но POST (логин/сохранения) отдаёт 403 # allowed_origins = 192.168.88.8:8080 ``` После рестарта сервера панель доступна на **`https://:`**. ### Admin-пароль (обязателен) Сервер хранит только **argon2id-хеш** (`web.password_hash`), не открытый пароль. **Fail-closed:** с пустым `password_hash` панель **не стартует ни на каком bind, включая loopback** (в лог — ошибка; VPN `:443` при этом работает — это отдельный процесс). До 0.7.12 loopback был исключением и отдавал ОТКРЫТУЮ панель — то есть полный админ-доступ любому локальному процессу и любому SSRF на хосте; если это действительно нужно, теперь это запрашивается явно через `web.insecure_no_auth = true`, и сервер предупреждает об этом при старте. Способы задать хеш: - **CLI `qeli set-web-password` (проще всего, для свежей установки без доступа в панель)** — генерирует/хеширует пароль, прописывает `web.username`/`password_hash` прямо в конфиг (комментарии сохраняются) и включает панель: ```bash qeli set-web-password # случайный пароль, печатается один раз qeli set-web-password --password 'PASS' # свой пароль qeli set-web-password --username admin --password 'PASS' --no-enable # только креды qeli set-web-password --config /etc/qeli/server.conf # путь по умолчанию ``` После — `systemctl restart qeli` (пароль виден только в момент генерации). - **В самой панели:** Config → Web → «Set admin password» (вводите пароль → хешируется в поле → Save) — когда доступ уже есть и нужно сменить пароль. Сохранение `[web]`-настроек через панель (админ-пароль/юзер, IP-allowlist, CSRF-origins) применяется **сразу, без рестарта** — смена пароля тут же инвалидирует текущую сессию. Рестарт нужен только для socket-полей (`bind`/`port`/`tls`). (CLI `set-web-password` выше — отдельный процесс, поэтому ему рестарт нужен.) - **CLI `argon2` (вручную):** `printf '%s' 'ВАШ_ПАРОЛЬ' | argon2 "$(head -c12 /dev/urandom|base64)" -id -t 3 -m 15 -p 1 -e` - **Через API** (только если панель уже доступна — то есть пароль уже задан либо стоит `web.insecure_no_auth = true`; на свежей установке без пароля панель не запущена, используйте `qeli set-web-password` выше): `curl -s localhost:8080/api/hash-password -H 'Content-Type: application/json' -d '{"password":"..."}'` ### TLS (`tls = true`) Панель сама отдаёт HTTPS (rustls, провайдер `ring`) — реверс-прокси не нужен. - **Self-signed (по умолчанию):** при пустых `tls_cert`/`tls_key` сертификат генерируется при старте и сохраняется в `/etc/qeli/web-tls-cert.pem` + `/etc/qeli/web-tls-key.pem` (ключ `0600`), SAN включает `bind`-адрес/IP, `localhost`, `127.0.0.1`. Браузер предупредит один раз (трафик шифруется) — можно принять/запинить. - **Свой сертификат** (без предупреждений, нужен домен): ```ini tls = true tls_cert = /etc/letsencrypt/live/vpn.example.com/fullchain.pem tls_key = /etc/letsencrypt/live/vpn.example.com/privkey.pem ``` - При `tls = true` к сессионной куке автоматически добавляется `Secure`. > Альтернатива публикации: оставить `bind = 127.0.0.1`, `tls = false` и ходить в > панель **через SSH-туннель** (`ssh -L 8080:127.0.0.1:8080 root@server`). Тогда > TLS не нужен — трафик шифрует SSH. ### IP-allowlist (`allowed_ips`) Самый сильный барьер для публичного bind: список CIDR/одиночных IP, которым разрешён доступ; остальным — `403`. Редактируется в Config → Web → «Source-IP allowlist». **Не забудьте включить свой текущий IP**, иначе сами получите `403`. **Как разрешить доступ отовсюду.** Все три записи ниже равнозначны — парсер конфига снимает окружающие кавычки, поэтому `""` это просто явный способ написать «пусто»: ```ini # allowed_ips = … ; ключа нет вовсе allowed_ips = ; ключ есть, значение пустое allowed_ips = "" ; то же самое, что строка выше ``` Без записей IP-фильтра нет, и защита держится на TLS + пароле + rate-limit. > **Голый `403` при простом ОТКРЫТИИ панели — это всегда allowlist.** Он применяется ко > всем маршрутам (страницы, ассеты, API) и отдаёт 403 с ПУСТЫМ телом. CSRF так сделать не > может: он пропускает `GET`/`HEAD`/`OPTIONS`, а его 403 несёт текстовое тело с названием > отклонённого origin. > > **Готча — дубликаты ключа складываются.** ВСЕ строки `allowed_ips` в `[web]` сливаются в > ОДИН список (а не «побеждает последняя»). Забытая выше строка `allowed_ips = 1.2.3.4` > держит фильтр включённым, даже если та строка, которую вы правите, выглядит пустой. Если > 403 необъясним — `grep -n allowed_ips /etc/qeli/server.conf` и проверьте лог старта: > `Web panel source-IP allowlist active (N entries)`; нет такой строки = фильтра нет. > Отклонённый запрос пишется как `panel: blocked request from (not in web.allowed_ips)`. ### Доступ по LAN-IP, домену или через reverse-proxy (CSRF-origin'ы) Чтобы вредоносная страница не могла заставить залогиненного админа отправить запрос, панель принимает **изменяющие** запросы (POST логина и любые сохранения) только если браузерный `Origin`/`Referer` совпадает с разрешённым хостом. **По умолчанию разрешены:** адрес `bind` и loopback (`127.0.0.1` / `localhost` / `[::1]`). Поэтому если открыть панель по **LAN-IP, домену или за reverse-proxy** (что угодно, кроме loopback) — страница грузится (`GET /login` → `200`), но `POST /login` и любые сохранения отдают **`403`**. Тело 403 теперь называет отклонённый origin и подсказывает, что именно добавить; в логе также: `CSRF: rejected POST … (origin/referer=…)`. Чинится — добавь этот адрес в разрешённые origin'ы в `[web]`, затем `systemctl restart qeli`: ```ini [web] # твой LAN-IP / домен — host или host:port allowed_origins = 192.168.88.8:8080 # public_host тоже принимается как origin; хост без порта совпадает и с портом bind ``` Либо, без правки конфига, открывай панель через SSH-туннель на loopback (он разрешён всегда): `ssh -L 8080:127.0.0.1:8080 root@<сервер>` → открыть `http://127.0.0.1:8080`. **За reverse-proxy на сабпасе** (например `https://host/qeli/`, а не в корне домена): задай `base_path = /qeli` в `[web]` и проксируй префикс **без среза**. Панель переустановит все свои ассеты, API-вызовы и редиректы под префикс и учтёт `X-Forwarded-Prefix`. Полный пример nginx — см. «Reverse-proxy sub-path» в [CONFIG.md](CONFIG.md). ### Что ещё обеспечивает безопасность (автоматически) - **Security-заголовки** на всех ответах: HSTS (при `tls`), `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy`, CSP (same-origin). - **CSRF** — изменяющие запросы (POST логина и любые сохранения `/api/*`) проверяются по `Origin`/`Referer`; принимаются только `bind`, loopback и заданные origin'ы (см. «Доступ по LAN-IP, домену или через reverse-proxy» выше). - **Анти-брутфорс** — жёсткая блокировка по source-IP + tarpit по username (чужой логин нельзя залочить перебором имени). - **Сессия** — кука `HttpOnly; SameSite=Strict; Path=/` (+`Secure` при TLS), токен HMAC-подписан хешем пароля (смена пароля рвёт все сессии). --- ## 2. Использование ### Вход `https://:` → логин `admin` + пароль. Внизу сайдбара (и в углу страницы входа) — **выпадающий список языка** (RU/EN). ### Dashboard - **Быстрый старт** — плитки режимов (REALITY / HTTPS-fake-tls / Obfuscated / QUIC): один клик создаёт готовый профиль (TUN/NAT/DNS/пул/обфускация), применяет и перезапускает сервер. Пресеты несут боевую **stealth-постуру** — Poisson flow-shaping вместо ~15-с heartbeat-маяка, MTU 1400 и stream bonding на TCP-режимах. **Mobile / LTE:** большой пост-квантовый хендшейк может попасть в чёрную дыру за path MTU < 1500; на сервере накати OS-тюнинг (внешний MSS-кламп + BBR / PMTU probing) — см. [CONFIG.md](CONFIG.md) → «sysctl + iptables». Установщик `install-qeli-server.sh` делает это автоматически. - **Живые клиенты** — кто подключён (профиль, IP, peer, сборка клиента, аптайм, трафик, лимит), действия **Kick** и **Set bandwidth**. Фильтр по профилю, автообновление 10с. Колонка **Client** показывает версию и платформу, которые сессия сообщает по туннелю (например `0.7.14 android`) — быстрый ответ на вопрос «кому ещё обновляться». Прочерк означает, что клиент их не сообщает: все сборки, вышедшие до этой возможности, и GUI-клиенты, пока в них не появится этот кадр. Значение **сообщает сам клиент, и сервер его не проверяет** — любой прошедший аутентификацию пир может написать что угодно, поэтому это метка, а не доказательство того, что реально запущено. ### Метрики хоста и туннеля на Dashboard Две карточки вверху страницы обновляются **раз в 2 с** (при скрытой вкладке поллинг останавливается и возобновляется при возврате). Данные готовит семплер супервизора: он снимает дешёвые счётчики `/proc` и агрегаты воркера **раз в секунду** и держит кольцевой буфер на 300 точек (= 5 минут). Эндпоинты — `/api/system` (последний снимок) и `/api/metrics` (история для графика); на запрос `/proc` не парсится. - **Host load** — CPU % (и число ядер), RAM % плюс абсолютные «использовано / всего», load average (1/5/15), процент занятого места на `/`, строка **qeli proc** (pid воркера дата-плейна, его CPU % и RSS), **WAN net** — ↓/↑ Мбит/с по физическим интерфейсам хоста (`lo`, `vpn*`, `tun*` исключены, чтобы туннель не считался дважды), **conns · uptime** — число установленных TCP-сокетов, число UDP-сокетов и аптайм хоста. Если семплер недоступен, в заголовке появляется «· unavailable». - **Tunnel throughput** — суммарная скорость по всем живым сессиям (↓ сервер→клиент, ↑ клиент→сервер) и график за последние 5 минут с подписью пикового значения. Кнопка **load** дополнительно накладывает CPU хоста (пунктир) и число клиентов (точки). Ниже — плитки *Connected clients / Active profiles / Total sent / Total received*, карточки профилей и таблица живых клиентов (обновление раз в 10 с). ### Действия над живым клиентом (Kick, Set bandwidth) В таблице живых клиентов у каждой строки две операции; обе уходят в дата-плейн через control-сокет и применяются сразу. - **Kick** (`POST /api/clients/{username}/kick`) — с подтверждением рвёт все сессии этого пользователя на указанном профиле. Если пользователь не подключён, панель ответит «user … not connected». - **Set bandwidth** (`POST /api/clients/{username}/bandwidth`) — лимит в Мбит/с, целое число, `0` = без лимита. Значение применяется к живым сессиям **и записывается в users-файл**, то есть переживает рестарт; если запись файла не удалась, панель прямо скажет, что лимит применён только к сессии и будет потерян при рестарте. Дробное, отрицательное или слишком большое значение отклоняется с ошибкой, а не превращается молча в «без лимита». ### Резервная копия и восстановление (Backup / Restore) Кнопки **⤓ Backup** и **⤒ Restore** в шапке карточки *Host load*. - **Backup** (`GET /api/backup`) — браузер скачивает `qeli-backup-.tar.gz`: это `tar czf` всего каталога **`/etc/qeli`**, то есть серверный конфиг, users-файл, **identity-ключи профилей**, `panel-secret.key`, `usage.json`, `notify.json`, клиентские профили, TLS-сертификат панели. Артефакты прошлых восстановлений (`.pre-restore-*`, `.restore-*`) исключаются, чтобы архив не вкладывался сам в себя. Файл уходит **с сервера на вашу машину** — это off-box копия. Если хотя бы один критичный файл (identity, `server.conf`, `panel-secret.key`, users) оказался нечитаемым, скачивание **отклоняется** с объяснением, а не отдаёт архив, который выглядит целым. > **В архиве секреты.** Приватные identity-ключи, argon2-хеши паролей, обратимо > зашифрованные пароли пользователей (`panel-secret.key` — это ключ **их** шифрования) > и клиентские профили с паролем в открытом виде. Храните архив как ключевой материал — > на шифрованном носителе, не в общем облаке и не в репозитории. > > **Чего в архиве НЕТ: ключа подписи сессий панели.** Это отдельный файл, и путается он > с `panel-secret.key` легко, потому что оба «ключи панели». Ключ сессий лежит в > `$STATE_DIRECTORY/session.key` — под systemd это `/var/lib/qeli/session.key`, вне > каталога `/etc/qeli`, который и попадает в архив; фолбэк без `StateDirectory` — > `/etc/qeli/.session_key`, и только в этом случае он в архив входит. Практический > эффект: после восстановления на другой машине все входы в панель придётся выполнить > заново. Это не потеря данных, но и не то, что стоит обнаружить в разгар аварии. - **Restore** (`POST /api/restore`) — выбираете `.tar.gz`, панель предупреждает, что текущие конфиг, пользователи и ключи будут **перезаписаны**, и заливает файл телом запроса. **Потолок тела запроса — 16 МиБ** (`DefaultBodyLimit` в `qeli/src/web/api/mod.rs`), больше — отказ. До распаковки архив проверяется: распакованный объём ≤ 64 МиБ и ≤ 5000 записей (защита от tar-бомбы), все пути строго внутри `qeli/` (без ведущего `/` и без `..`), только обычные файлы и каталоги — симлинки/хардлинки/спецфайлы отклоняются. - **Восстановление обратимо.** Перед публикацией сервер снимает снимок текущего состояния в `/etc/qeli/.pre-restore-<метка>.tgz` (`0600`, хранятся 5 последних). Если снимок снять не удалось, восстановление **отменяется** — иначе изменение стало бы необратимым. Откат: `tar xzf /etc/qeli/.pre-restore-<метка>.tgz -C /etc` и рестарт. - Распаковка идёт в staging-каталог, а не сразу в `/etc/qeli`: содержимое проверяется ещё раз (исполняемых файлов быть не должно; `post_up`/`post_down` и `password_command` **нельзя ввести или изменить** восстановлением — то же правило, что и у редактора конфига; серверный конфиг обязан пройти ту же валидацию профилей, что и при старте). Только после этого файлы переносятся атомарными `rename`. - Два одновременных восстановления не выполняются — второе получит отказ «another restore is already in progress». После успешного восстановления **нужен рестарт**, чтобы применить, и уходит уведомление *Конфиг восстановлен*. ### Быстрый старт (страница Quick start) Отдельная страница сайдбара — таблица из десяти режимов маскировки, каждый запускается кнопкой **Launch**: `reality-tls` (TCP 443, помечен «flagship»), `reality` (8443), `fake-tls` (8444), `obfs-ws` (8445), `obfs-none` (8446), `plain` (8447), `udp-fake-tls` (UDP 8448), `udp-quic` (8449), `udp-obfs` (8450), `obfs-awg` (TCP 8451). У каждой строки — транспорт, порт и одна фраза о том, что режим делает. Что делает **Launch**: 1. читает текущий конфиг и **проверяет порт**: если этот порт+транспорт уже занят ДРУГИМ профилем, появляется попап «Port already in use» со ссылкой в Config (запуск того же режима повторно просто заменяет его собственный профиль — это разрешено); 2. спрашивает подтверждение и собирает профиль поверх канонических дефолтов сервера (`/api/config/defaults`): bind `0.0.0.0:<порт>`, отдельный TUN `vpn`, отдельная подсеть `10.9..0/24` с пулом, DNS в туннеле, NAT-выход и стек обфускации под выбранный режим; 3. сохраняет конфиг (`PUT /api/config`) и перезапускает сервер. По завершении модалка показывает имя профиля и endpoint, а для режимов, которым они нужны, — сгенерированные **REALITY short_id** и **obfs pre-shared key** с кнопкой Copy: их надо прописать каждому клиенту. Там же напоминание, что клиенту требуется ещё и запиненный публичный ключ сервера (`qeli show-identity` либо Config → Global → Server identity keys), а для TCP-режимов — предупреждение про mobile/LTE и path MTU. Режимы независимы: каждый получает свой интерфейс, подсеть и порт, поэтому держать несколько одновременно — рекомендуемая продовая раскладка (клиент подключается тем портом, который проходит в его сети). ### Config (настройки) - **Вкладки сверху** — `Global` + по одной на каждый профиль (+ добавить). Внутри профиля **всё одной страницей** (без внутренних вкладок); сверху — **липкая якорная навигация** (Bind / TUN / Pool / Routing / DNS / DHCP / Obfuscation / Performance), прилеплена под шапкой. - **Профиль** — тумблер `enabled`, и секции со **всеми** полями: транспорт/bind/ identity-путь, TUN (вкл. `queues` multi-queue), пул IP + резервации, маршрутизация + NAT + pushed-routes, DNS + blocklist, DHCP (только для TAP — см. ниже), обфускация (mode/cipher/fronting, TLS-маскировка + SNI-пул, REALITY + `handrolled`/`peek`, padding, heartbeat, fragmentation, http2, traffic-norm, anti-fingerprint, QUIC, **multipath**), производительность (лимиты, rate-limit/new-session, TCP/TUN-буферы). - **Global** — Authentication (вкл. `bind_static_to_session` H-1), Web UI (вкл. TLS, allowlist, public_host, admin-пароль), Logging, **Server identity keys** (показ pinned-ключа каждого профиля + кнопка **Rotate**). - **Сохранение:** `Save to Disk` (запись в конфиг, применится при следующем рестарте) или `Apply & Restart` (сохранить и перезапустить сейчас). Вью `Form` / `JSON` / `Raw INI` (raw сохраняет дословно — комментарии целы). > **`Apply & Restart` требует прав на перезапуск сервиса.** Он выполняет > `systemctl restart `. **root**-сервис делает это напрямую; закалённый > **non-root** сервис `User=qeli` требует polkit-правила. **.deb ставит его сам** > (`/etc/polkit-1/rules.d/49-qeli.rules`) — делать ничего не нужно. Если ставили > **иначе** (голый бинарь, tarball), панель обнаружит отсутствие правила и попросит > один раз выполнить на сервере: > ``` > sudo qeli install-polkit # по умолчанию: user=qeli unit=qeli.service > sudo qeli install-polkit --unit qeli-server.service --user vpn # свои значения > ``` > После этого `Apply & Restart` работает. **Внутри контейнера** systemctl недоступен: > изменения профилей/data-plane применяются автоматически через перезапуск воркера > в процессе; изменение **сокета панели** (`web.bind`/`port`/`tls`/`enabled`) требует > пересоздания контейнера (`docker restart `). Панель теперь показывает точную > причину, а не молча ничего не делает. > **DHCP — частая путаница.** В обычном режиме **TUN** IP клиентам выдаёт > встроенный **пул** (секция Pool → CIDR/резервации), а **не** DHCP. DHCP-сервер > нужен только для **TAP/bridged**. При выключенном DHCP в TUN адреса всё равно > назначаются. ### Перезапуск сервера: воркер или полный рестарт Панель умеет два разных перезапуска, и разница принципиальна. - **Перезапуск воркера** (`POST /api/server/restart`) — супервизор, а вместе с ним и сама панель, продолжают работать; пересоздаётся только процесс дата-плейна, то есть VPN-профили (TUN, слушатели, DNS, DHCP) сносятся уходящим воркером и поднимаются заново свежим. Панель при этом не падает — JS просто опрашивает `/api/status`, пока клиенты не вернутся. Именно это делает **Launch** на странице Quick start. - **Полный рестарт** (`POST /api/server/full-restart`) — `systemctl restart <юнит>` (юнит определяется по cgroup самого процесса, иначе берётся `qeli.service`). Заменяется весь процесс, включая сокет панели. Именно это делает кнопка **Apply & Restart** на странице Config. **Полный рестарт ОБЯЗАТЕЛЕН** для полей, которыми панель слушает сеть: `web.enabled`, `web.bind`, `web.port`, `web.tls`, `web.tls_cert`, `web.tls_key` (а также `web.base_path`) — они привязываются при старте супервизора, и перезапуск воркера их не применяет. Сохранение, затронувшее их, так прямо и отвечает: применить FULL restart'ом. - **Сессия переживает полный рестарт**, пока включён `web.persist_session_key` (по умолчанию включён: ключ подписи сессий лежит в файле `0600`). Выключите его — и каждый рестарт будет разлогинивать всех. - **Права.** Перед перезапуском выполняется пре-флайт, чтобы отказать *громко*, а не молча: нет systemd (контейнер или запуск руками), нет бинаря `systemctl`, либо сервис работает НЕ от root и нет polkit-правила `/etc/polkit-1/rules.d/49-qeli.rules` — в последнем случае в ответе прямо сказано выполнить `sudo qeli install-polkit` (в `.deb` правило уже стоит). Ответ уходит первым, `systemctl restart` выполняется примерно через 0,8 с, чтобы браузер успел его получить. - **В контейнере** systemctl недоступен; если правка НЕ трогает сокет панели, панель сама откатывается на перезапуск воркера и пишет об этом. ### Редактор Raw INI Третий вид на странице Config (`GET`/`PUT /api/config/raw`) показывает **файл конфига дословно** и записывает обратно ровно тот текст, который вы ввели, — **комментарии и форматирование сохраняются**. Виды `Form` и `JSON` работают через разбор в структуру и её обратную сериализацию, поэтому рукописные комментарии при сохранении из файла пропадают; конфиг с комментариями правьте в Raw INI (или прямо на сервере). > **Секреты в этом виде замаскированы (с 0.7.13).** Значения `password_hash`, > `password_enc` и `password` отдаются как `` — раньше редактор показывал их > дословно, то есть выдавал хеш админского пароля и секреты пользователей всякому, кто > открыл страницу. Плейсхолдер **можно оставлять как есть**: отправленный обратно > неизменённым, он означает «сохранить то, что на диске». Чтобы поменять значение — > впишите новое поверх плейсхолдера. Остальной файл (комментарии, порядок, отступы) > при этом сохраняется дословно. Проверки у Raw те же, что у обычного сохранения: текст обязан разбираться; `logging.file` — внутри `/var/log/qeli`; `auth.users_file`, `identity_key`, `web.tls_cert`/`tls_key` — внутри `/etc/qeli`; `web.password_hash` обязан быть валидным argon2-хешем (в ручном редакторе проще всего залочить себя опечаткой); `routing.post_up`/`post_down` нельзя ни ввести, ни изменить через панель; и конфиг обязан пройти ту же валидацию профилей, что и при старте сервера. Панельные настройки применяются сразу, профиль/bind/tun — после рестарта. ### Идентичность сервера: показ и ротация **Config → Global → Server identity keys** (`GET /api/identity`) перечисляет профили с их bind-строкой и **запиненным публичным ключом** (hex) — панельный аналог `qeli show-identity`; при первом обращении файл ключа создаётся, если его ещё нет. Кнопка **Rotate** (`POST /api/identity/{профиль}/rotate`) генерирует профилю новый ключ. **Работающий воркер продолжает использовать старый ключ до рестарта**, а после рестарта **каждому клиенту этого профиля придётся выдать новый `auth.server_public_key`** — ранее запиненный ключ больше не подойдёт. То есть ротация означает переиздание конфига/ссылки всем клиентам профиля, и делать её «на всякий случай» не нужно. ### Users (пользователи и группы) - **Создание/правка:** вводите пароль **открытым текстом** — сервер сам хеширует (argon2id) и хранит обратимо-зашифрованную копию (для переиздания конфига, см. ниже). Поля: bandwidth/burst, static IP, группа, max sessions, **разрешённые профили** (изоляция интерфейсов), allowed networks, **персональные маршруты**. - **Группы** (`/api/groups`) — именованные шаблоны, которые лежат в том же users-файле рядом с пользователями (секция `[group:<имя>]`) и несут три поля: `bandwidth_limit_mbps`, `max_sessions`, `allowed_networks`. Участник группы наследует то поле, которого не задал у себя: **собственное значение всегда перекрывает групповое**, а если не задано ни там, ни там — лимита нет. Группу можно создать, изменить и удалить; после изменения воркер перечитывает users-файл. - **Лимит трафика и срок** (кнопка ⚙ у пользователя): пожизненный лимит **загрузки** (ГБ, `0` = без лимита) и **срок действия** аккаунта — задаётся как *Expire in (days)* или выбором конкретной **даты из календаря** (*Or until date*, оба поля синхронизированы). По наступлении срока пользователь не может подключиться, активная сессия рвётся, и уходит уведомление *Quota breach*. Кнопка ↺ сбрасывает счётчик трафика. - **Лимит считается ТОЛЬКО по загрузке** (download, сервер→клиент). Отдача (upload) не лимитируется — пользователя нельзя заблокировать отправкой. В колонке трафика два направления показаны раздельно: `↓` загрузка (лимитируемая, по ней рисуется полоса против лимита) и `↑` отдача. - Действия: Enable/Disable (с киком сессий), Delete. ### Трафик и квоты (Usage & quotas) Колонка **Data usage** в таблице пользователей (`GET /api/usage`) показывает пожизненные счётчики: полосу «загрузка против лимита», `↓` загрузку (лимитируемое направление), `↑` отдачу, число подключений за всё время (`· N×`) и срок действия. Кнопки ⚙ (лимит и срок) и ↺ (сброс счётчика) описаны выше, в разделе Users. - **Где хранится.** Отдельный sidecar-файл **`/etc/qeli/usage.json`**, а не users-файл (в котором лежат хеши паролей и который переписывается на каждый CRUD) — так учёт ничем не рискует. Пишет его воркер атомарно; панель перечитывает файл на каждый запрос `/api/usage` и помечает, кто сейчас онлайн. - **Как накапливается.** Свип воркера **раз в 10 с** снимает счётчики живых сессий и доливает в пожизненный итог только прирост с прошлого раза (идемпотентно по `session_id`), поэтому в горячем пути пакетов работы не прибавляется. - **Лимит считается ТОЛЬКО по загрузке** — `used_down` (сервер→клиент); 1 ГБ = 1 000 000 000 байт, `0` = без лимита. Отдача (`used_up`) учитывается и показывается, но **никогда не лимитируется**: заблокировать пользователя отправкой нельзя. - **Что происходит при превышении.** Новый вход отбивается ещё на авторизации (`AUTH DENIED … download quota exhausted`), а **живую сессию рвёт тот же свип**: она отключается, её IP возвращается в пул, в лог уходит `usage: disconnected … over quota / expired`, и шлётся уведомление *Quota breach* (не чаще раза в час на пользователя, чтобы клиент в цикле реконнекта не спамил). Точно так же обрабатывается наступивший срок действия. - **Установка и сброс** (`POST /api/usage/{username}/limit` и `…/reset`) идут через воркер по control-сокету — правится авторитетная users-БД и файл сохраняется. Некорректное число отклоняется с ошибкой, а не превращается тихо в «без лимита» / «без срока». Сброс обнуляет **обе** стороны счётчика, лимит и срок не трогает. ### Выдача конфига (Share / QR) — без ввода пароля Кнопка **Share/QR** у пользователя. Укажите только публичный хост (предзаполнен из `web.public_host`, иначе из последнего использованного) → **Generate**. Пароль **вводить не нужно** — сервер берёт его из зашифрованной копии и собирает `qeli://`-ссылку + QR. Пароль при этом **не меняется**. - **Старые пользователи** (заведены до этой функции — копии пароля нет): панель покажет «нет сохранённого пароля» и кнопку **«Reset password & issue config»** — один раз сбросит пароль (покажет новый), после чего конфиг переиздаётся всегда. (Сброс — единственный путь: старый пароль восстановить неоткуда.) ### Клиент-менеджер (вкладка Client) — исходящие туннели Отдельная страница сайдбара, где панель выступает **клиентом**: этот сервер сам дозванивается до ДРУГИХ qeli-серверов (роль клиента рядом с ролью сервера — например, чтобы завернуть собственный исходящий трафик или связать площадки). К входящим VPN-клиентам этой машины страница отношения не имеет. **Добавить профиль** — три кнопки: - **Import qeli:// link** — вставляете строку вида `qeli://user:pass@host:443?mode=…&key=…&sni=…&rsid=…` и, если хотите, имя профиля; без имени оно берётся из метки ссылки или из хоста. Импортированный профиль всегда **split-tunnel** — признак full-tunnel в ссылке не передаётся. - **Paste INI config** — вставить клиентский INI целиком (`[qeli]` + `[logging]`); он сохраняется дословно, поэтому доступен любой ключ клиента. - **Add manually** — форма: server `host:port`, протокол tcp/udp, wire-режим (`fake-tls` / `reality-tls` / `obfs` / `plain`), пользователь и пароль, TUN-девайс (пусто = авто), SNI, **запиненный ключ сервера** (hex; обязателен для `reality-tls`), REALITY short_id, obfs-ключ и fronting, AmneziaWG-junk (jc/jmin/jmax), QUIC-маскировка (только UDP), автоподключение при старте, `route_local` и full-tunnel. Переключатель **Raw INI ↦** в том же окне даёт полный конфиг, а ключи, которых нет в форме, сохраняются при переходе туда и обратно. **Где что лежит.** - Профили — `/etc/qeli/clients/<имя>.conf`, права `0600` (внутри пароль открытым текстом). Имя — только `[A-Za-z0-9._-]`, до 64 символов. - TUN-устройство, если не задано вручную, назначается автоматически: младший свободный `vpnN`, не занятый ни другим клиентским профилем, ни уже существующим интерфейсом хоста (в том числе TUN серверного профиля). - Лог каждого туннеля — `/var/log/qeli/client-<имя>.log`; при каждом Connect файл перезаписывается заново. **Connect / Disconnect.** Connect запускает `qeli client -c <файл>` дочерним процессом супервизора (наследует его права, поэтому может поднять TUN и маршруты). Disconnect шлёт SIGTERM — клиент восстанавливает DNS и маршруты и выходит; если за 5 с не вышел, его добивают SIGKILL. **Delete** сначала отключает, потом удаляет профиль и его лог. Флаг `autostart = true` подключает профиль при старте супервизора. **Статус честный, а не «процесс жив».** Список обновляется раз в 5 с, состояние выводится из хвоста лога: **● Connected**, **◌ Connecting…**, **⚠ Error — retrying** (процесс жив, но туннель зациклился на реконнекте — например, `reality-tls` без short_id) или **○ Disconnected**. Рядом показываются выданный внутренний IP туннеля и последние строки лога. > **Full-tunnel на сервере опасен.** Галочка «Full-tunnel (route ALL traffic)» > заворачивает ВЕСЬ трафик этой машины в удалённый сервер и может отрезать вам и панель, > и SSH — поэтому по умолчанию она выключена и помечена предупреждением. > > **Хуки запрещены.** Профиль, сохранённый через панель, не может содержать > `post_up`/`post_down` и `password_command` (они выполняются через shell), а > `password_file` обязан лежать внутри `/etc/qeli`. Нужны хуки — правьте файл профиля > на самом сервере. ### Логи (вкладка Logs) Показывает **хвост файла**, заданного в `logging.file` (`GET /api/logs`). - Если `logging.file` не задан, страница честно пишет, что логи идут в stderr/journald, и подсказывает `journalctl -u qeli -n 200 --no-pager`: читать журнал systemd панель не умеет. - Путь обязан находиться внутри **`/var/log/qeli`**, иначе — «log path rejected» (чтобы правка конфига не превратилась в чтение произвольного файла вроде `/etc/shadow`). - Сервер читает только последние ~4 МиБ файла и отдаёт не больше запрошенного числа строк: селектор **100 / 200 / 500 / 1000**, по умолчанию 200, жёсткий потолок API — 2000 строк. - **Фильтры:** выпадающий список уровня (All / ERROR / WARN / INFO / DEBUG) и строка поиска (регистронезависимая). Поиск без выбранного уровня уходит на сервер и применяется к прочитанному окну; уровень и поиск дополнительно фильтруют уже загруженные строки в браузере — то есть фильтр работает в пределах последнего хвоста, а не по всему архиву логов. - **Auto-refresh** (переключатель) перечитывает лог раз в 5 с и прокручивает вниз; кнопки **Refresh** и **Bottom** — то же вручную. В строке статистики видны путь к файлу, «показано N из M» и счётчики по уровням; строки подсвечиваются по уровню. ### Blocked IPs (заблокированные адреса) Отдельная вкладка сайдбара. Показывает source-IP, **залоченные брутфорс-защитой** (повторный неверный пароль), разделённые на **два независимых журнала**: **VPN authentication** и **Panel login**. Для каждой записи — адрес, число неудач и сколько осталось до авто-разблокировки. **Вкладка сама обновляется каждые 5 с** (фоновый поллинг, без мигания спиннера), а счётчик «до разблокировки» **тикает посекундно** — истёкшие записи исчезают сами, так что активная блокировка видна в реальном времени (раньше список грузился один раз при открытии и транзиентный лок было легко не увидеть). Кнопка **Unblock** снимает блок с одного адреса, **Clear all** — со всех, в пределах своего журнала (снятие лока панели не трогает журнал VPN). Лок и так снимается сам по таймауту своей поверхности (`brute_force.lockout_secs`, дефолт 900 с). То же из CLI для журнала VPN: `qeli list-blocked` / `qeli unblock ` (см. GETTING-STARTED §10). **Политика блокировки — две независимые политики, правятся вживую на этой вкладке.** С 0.7.7 редактор *Lockout policy* несёт **две** политики рядом: - **VPN authentication** → `[auth] brute_force`, - **Panel login** → `[web] brute_force`. У каждой свой **выключатель**, *Max attempts*, *Window* и *Lockout*, так что туннель и панель ограничиваются (или отключаются) раздельно. **Save policy** применяет обе вживую без рестарта и без разрыва сессий (сбрасывает счётчики этой поверхности). Выключите переключатель, чтобы полностью отключить ограничение для поверхности (для входа в панель безопасно только на доверенном / loopback-bind). Те же политики правятся и в **Config → Authentication** (VPN) и **Config → Web UI** (панель). > Частые `New TCP connection from …` с одного IP на `reality-tls` в логах — это > **сканеры/пробы**, а не подбор пароля: они прозрачно проксируются на upstream и не > имеют юзера. Настоящие попытки видны как `AUTH FAIL … user=X — wrong password`. ### Уведомления (Notifications) Исходящие оповещения о ключевых событиях сервера через **Telegram** и **произвольный webhook** — два независимых канала, у каждого свой переключатель, реквизиты, набор событий и кнопка **Send test**. Конфиг в `/etc/qeli/notify.json` (правится и панелью, и файлом); отправка best-effort и не блокирует дата-плейн, сертификаты исходящего TLS проверяются. По умолчанию ВЫКЛ (нет `notify.json` → no-op). - **Имя сервера (Server name)** — метка, добавляемая в начало каждого сообщения (`[имя] …`) и в поле `server` webhook-JSON, чтобы различать несколько серверов, пишущих в один чат / хук. Пусто = без префикса. - **События** (переключаются на каждый канал): *старт / рестарт сервера*, *превышение квоты* (юзер исчерпал лимит трафика или срок), *блокировка входа в панель* (IP заблокирован после неудачных входов в **панель**), **блокировка IP (VPN-авторизация)** (IP залочен брутфорс-защитой после повторного неверного **VPN**-логина/пароля), *конфиг восстановлен*. Повторяющиеся условия троттлятся (≤ раз/час на юзера или IP). - Токен Telegram — **только запись** (после сохранения маскируется); **Send test** шлёт пробу в один канал с текущими (даже несохранёнными) настройками. ### Баннер обновлений (opt-in) При `[web] update_check = true` панель показывает dismissible-баннер **«Доступна новая версия»**, если на GitHub есть более новый релиз qeli. Проверку делает **браузер оператора** (как маркетинг-сайт), а не серверный процесс — никакого серверного beacon; ничего идентифицирующего не отправляется и ничего не скачивается. Баннер даёт **копируемую команду обновления** под способ установки (`.deb`: скачать → сверить SHA256 → `dpkg -i` → restart; Docker: `docker pull`) — выполняете вы. По умолчанию ВЫКЛ. (У десктоп/мобильных клиентов свой opt-in в Настройках; в CLI — `qeli version --check`.) См. «Проверка обновлений» в [CONFIG.md](CONFIG.md). --- ## 3. Хранение пароля (модель и компромисс) Для аутентификации хранится **argon2id-хеш** (необратим). Чтобы можно было **переиздавать** конфиг существующему пользователю без знания пароля, дополнительно хранится **обратимо-зашифрованная** копия пароля: - Шифр — ChaCha20-Poly1305, ключ панели `/etc/qeli/panel-secret.key` (`0600`, генерируется автоматически). В users-файле — поле `password_enc` (base64); по API **не отдаётся**. - **Компромисс (осознанный):** при компрометации сервера (доступ к ключу + users- файлу) эти пароли восстановимы. Это VPN-only креды; так работает большинство VPN-панелей. Если нужна модель «только хеш» без переиздания — не задавайте пароль через панель/CLI (но тогда Share потребует сброса для каждого). --- ## 4. Шпаргалка по `[web]` | Ключ | Назначение | |---|---| | `enabled` | включить панель | | `bind` / `port` | адрес и порт (внешний IP или `127.0.0.1` для SSH-туннеля) | | `username` / `password_hash` | админ-логин и argon2id-хеш (хеш **обязателен на ЛЮБОМ bind, включая loopback** — пусто = fail-closed, панель не стартует) | | `tls` | встроенный HTTPS (rustls) | | `tls_cert` / `tls_key` | свой PEM cert/key; пусто = self-signed авто | | `allowed_ips` | белый список source-IP/CIDR. Отсутствует, пусто или `""` = любой источник. Дубликаты строк складываются в один список; заблокированному источнику — голый 403 на всех маршрутах | | `public_host` | дефолтный хост для share-ссылок (можно переопределить в диалоге); ещё и разрешённый CSRF-origin | | `allowed_origins` | доп. браузерные origin'ы (`host` или `host:port`) для изменяющих запросов — нужны для доступа по LAN-IP / домену / reverse-proxy, иначе логин и сохранения `403` | | `base_path` | отдавать панель под сабпасом reverse-proxy (напр. `/qeli`); пусто = в корне. См. «Reverse-proxy sub-path» в CONFIG.md | | `csrf` | CSRF same-origin защита (default `true`); `false` отключает — только на loopback-bind, иначе любой открытый сайт сможет дёргать панель | | `update_check` | opt-in баннер «доступна новая версия» (default `false`); панель проверяет GitHub в браузере оператора и показывает копируемую команду обновления. См. «Проверка обновлений» в CONFIG.md | | `session_ttl_secs` | время жизни сессии панели (Max-Age куки + срок токена; default `86400`) | | `trusted_proxies` | source-IP/CIDR reverse-proxy'ей, чьему `X-Forwarded-For` доверять (для allow-листа и rate-limit); пусто = XFF не доверяется | | `secure_cookie` | `Secure` на куке (авто при `tls`; вручную — за TLS-прокси) | Все `[web]`-ключи выше (включая `base_path`, `csrf`, `session_ttl_secs`, `trusted_proxies`, `update_check`) правятся прямо в **Config → Web UI** — раньше их можно было задать только ручной правкой INI. Идентичность сервера, пиннинг ключей, H-1, авторизация по профилям, лимиты, wire-режимы/REALITY — в [CONFIG.md](CONFIG.md).