Skip to content

About

Node exporter for inspecting abuse actions and viewing detailed information with dashboards

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

remnanode-exporter

Ловит тех, кто грузит ноды.

Читает 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.

Запуск

1. Включить экспорт в панели

В .env Remnawave:

EXPORT_TO_STREAM_ENABLED=true
EXPORT_TO_STREAM_MAXLEN=3000

EXPORT_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

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

2. Поднять экспортер на сервере панели

Ему нужен сокет 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.

3. Подключить свою Grafana

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:

  1. Плагин — в окружение её контейнера добавляется GF_INSTALL_PLUGINS=grafana-clickhouse-datasource, после чего контейнер перезапускается.
  2. Датасорс — Connections → Add new connection → ClickHouse. Host — то же значение, что в CLICKHOUSE_BIND, port 9000, protocol Native, база remnawave, юзер и пароль из .env.
  3. Дашборды — 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 и сырой лог последних запросов.

GeoIP

Нужен бесплатный аккаунт 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 e2e

internal/e2e вытаскивает rawSql из всех панелей dashboards/*.json, подставляет макросы Grafana и выполняет их. Опечатка в панели ломает тесты, а не дашборд в проде. Там же синк справочников, HWID и торрентов гоняется против фейковой панели с ответами в формате 3.4.5.

Лицензия

MIT.

About

Node exporter for inspecting abuse actions and viewing detailed information with dashboards

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages