Ловит тех, кто грузит ноды.
Читает Redis Streams, которые Remnawave публикует при
EXPORT_TO_STREAM_ENABLED=true, добирает из API панели то, чего в стримах нет
(отчёты Torrent Blocker, HWID-устройства, справочники), обогащает IP через
MaxMind GeoLite2, складывает всё в ClickHouse и показывает в Grafana. Написан на
Go, один статический бинарь, никакого рантайма.
Remnawave ──► Valkey Streams ──► remnanode-exporter ──► ClickHouse ──► Grafana
│ ▲ ▲
└──────────── REST API ───────────┘ │
(юзеры, ноды, HWID, Torrent Blocker) MaxMind GeoLite2
(страна / город / ASN / хостинг)
Минимальная версия панели — 3.1.0: именно там в стримы попали поля
Subscription Response Rules, а GET /api/nodes начал отдавать числовой id ноды.
Проверено на 3.4.5: контракт стримов с 3.1.1 не изменился ни на байт, так
что обновление панели в пределах 3.x экспортера не касается.
Grafana своя не тащится — подключается та, что уже есть. Существующий дашборд 25064 это не заменяет и не ломает: там Prometheus-метрики железа, здесь — поведение аккаунтов.
| Сигнал | Откуда | Что означает |
|---|---|---|
| Трафик по юзеру и ноде, пик за 5 минут | user_usage |
кто реально выжирает канал |
| Число разных /24 и /48 сетей у одного аккаунта | node_connections |
подписку раздали |
| Число стран и ASN одновременно | node_connections + GeoIP |
продали доступ |
| IP в хостинг-ASN (OVH, Hetzner, AWS, Aeza…) | node_connections + GeoIP |
перепродажа или цепочка прокси |
| Один IP у нескольких аккаунтов | node_connections |
общий выходной прокси, реселлер |
| Частота запросов подписки, число User-Agent | subscription_requests |
скрипт, парсер, ссылка гуляет по чату |
curl / python-requests / боты в UA |
subscription_requests |
автоматизация, а не клиент |
Запросы, отбитые правилом SRR (BLOCK, 404, 451, дроп сокета) |
subscription_requests |
клиент долбится в правило, которое его не пускает |
| Разные типы ответа SRR у одного аккаунта | subscription_requests |
под одной подпиской сидит зоопарк клиентов |
| Отчёты плагина Torrent Blocker | API, /api/node-plugins/torrent-blocker |
качает торренты через ноду |
| HWID-устройства против лимита | API, /api/hwid/devices |
сколько устройств реально привязано к подписке |
| Подключения с адресов собственных нод | API, /api/nodes (address + ips, 3.3.0+) |
это каскад, а не абузер: из хостинга и score исключается |
Всё это сводится в один score — колонка в дашборде Abuse Radar, сортировка
по убыванию. Формула простая, лежит в
internal/schema/sql/03_views.sql и правится
под себя. Каждый пойманный торрент — 10 очков: это не эвристика, а факт.
Каскадные ноды не должны попадать в абузеры: мост ходит на выходную ноду под
служебным юзером, из дата-центра и с огромным трафиком — по всем признакам это
первое место в рейтинге. Поэтому адреса собственных нод (поле address каждой
ноды, если надо — через DNS, плюс список ips, который появился в 3.3.0)
помечаются is_infra, в hosting_ips не считаются, а аккаунт, который виден
только с них, получает score = 0. Если мост ходит с адреса, которого панель не
знает, — его стоит дописать ноде в ips.
Схемы сообщений взяты не на глаз, а из контракта панели —
libs/contract/models/export-stream/export-stream.schema.ts в
remnawave/backend (он же
RemnawaveUserUsageStreamMessageDto и соседи в OpenAPI). В 3.4.5 этот файл
идентичен версии из 3.1.1, версия сообщений во всех трёх стримах — 1:
| Стрим | DTO | Периодичность |
|---|---|---|
ioraw:export:user_usage |
RemnawaveUserUsageStreamMessageDto |
батчами по мере учёта трафика |
ioraw:export:subscription_requests |
RemnawaveSubscriptionRequestStreamMessageDto |
на каждый запрос подписки |
ioraw:export:node_connections |
RemnawaveNodeConnectionsStreamMessageDto |
снапшот на ноду примерно раз в 5 минут |
Одно расхождение было: контракт объявляет поле srrResponseType, а панели с
3.1.0 по 3.4.3 клали в стрим ssrResponseType — перестановка букв в
subscription-requests.processor.ts, исправленная в 3.4.4. Парсер читает оба
написания: после обновления панели в стриме ещё могут лежать старые записи.
Остальное приходит не из стримов, а из REST API тем же токеном, что и справочники:
| Источник | Эндпоинт | Как часто |
|---|---|---|
| Отчёты Torrent Blocker | GET /api/node-plugins/torrent-blocker |
TORRENT_POLL_INTERVAL, по умолчанию раз в минуту, только новое |
| HWID-устройства | GET /api/hwid/devices |
вместе со справочниками, DICT_REFRESH_INTERVAL |
| Юзеры, ноды, адреса нод | GET /api/users/stream, GET /api/nodes |
DICT_REFRESH_INTERVAL, по умолчанию 5 минут |
Отчёты торрентов при первом запуске подтягиваются за всю историю (до 100 000 последних), дальше — только новее последнего сохранённого. Работает, только если на нодах включён плагин Torrent Blocker.
В .env Remnawave:
EXPORT_TO_STREAM_ENABLED=true
EXPORT_TO_STREAM_MAXLEN=3000EXPORT_TO_STREAM_MAXLEN — это MAXLEN ~ стрима: сколько сообщений держится,
пока консьюмер отстаёт. 3000 по умолчанию маловато: если экспортер пролежал ночь,
разумнее поднять до 100000+, места это ест немного.
После перезапуска панели стоит убедиться, что данные реально льются:
# deploy/remnawave/stream-debugger.yml кладётся рядом с compose-файлом панели
docker compose -f docker-compose.yml -f stream-debugger.yml up -d rw-stream-debugger
docker logs -f rw-stream-debuggerПусто в логах — значит дело в панели, а не в экспортере. Дебаггер стоит выключить сразу после первых сообщений: он читает те же стримы.
Ему нужен сокет Valkey, поэтому он живёт там же, где панель. Grafana в комплект не входит — предполагается, что она уже развёрнута.
git clone https://github.com/prettyleaf/remnanode-exporter && cd remnanode-exporter
cp .env.example .env
$EDITOR .env
docker compose up -dДефолты тома и сети (valkey-socket, remnawave-network) подходят штатной
установке: панель объявляет и то и другое явным name: в своём
docker-compose-prod.yml, поэтому префикса compose-проекта на них нет. Если
стек переименован — проверить и прописать в .env:
docker volume ls | grep valkey # -> VALKEY_SOCKET_VOLUME
docker network ls | grep remnawave # -> REMNAWAVE_NETWORKВалки в панели стартует с --port 0, то есть TCP у неё выключен и сокет —
единственный вход. Если у вас она всё-таки слушает порт, хватит
REDIS_URL=redis://:password@host:6379/0; на панелях 3.3.0+, где появился
REDIS_USERNAME (ACL валки), — redis://user:password@host:6379/0.
CLICKHOUSE_BIND в .env — адрес, по которому Grafana достучится до ClickHouse.
На 0.0.0.0 не вешать, 9000 это нативный протокол без TLS.
CLICKHOUSE_BIND=127.0.0.1 # Grafana на этом хосте, вне docker
CLICKHOUSE_BIND=172.17.0.1 # Grafana в docker на этом хосте
CLICKHOUSE_BIND=100.x.x.x # Grafana на другом сервере, через NetBirdДальше три шага в самой Grafana:
- Плагин — в окружение её контейнера добавляется
GF_INSTALL_PLUGINS=grafana-clickhouse-datasource, после чего контейнер перезапускается. - Датасорс — Connections → Add new connection → ClickHouse. Host — то же
значение, что в
CLICKHOUSE_BIND, port9000, protocol Native, базаremnawave, юзер и пароль из.env. - Дашборды — Dashboards → New → Import, по очереди три JSON из
dashboards/. Датасорс в них переменная, Grafana подставит ClickHouse сама.
dial tcp 127.0.0.1:9000: connect: connection refused при сохранении
датасорса — значит Grafana крутится в docker, а 127.0.0.1 внутри её контейнера —
это loopback самого контейнера, а не хоста. Лечится так: CLICKHOUSE_BIND=172.17.0.1
в .env, docker compose up -d (адрес порта меняется только при пересоздании
контейнера) и тот же 172.17.0.1 в Host датасорса. Проверить, что порт виден из
контейнера Grafana (имя — из docker ps):
docker exec grafana wget -T3 -O- http://172.17.0.1:9000
# HTTP/1.0 400 Bad Request — достучались: это ClickHouse отказывается говорить HTTP на 9000
# Connection refused — на этом адресе порт не опубликованМетрики самого экспортера (необязательно) — /metrics на 127.0.0.1:9102.
Готовый scrape-конфиг: deploy/vmagent/remnanode-exporter.yml.
Порт именно 9102, потому что 9101 обычно занят cAdvisor'ом.
Три штуки, живут рядом с дашбордом 25064 без конфликтов — разные датасорсы:
Node Load (rw-node-load) — кто грузит ноду прямо сейчас. Стек трафика по
нодам, топ-10 аккаунтов, таблица топ-талкеров с пиковыми Mbps за 5-минутку
(короткий всплеск не размазывается по часу), топ-5 аккаунтов на каждой ноде с
долей от ноды. Сверху фильтр по нодам.
Abuse Radar (rw-abuse-radar) — главная таблица со score (с колонками
торрентов, HWID-устройств и сквадов), плюс: сколько сетей у аккаунта во
времени, адреса, за которыми сидит больше одного аккаунта, подключения из
дата-центров (без собственных нод), география каждого аккаунта и отдельный
блок Torrent Blocker — кто качал и полный лог отчётов.
Subscription Requests (rw-sub-requests) — запросы подписки по типу клиента,
самые активные аккаунты, разбивка по типам ответа SRR и по правилам, которые
отбили запрос, разбивка по клиентам/странам/ASN и сырой лог последних запросов.
Нужен бесплатный аккаунт GeoLite2.
MAXMIND_ACCOUNT_ID и MAXMIND_LICENSE_KEY в .env — контейнер geoipupdate
качает GeoLite2-City и GeoLite2-ASN и обновляет раз в сутки, экспортер
переоткрывает файлы на ходу (GEOIP_RELOAD_INTERVAL, по умолчанию 6h).
Без ключей всё продолжает работать, но страна/город/ASN будут пустыми, а вместе
с ними отвалится детект хостингов. Определение «это дата-центр» — по ключевым
словам в названии AS-организации, список в
internal/geoip/geoip.go (DefaultHostingKeywords).
Адреса схлопываются в /24 (IPv4) и /48 (IPv6): считать уникальные сети, а не уникальные адреса, — единственный способ не ловить ложную «раздачу» на каждом мобильном операторе с CGNAT.
В стримах ездят числовые id. И юзеры, и ноды резолвятся через API панели —
/api/users/stream и /api/nodes отдают те же числовые id, что лежат в
стримах. Нужен только REMNAWAVE_API_TOKEN.
Заодно в dim_users ложится то, что помогает решить, что делать с аккаунтом:
внутренние сквады (тариф), Telegram ID, дата создания (свежий аккаунт с
сотнями гигабайт — классика), последний онлайн, стратегия сброса и
израсходованный трафик. В dim_nodes — адрес, провайдер и теги ноды.
Секреты подписки (shortUuid, subscriptionUrl, ключи протоколов) экспортер
не читает и не хранит.
Токен лучше выпустить узкий. Скоупы, которые экспортер использует, — все читающие:
| Скоуп | Зачем | Без него |
|---|---|---|
users:stream |
имена юзеров и их поля | user-42 в дашбордах |
nodes:list |
имена нод и адреса для детекта каскадов | node-1, каскады в абузерах |
hwid-user-devices:list |
число HWID-устройств | колонка hwid_devices пустая |
node-plugins:torrent-blocker-reports |
отчёты Torrent Blocker | торренты не видны |
system:metadata, system:configuration |
проверки на старте, см. ниже | проверки молча пропускаются |
Вместо точечных скоупов годятся и users:read, nodes:read и т.д. Без
скоупа на HWID или торренты экспортер один раз пишет об этом в INFO и дальше
не шумит.
Числовой id ноды появился в REST API только в 3.1.0 — до этого имена приходилось
тянуть напрямую из Postgres панели, и на более старой панели ноды останутся
безымянными. Без токена в дашбордах будет node-1 и user-42, всё остальное
работает.
Тем же токеном экспортер один раз на старте дёргает два эндпоинта и пишет в первые строки лога, с какой панелью он вообще разговаривает.
GET /api/system/metadata (есть с 3.0.0) — версия панели уезжает в INFO
полем panel_version. Если она младше 3.1.0, будет WARN: на такой панели
ноды останутся безымянными, а в запросах подписки не будет полей Subscription
Response Rules.
GET /api/system/configuration (появился в 3.2.0) — как панель настроена на
экспорт. Смысл один: самый неприятный сценарий — когда экспортер поднялся без
ошибок, а данных нет, потому что в панели забыли
EXPORT_TO_STREAM_ENABLED=true. Теперь про это будет WARN. Заодно в INFO
уезжает user_usage_ignore_below_bytes — панель отбрасывает мелкие дельты
трафика ещё до стрима, и это штатное объяснение «почему в ClickHouse не все
байты».
Обе проверки необязательные: нет эндпоинта (404) или у токена нет скоупа
(system:metadata и system:configuration соответственно, 403) — проверка
молча уходит в DEBUG и ни на что не влияет.
- Consumer group на каждый стрим,
XREADGROUPплюсXAUTOCLAIMдля зависших записей. Несколько реплик экспортера с одинаковымREDIS_GROUPи разнымиREDIS_CONSUMERделят нагрузку. - At-least-once:
XACKтолько после успешной вставки в ClickHouse. Падение между вставкой и ack продублирует записи — роллапы аддитивные, а там, где это важно, считаетсяuniq()по сущностям, а не сырые строки. - Битое сообщение не блокирует стрим: логируется, ack-ается, обработка идёт
дальше (счётчик
remnanode_exporter_messages_failed_total). - Схема ClickHouse вшита в бинарь и применяется на старте. Апгрейд
экспортера апгрейдит таблицы, в том числе на уже инициализированном томе, где
docker-entrypoint-initdb.dдавно не отрабатывает: новые колонки доезжают черезALTER ... ADD COLUMN IF NOT EXISTS, а роллап-вьюхи — черезALTER ... MODIFY QUERY. - TTL: сырые снапшоты подключений 30 дней, запросы подписки 60, трафик 90,
отчёты Torrent Blocker 180, 5-минутные роллапы 90–365. Правится в
internal/schema/sql/. - Отчёты торрентов без дублей: поллер перечитывает пару минут за курсором
(отчёт мог закоммититься позже соседнего), уже записанное помнит в памяти, а
ReplacingMergeTreeсFINALво вьюхеv_torrent_reportsподчищает перечитанное после рестарта.TRUNCATEотчётов в панели сбрасывает id, но не время, поэтому ключ — пара (id, время). - ClickHouse ужат под соседей: по умолчанию он забирает до 90% RAM, а тут
делит машину с панелью, Postgres и Valkey. В
deploy/clickhouse/config.d/exporter.xmlему оставлена треть памяти и урезаны кеши. На выделенном сервере лимиты можно вернуть обратно.
http://127.0.0.1:9102/metrics — прочитано и провалено сообщений по стримам,
вставлено строк по таблицам, латентность вставки, размеры справочников и
remnanode_exporter_stream_pending — отставание консьюмера, именно его и стоит
алертить. /healthz пингует Redis.
make test # юнит-тесты, без внешних зависимостей
make lint
make build
# полный прогон по живому ClickHouse: схема, апгрейд схемы, пайплайн
# и КАЖДЫЙ запрос из дашбордов
docker run --rm -d -p 9000:9000 --name ch clickhouse/clickhouse-server:24.12-alpine
make e2einternal/e2e вытаскивает rawSql из всех панелей dashboards/*.json,
подставляет макросы Grafana и выполняет их. Опечатка в панели ломает тесты, а не
дашборд в проде. Там же синк справочников, HWID и торрентов гоняется против
фейковой панели с ответами в формате 3.4.5.
MIT.