Skip to content

feat(plugin): add abuse blocker runtime - #46

Draft
l0nelynx wants to merge 3 commits into
remnawave:devfrom
l0nelynx:staging/abuse-blocker-node
Draft

feat(plugin): add abuse blocker runtime#46
l0nelynx wants to merge 3 commits into
remnawave:devfrom
l0nelynx:staging/abuse-blocker-node

Conversation

@l0nelynx

@l0nelynx l0nelynx commented Aug 15, 2026

Copy link
Copy Markdown

Suggested PR title

feat(plugin): add abuse blocker runtime

Summary

This PR adds the Node-side runtime for the new optional abuseBlocker plugin introduced by remnawave/backend#207.

It observes Xray routing webhooks, detects scan-like behavior per numeric user ID, maintains a rolling in-memory score, buffers idempotent reports for the backend, exposes health and coverage data, and temporarily blocks the current source IP in a dedicated nftables set when the configured score reaches the blocking threshold.

This is PR 2 of 3. It targets dev and is intentionally opened as a Draft while @remnawave/node-plugins@0.7.0 is awaiting publication and the final @remnawave/node-contract release version is coordinated with the backend runtime PR.

Motivation

torrentBlocker can react to one Xray protocol classification. Scan detection is stateful: it must aggregate unique destinations across multiple routing events for the same user, port, and time window without changing the selected outbound.

The implementation therefore observes routing decisions through Xray webhooks and keeps only bounded, ephemeral detector state on each Node. Xray-core is not patched, traffic is not sent through loopback, and Node-local scores are not combined across Nodes.

Detection and scoring

Only events with all of the following are processed:

  • TCP network;
  • numeric userId from the Xray email field;
  • source IP;
  • destination IP and port.

The destination is selected in this order: originalTarget, routeTarget, then destination. Domain-only destinations and malformed endpoints are ignored.

The default detectors are:

  1. Horizontal scan

    • same user;
    • same destination port, excluding 80 and 443 by default;
    • same IPv4 /24 or IPv6 /64;
    • at least 20 unique destination IPs in 60 seconds;
    • adds 100 points.
  2. Destination sweep

    • same user;
    • same non-web destination port;
    • at least 50 unique destination IPs in 60 seconds;
    • adds 50 points.

Score events are retained for a rolling hour by default:

  • below 50: no report or action;
  • 50–99: buffer a suspicious report;
  • 100–149: buffer an alert report and retain enhanced evidence;
  • 150 or more: buffer a blocked report and block the current source IP for 10 minutes.

A continuously active detector key scores once. It is re-armed only after its rolling set falls below the threshold and the default 300-second cooldown has elapsed. If both detectors cross their thresholds on the same observation, their scores are aggregated into one event and one action.

Buffered reports are keyed by eventId. Later traffic can expand evidence for an existing alert without adding score or repeating the action. Collection uses the same destructive-flush model as torrentBlocker.

Xray routing instrumentation

The generated routing configuration now uses separate internal endpoints:

  • /internal/webhook/torrent;
  • /internal/webhook/abuse;
  • /internal/webhook/combined;
  • the existing /internal/webhook remains a torrent-compatible alias.

When Abuse Blocker is enabled:

  • rules without a webhook receive the abuse endpoint with deduplication: 0;
  • rules observed by both plugins receive the combined endpoint;
  • the previous five-second torrent deduplication is enforced in Node instead of Xray;
  • existing external webhooks are preserved and counted as skipped coverage;
  • a final TCP rule observes the normal default outbound without changing it;
  • IPIfNonMatch uses an IPv4/IPv6 catch-all so Xray's DNS second pass remains intact;
  • if the default outbound cannot be addressed by tag, the catch-all is omitted and coverage is reported as partial.

The generated configurations for AsIs, IPOnDemand, and IPIfNonMatch were validated with Xray v26.7.28, including unchanged selected outbounds.

State and memory bounds

All detector state is local RAM and is reset when Node restarts or the plugin configuration is replaced.

  • unique destinations use exact Map<destinationIp, lastSeen> rolling windows;
  • users and detector keys use LRU eviction;
  • defaults are 50,000 users, 256 keys per user, and 10,000 buffered reports;
  • evidence defaults to 10 destinations and expands to 50 at alert score;
  • eviction and drop counters are exposed through Node health.

The plugin honors user, source-IP/CIDR, and destination-IP/CIDR ignore lists, including resolved shared ext: IP lists.

nftables enforcement

  • Adds the dedicated abuse-blocker ingress address set to the existing remnanode table.
  • Supports IPv4 and IPv6 timeout elements through nftables-napi.
  • Initial Node-side blocks use initialBlockSeconds (600 seconds by default).
  • Adds an authenticated refresh command that serializes remove -> add with a new timeout for backend repeat-offender escalation.
  • Publishes DropConnectionsEvent after a block or refresh so active connections are terminated.
  • Keeps Abuse Blocker entries separate from torrent-blocker and ingress-filter sets.

Node contract additions

The embedded @remnawave/node-contract source adds:

  • abuse report schemas with eventId, user/source/destination data, rule hits, score transition, severity, evidence, action result, full policy snapshot, config fingerprint, coverage mode, and raw Xray report;
  • POST /node/plugin/abuse-blocker/collect;
  • POST /node/plugin/abuse-blocker/refresh-block;
  • Abuse Blocker health fields in system stats.

The package version is deliberately not bumped in this Draft branch because the repository's package workflow publishes on any push that changes libs/**/package.json. The coordinated release bump will be added only when the dependent backend PR is ready. The expected next version is 3.3.0 unless maintainers choose another release number.

Health fields

System stats now include:

  • available and enabled;
  • buffered report count;
  • tracked users and active incidents;
  • coverage mode and skipped webhook rule count;
  • user/key eviction and report-drop counters;
  • last detector/enforcement error.

Compatibility and limitations

  • Existing configurations without abuseBlocker remain unchanged.
  • Existing external routing webhooks are never overwritten.
  • Temporary enforcement affects only the current source IP on the Node that detected the incident.
  • Scores are not shared between Nodes.
  • RAM state is intentionally lost on restart.
  • A destructive report flush can lose a batch if backend transport fails after collection.
  • Global repeat-offender escalation and account disablement belong to PR 3/3.

Tests

Added 25 focused tests covering:

  • IPv4 /24 and IPv6 /64 aggregation;
  • exact 60-second boundary and expiration;
  • uniqueness, different users, different ports, excluded ports, and ignore ranges;
  • UDP, non-numeric users, and domain-only destinations;
  • score windows, simultaneous rule scoring, cooldown/re-arm, evidence updates, and memory limits;
  • all routing strategies, default outbound, external webhooks, and combined torrent behavior;
  • dedicated nftables set use, IPv4/IPv6 timeouts, serialized refresh, and connection drop.

Local validation:

npx oxfmt --check <changed files>
npx oxlint <changed TypeScript files>
npx tsx --test tests/abuse-blocker/*.test.ts
npm run typecheck
npm run build
cd libs/contract && npm run build
xray run -test -c <generated config>  # v26.7.28, all three domain strategies

All 25 focused tests, typecheck, Node build, contract build, and Xray validation pass using the locally packed @remnawave/node-plugins@0.7.0 schema from PR 1.

Dependency chain

Before this Draft is marked ready, the registry dependency and lockfile will be finalized and the full repository CI suite will be rerun.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant