Централизованный контроль устройств для Remnawave
Мониторит одновременные подключения пользователей через API панели: собирает IP со всех нод, сравнивает с лимитом устройств каждого пользователя (hwidDeviceLimit) и реагирует на превышения — через Telegram-бота или автоматической блокировкой подписки.
- Агрегация IP со всех нод — полная картина подключений, сравнение с индивидуальным лимитом + погрешность (tolerance)
- Два режима:
manual— алерт с inline-кнопками (сброс, бан, whitelist);auto— автоблокировка с автовосстановлением по таймеру - Порог нарушений — действие только после N превышений за окно (защита от ложных срабатываний)
- Группировка IP по подсетям (
/24) или по ASN-провайдерам — против CGNAT и шаринга - Webhook (JSON POST) о нарушениях с HMAC-подписью
- Статистика и отчёты — команда
/stats(за 24ч/неделю + топ-5 нарушителей) и ежедневный отчёт в чат - Рантайм-настройки через
/settings— изменение параметров на лету без перезапуска (хранятся в Redis) - Whitelist пользователей и IP/CIDR, кулдаун, кэш, выбор часового пояса, ru/en
- Liveness
/healthz(опционально, поHEALTH_ADDR) для healthcheck в Docker/оркестраторе - Логи под
docker compose logs -f— сводка по каждому циклу проверки, уровень меняется на лету,LOG_FORMAT=jsonдля Loki/ELK
┌─────────────┐ ┌──────────────────┐ ┌───────────────┐
│ Remnawave │◄────│ remnawave- │────►│ Telegram │
│ Panel API │ │ limiter │ │ Bot API │
└─────────────┘ │ (Go binary) │ └───────────────┘
│ ┌────────────┐ │
│ │ Redis │ │
│ └────────────┘ │
└──────────────────┘
Один инстанс рядом с панелью или на любом сервере. Установка на ноды не нужна — всё через API. Docker Compose поднимает сервис + Redis.
Цикл проверки: список нод (GET /api/nodes) → параллельный сбор IP по нодам → агрегация по пользователю → фильтр по lastSeen → сравнение с лимит + погрешность → инкремент порогового счётчика → при достижении порога реакция (алерт или автоблокировка).
Сбойная нода не роняет цикл: проверка выполняется по остальным, а в лог идёт предупреждение о неполных данных. Если не ответила ни одна нода, цикл пропускается целиком — иначе отсутствие подключений было бы прочитано как «все в пределах лимита».
- Docker и Docker Compose
- Remnawave 3.0.0+ с API-токеном (scope
connections:*,users:*иnodes:read) - Telegram-бот (@BotFather)
Минимальный набор scope
Если хотите выдать токену только то, что limiter реально вызывает:
| Scope | Эндпоинт | Зачем |
|---|---|---|
nodes:list |
GET /api/nodes |
список нод для опроса — без него цикл не начнётся |
connections:by-node |
POST /api/connections/by-node/{nodeUuid} |
запуск сбора IP по ноде |
connections:by-node-result |
GET /api/connections/by-node/{jobId} |
результат сбора |
users:by-id |
GET /api/users/{id} |
лимит устройств и данные пользователя |
users:disable |
POST /api/users/{id}/actions/disable |
отключение подписки |
users:enable |
POST /api/users/{id}/actions/enable |
кнопка «Включить» и восстановление по таймеру |
connections:drop |
POST /api/connections/drop |
только кнопка «Сбросить подключения» в режиме manual |
Для нод достаточно чтения: nodes:* дал бы ещё запись (перезапуск и отключение нод, правку профилей и инбаундов), которая limiter не нужна.
| Версия панели Remnawave | Версия limiter |
|---|---|
| 3.0.0 и новее | 4.0.0+ — текущая, тег latest |
| ниже 3.0.0 (ветка 2.x) | 3.0.0 — последняя с поддержкой панелей 2.x |
Версии совместимы только в одну сторону: в Remnawave 3.0.0 модуль ip-control переименован в connections, а пользователи идентифицируются числовым id вместо uuid — старые эндпоинты удалены. Поэтому limiter 4.0.0+ не работает с панелью 2.x, а limiter 3.0.0 — с панелью 3.x.
Если панель ещё не обновлена, зафиксируйте образ на релизе 3.0.0 в docker-compose.yml:
image: ghcr.io/syvlech/remnawave-limiter:b422f43b422f43 — коммит релиза 3.0.0. Отдельного тега образа 3.0.0 в ghcr нет: CI собирает semver-теги только для рефов вида v*, а релиз помечен как 3.0.0 без префикса.
mkdir -p /opt/remnawave-limiter && cd /opt/remnawave-limiter
curl -O https://raw.githubusercontent.com/syvlech/remnawave-limiter/master/docker-compose.yml
curl -O https://raw.githubusercontent.com/syvlech/remnawave-limiter/master/.env.example
cp .env.example .env && nano .envОбязательные параметры в .env:
REMNAWAVE_API_URL=https://panel.example.com
REMNAWAVE_API_TOKEN=your-api-token-here
TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11X
TELEGRAM_CHAT_ID=-1001234567890
TELEGRAM_ADMIN_IDS=123456789Запуск, логи и обновление:
docker compose pull && docker compose up -d # запуск / обновление
docker compose logs -f limiter # проверкаВсе настройки — через .env или переменные окружения.
| Параметр | По умолчанию | Описание |
|---|---|---|
REMNAWAVE_API_URL |
обязательный | Адрес панели Remnawave |
REMNAWAVE_API_TOKEN |
обязательный | API-токен (генерируется в панели) |
REMNAWAVE_COOKIES |
— | Доп. авторизация через cookie. Формат: key=value через ; (напр. cf_clearance=abc; session=xyz). Для панелей за Cloudflare/WAF |
REMNAWAVE_HEADERS |
— | Доп. HTTP-заголовки для каждого запроса к API. Формат: Name: Value через ; (напр. X-Api-Key: secret123; X-Custom-Header: value). Для панелей за прокси/Cloudflare/защитой |
TELEGRAM_BOT_TOKEN |
обязательный | Токен бота от @BotFather |
TELEGRAM_CHAT_ID |
обязательный | ID чата/канала/группы для алертов |
TELEGRAM_ADMIN_IDS |
обязательный | ID админов через запятую (только они нажимают кнопки) |
TELEGRAM_THREAD_ID |
— | ID треда/топика в супергруппе |
TELEGRAM_PROXY |
— | Прокси для Telegram API. Схемы: http, https, socks5, socks5h. Формат: scheme://[user:pass@]host:port |
CHECK_INTERVAL |
30 |
Интервал проверки (сек) |
ACTIVE_IP_WINDOW |
300 |
IP активен, если lastSeen < этого значения (сек) |
TOLERANCE |
0 |
Фиксированное допустимое превышение лимита. Лимит 3 + tolerance 1 → реакция при 5+ |
TOLERANCE_MULTIPLIER |
0 |
Пропорциональный допуск: TOLERANCE + floor(limit × множитель). 0 — не используется. Допустимо 0..100 |
COOLDOWN |
300 |
Кулдаун между алертами на пользователя (сек) |
USER_CACHE_TTL |
600 |
TTL кэша данных пользователя (сек) |
DEFAULT_DEVICE_LIMIT |
0 |
Лимит, если hwidDeviceLimit не задан. 0 = без ограничения |
ACTION_MODE |
manual |
manual — алерт с кнопками; auto — автоотключение подписки |
AUTO_DISABLE_DURATION |
0 |
Длительность временного отключения (мин). 0 = перманентно. В manual добавляет кнопку, в auto — время автовосстановления |
AUTO_NOTIFY_SOFT |
false |
Только auto. Превышение в пределах допуска (limit < устройств <= limit+TOLERANCE) даёт информационный алерт без бана. Бан — только выше limit+TOLERANCE |
WEBHOOK_URL |
— | URL для webhook при нарушениях (POST JSON). Пусто = выключен |
WEBHOOK_SECRET |
— | Секрет webhook для HMAC-SHA256 подписей X-Signature (тело) и X-Signature-V2 (<X-Timestamp>.<тело>). См. Webhook (опционально) |
WEBHOOK_SECRET_HEADER |
true |
Передавать ли сам секрет в заголовке X-Webhook-Secret. Оставлено ради совместимости; если получатель проверяет подпись — выключите |
WHITELIST_USER_IDS |
— | Числовые ID пользователей для исключения из проверки (через запятую). UUID из версий до 4.0 здесь не работают |
IGNORED_NODE_UUIDS |
— | UUID нод, игнорируемых при сборе IP (не в отчётах, не в решениях). Для технических/тестовых нод |
IP_WHITELIST |
— | IP и/или CIDR-подсети через запятую, исключаемые из подсчёта. Отсеивает IP нод/мостов/релеев. IPv4/IPv6. Пример: 203.0.113.5,10.0.0.0/8,2001:db8::/32 |
IGNORE_DURATION |
0 |
Время действия кнопки «Игнорировать» (мин). 0 = навсегда. > 0 = временный whitelist с TTL |
VIOLATION_THRESHOLD |
1 |
Сколько нарушений нужно для действия. 1 = мгновенная реакция |
VIOLATION_THRESHOLD_WINDOW |
3600 |
Окно подсчёта нарушений (сек). Должно быть больше (VIOLATION_THRESHOLD − 1) × (COOLDOWN + CHECK_INTERVAL), иначе порог недостижим и конфиг не принимается |
SUBNET_GROUPING |
false |
Группировка IPv4 по /SUBNET_PREFIX_V4 — считаются подсети вместо IP (снижает ложные срабатывания от CGNAT). IPv6 — по-штучно |
SUBNET_PREFIX_V4 |
24 |
Длина IPv4-префикса (8..32). 24 — стандарт; 16 — для мобильной аудитории. При SUBNET_GROUPING=true |
ASN_GROUPING |
false |
Считать уникальные ASN-провайдеры вместо IP/подсетей — самый точный сигнал против шаринга. IP без ASN — отдельная группа. Приоритетнее SUBNET_GROUPING. Требует базу MaxMind ASN |
ASN_DATABASE_PATH |
./geoip/GeoLite2-ASN.mmdb |
Путь к GeoLite2-ASN.mmdb. Директория создаётся автоматически. Менять только при нестандартной раскладке |
MAXMIND_LICENSE_KEY |
— | Ключ MaxMind. Если задан — недостающая база скачивается при старте + фоновое обновление. Получить |
MAXMIND_UPDATE_INTERVAL |
168h |
Интервал автообновления базы (мин. 1h). Только при заданном MAXMIND_LICENSE_KEY |
REDIS_URL |
redis://redis:6379 |
Адрес Redis |
TIMEZONE |
UTC |
Часовой пояс для timestamps в алертах (напр. Europe/Moscow) |
LANGUAGE |
ru |
Язык интерфейса: ru или en |
DAILY_REPORT |
false |
Ежедневный отчёт о нарушениях в чат (топ нарушителей + счётчики). Переключается на лету через /settings |
DAILY_REPORT_TIME |
09:00 |
Локальное время отправки отчёта (HH:MM, в TIMEZONE). Меняется только перезапуском |
HEALTH_ADDR |
— | Адрес HTTP liveness-эндпоинта /healthz (напр. :8080). Пусто = выключен |
LOG_LEVEL |
info |
Детальность логов: trace, debug, info, warn, error. На info — по одной сводной строке на цикл проверки плюс все действия; debug добавляет разбор по IP и детали Telegram. Меняется на лету через /settings |
LOG_FORMAT |
text |
text для чтения человеком, json для сборщиков логов (Loki, ELK). Меняется только перезапуском |
ASN в уведомлениях. Если база MaxMind доступна, рядом с каждым IP в алерте и webhook показывается провайдер (• 91.107.96.11 - Hetzner Online GmbH (Chicago-1)), а в заголовке — счётчик уникальных ASN (Обнаружено: 4 IP (3 ASN)). На логику ограничения не влияет — решения только по количеству IP/подсетей/ASN.
hwidDeviceLimit |
Поведение |
|---|---|
> 0 |
Используется как лимит устройств |
null |
Берётся DEFAULT_DEVICE_LIMIT из конфига |
0 |
Без ограничений — пользователь пропускается |
При VIOLATION_THRESHOLD=1 (по умолчанию) реакция на каждое превышение. При большем значении действие выполняется только после накопления N нарушений в окне VIOLATION_THRESHOLD_WINDOW: превышение → проверка cooldown → инкремент счётчика (TTL = окно) → при достижении порога действие и сброс счётчика. Если между нарушениями проходит больше окна — счётчик обнуляется.
Cooldown ставится до инкремента, поэтому счётчик растёт не чаще раза за COOLDOWN, а следующее нарушение замечается на ближайшей проверке. Окно должно вмещать N − 1 таких шагов: VIOLATION_THRESHOLD_WINDOW > (VIOLATION_THRESHOLD − 1) × (COOLDOWN + CHECK_INTERVAL). Конфиг, где это не выполняется, отклоняется при старте и в /settings — иначе порог никогда не был бы достигнут и лимитер молча перестал бы действовать.
Пример при VIOLATION_THRESHOLD=3, VIOLATION_THRESHOLD_WINDOW=3600:
| Время | Счётчик | Действие |
|---|---|---|
| 12:00 | 1/3 | Лог, без действия |
| 12:05 | 2/3 | Лог, без действия |
| 12:10 | 3/3 | Алерт/блокировка, сброс |
| 12:15 | 1/3 | Лог, без действия |
Все сообщения бота — rich-сообщения Telegram (Bot API 10.3, sendRichMessage): заголовки, таблицы, сворачиваемые блоки, время в часовом поясе читателя (tg-time) и цветные кнопки прямо внутри сообщения. Если Telegram отклонит rich-разметку, сообщение всё равно уйдёт — обычным текстом с inline-кнопками. Для отображения нужен клиент Telegram с поддержкой rich-сообщений.
Доступны только администраторам из TELEGRAM_ADMIN_IDS. Регистрируются в меню бота автоматически при старте.
| Команда | Что делает |
|---|---|
/settings |
Интерактивное меню рантайм-настроек. Меняет безопасные параметры на лету (без перезапуска); значения сохраняются в Redis и переживают рестарт. Подробнее — раздел «Рантайм-настройки» ниже |
/stats |
Статистика нарушений: количество за последние 24 часа и за неделю + топ-5 нарушителей за неделю. Кнопка «🔄 Обновить» перерисовывает отчёт на месте |
Приоритет источников: Redis-override > .env / переменные окружения > значения по умолчанию. Изменения применяются на лету (например, CHECK_INTERVAL пересоздаёт тикер). Структурные и секретные ключи (API URL/токен, все TELEGRAM_*, REDIS_URL, TIMEZONE, LANGUAGE, WEBHOOK_*, IP_WHITELIST, IGNORED_NODE_UUIDS, MAXMIND_*, DAILY_REPORT_TIME, HEALTH_ADDR, LOG_FORMAT) меняются только через .env + перезапуск. Кнопка «Сбросить к .env» (для ключа или для всех, с подтверждением) убирает override.
Меню работает в одном сообщении, которое перерисовывается на месте:
- Главный экран — таблица всех параметров по разделам (🎯 Обнаружение, 🛡 Действия, ⏳ Пороги и кулдауны, 🌐 Группировка, 🧰 Сервис); ♻️ отмечает значения, изменённые в боте.
- Раздел — значения, подсказки к каждому параметру и кнопки: флаги переключаются одним нажатием (включённые — зелёные), остальные открывают экран редактирования.
- Редактирование — текущее значение, источник (
.envили бот), имя переменной, быстрые пресеты (для минут — в человекочитаемом виде,0= «навсегда») и ввод своего значения сообщением. Ошибка валидации показывается на том же экране, ввод можно сразу повторить.
Раз в сутки в DAILY_REPORT_TIME (локальное время в TIMEZONE) бот присылает в чат тот же отчёт, что и /stats (нарушений за 24ч/неделю + топ-5). Учитываются только реальные срабатывания после порога VIOLATION_THRESHOLD; «мягкие» предупреждения в статистику не попадают. DAILY_REPORT переключается на лету через /settings, время DAILY_REPORT_TIME — только перезапуском.
При превышении бот отправляет алерт с кнопками:
⚠️ Превышение лимита устройств
┌──────────────────────┬──────────────────────┐
│ 👤 Пользователь │ username123 │
│ 🔗 Подписка │ открыть │
│ 📊 Лимит │ 3 │
│ 📡 Обнаружено │ 5 IP · 2 ASN │
│ 🚦 Превышение │ +2 │
│ 📈 Нарушений за 24ч │ 3 │
│ 🕐 Время │ сб, 29.11 04:15 │ ← в часовом поясе читателя
└──────────────────────┴──────────────────────┘
▾ 📍 IP-адреса (5)
IP Провайдер Нода
10.0.1.10 Hetzner node-1
10.0.3.30 Cloudflare node-2
[🔄 Сбросить подключения]
[🔒 Отключить навсегда] [🔒 Отключить на 10 мин] ← вторая — если AUTO_DISABLE_DURATION > 0
[🔇 Игнорировать 15 мин] ← если IGNORE_DURATION > 0, иначе "🔇 Игнорировать"
После нажатия кнопки содержимое алерта сохраняется, кнопки исчезают, а внизу появляется итог: что сделано, кем и когда. После отключения подписки остаётся кнопка «🔓 Включить подписку» — действие можно отменить. Если алерт отредактировать нельзя (он удалён, слишком старый или Telegram отверг правку), итог приходит отдельным сообщением ответом на алерт.
Длительность «Отключить на N мин» и «Игнорировать N мин» запоминается в самой кнопке при отправке алерта: смена AUTO_DISABLE_DURATION/IGNORE_DURATION через /settings не меняет смысл уже отправленных кнопок. «Отключить навсегда» и «Включить подписку» снимают запланированное автовосстановление, чтобы старый таймер не включил пользователя снова.
| Кнопка | Действие |
|---|---|
| Сбросить подключения | Сброс активных подключений через API |
| Отключить навсегда | Перманентная деактивация подписки |
| Отключить на N мин | Временная деактивация с автовосстановлением (при AUTO_DISABLE_DURATION > 0) |
| Игнорировать | В whitelist: временно при IGNORE_DURATION > 0, иначе навсегда |
Подписка отключается автоматически, бот шлёт информационный алерт с кнопкой «Включить подписку». При AUTO_DISABLE_DURATION > 0 подписка восстанавливается по таймеру, и в алерте видно, когда это произойдёт.
Уведомления в пределах допуска (AUTO_NOTIFY_SOFT=true). Когда нужно банить только при заметном превышении (TOLERANCE), но знать о тех, кто уже вышел за HWID-лимит:
| Кол-во устройств | Действие |
|---|---|
устройств <= limit |
тишина |
limit < устройств <= limit+TOLERANCE |
🔔 информационный алерт, без бана (с кнопками «Сбросить подключения» и «Игнорировать») |
устройств > limit+TOLERANCE |
🔒 бан + алерт |
«Мягкие» предупреждения используют отдельный кулдаун (cooldown:soft:), не увеличивают счётчик нарушений за 24ч, не учитываются в VIOLATION_THRESHOLD и шлют webhook с событием soft_violation_detected.
При нарушении limiter может отправить HTTP POST на WEBHOOK_URL (работает в обоих режимах). К каждому запросу добавляется X-Timestamp (unix-время отправки). Если задан WEBHOOK_SECRET, добавляются заголовки:
| Заголовок | Содержимое |
|---|---|
X-Signature-V2 |
sha256=<hex> — HMAC-SHA256 от строки <X-Timestamp>.<тело>. Рекомендуемая проверка: подпись покрывает и время, поэтому переигранный запрос с новым X-Timestamp её не пройдёт. Отбрасывайте запросы, у которых X-Timestamp старше нескольких минут |
X-Signature |
sha256=<hex> — HMAC-SHA256 только от тела. Для совместимости: защищает от подмены, но не от повтора |
X-Webhook-Secret |
Сам секрет, для простой сверки. Кто увидел один запрос, узнаёт секрет и может подписать что угодно — отключите WEBHOOK_SECRET_HEADER=false, если получатель проверяет подпись |
Проверка на получателе (псевдокод): hmac_sha256(secret, ts + "." + body) == header["X-Signature-V2"] и now − ts < 300.
Отправка не блокирует цикл мониторинга. При сетевой ошибке, 5xx или 429 доставка повторяется (до 3 попыток); 4xx считается осознанным отказом получателя и не повторяется. Событие переживает остановку сервиса: при SIGTERM limiter дожидается отправки, а не теряет её.
Пример payload:
{
"event": "violation_detected",
"action_mode": "auto",
"user": {
"user_id": 123,
"username": "john",
"email": "[email protected]",
"telegram_id": 123456789,
"subscription_url": "https://panel.example.com/sub/abc"
},
"violation": {
"ips": [
{ "ip": "1.2.3.4", "node_name": "DE-1", "node_uuid": "node-uuid-1", "last_seen": "2025-11-29T12:00:00Z", "asn": 24940, "asn_org": "Hetzner Online GmbH" },
{ "ip": "5.6.7.8", "node_name": "US-1", "node_uuid": "node-uuid-2", "last_seen": "2025-11-29T12:01:00Z" }
],
"ip_count": 5,
"device_limit": 3,
"tolerance": 1,
"effective_limit": 4,
"violation_count_24h": 3,
"device_group_count": 5,
"grouping_mode": "ip"
},
"action": { "auto_disable_duration_min": 10 },
"timestamp": "2025-11-29T12:05:00Z"
}| Поле | Описание |
|---|---|
event |
violation_detected при бане; soft_violation_detected для «мягкого» предупреждения (AUTO_NOTIFY_SOFT) |
action_mode |
manual или auto |
user |
Данные пользователя (user_id, username, email, telegram_id, subscription_url) |
violation.ips |
Активные IP с нодой и временем последней активности |
violation.ip_count |
Количество уникальных IP |
violation.device_limit / tolerance / effective_limit |
Лимит, допуск и эффективный лимит (device_limit + tolerance) |
violation.violation_count_24h |
Нарушений за 24 часа |
violation.grouping_mode |
По чему считались устройства: ip, subnet или asn |
violation.device_group_count |
Итоговое число «устройств» в выбранном режиме — именно оно сравнивается с лимитом |
violation.subnet_count / asn_group_count |
Число подсетей / ASN-групп. Присутствуют, только если режим включён |
violation.ips[].asn / asn_org |
Номер и название провайдера. Присутствуют, только если база MaxMind загружена и ASN определился |
action.auto_disable_duration_min |
Длительность блокировки (0 = перманентная) |
timestamp |
Время обнаружения (ISO 8601) |
Как узнать Telegram Chat ID? Добавьте @userinfobot и отправьте /start. Для группы/канала — @getidsbot.
Что если API панели недоступен? Сервис логирует ошибку, пропускает цикл и пробует снова через CHECK_INTERVAL. Запросы к API повторяются до 3 раз с exponential backoff и джиттером; при 429 и 408 повтор тоже выполняется, а заголовок Retry-After от панели учитывается. Цикл, в котором не удалось опросить ни одну ноду, не считается успешным — /healthz в этом случае покажет проблему.
Можно ли использовать Redis от Remnawave? Можно, но не рекомендуется — проект поднимает свой Redis. Для существующего укажите REDIS_URL.
Как изменить лимит пользователя? Через поле hwidDeviceLimit в настройках подписки в панели Remnawave.
GitHub Issues · GNU GPL v3.0 — см. LICENSE