From 9d3319216cd7ebbfffb111d0ca4e1dccb912c716 Mon Sep 17 00:00:00 2001 From: kastov Date: Sun, 8 Mar 2026 18:48:42 +0300 Subject: [PATCH] chore: update @nestjs packages and other dependencies; refactor torrent blocker state management and API config generation --- package-lock.json | 40 +++++------ package.json | 8 +-- src/common/utils/generate-api-config.ts | 72 ++++++++++++++----- src/modules/_plugin/plugin.service.ts | 16 ++++- .../get-torrent-blocker-state.handler.ts | 10 ++- .../get-torrent-blocker-state.query.ts | 5 +- .../services/states/torrent-blocker.state.ts | 11 +++ src/modules/xray-core/xray.service.ts | 10 +-- 8 files changed, 120 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc3065b..f40b8e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,17 +12,17 @@ "@henrygd/semaphore": "0.1.0", "@kastov/body-parser-with-zstd": "2.2.2", "@kastov/node-supervisord": ">=2.0.3", - "@nestjs/common": "11.1.15", + "@nestjs/common": "11.1.16", "@nestjs/config": "4.0.3", - "@nestjs/core": "11.1.15", + "@nestjs/core": "11.1.16", "@nestjs/cqrs": "11.0.3", "@nestjs/jwt": "11.0.2", "@nestjs/passport": "11.0.5", - "@nestjs/platform-express": "11.1.15", + "@nestjs/platform-express": "11.1.16", "@peculiar/webcrypto": "^1.5.0", "@peculiar/x509": "^1.14.3", "@remnawave/hashed-set": "^0.0.4", - "@remnawave/node-plugins": "0.1.1", + "@remnawave/node-plugins": "0.1.3", "@remnawave/supervisord-nestjs": "0.3.1", "@remnawave/xtls-sdk": "0.11.1", "@remnawave/xtls-sdk-nestjs": "0.6.1", @@ -1207,9 +1207,9 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.15", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.15.tgz", - "integrity": "sha512-51sqIj83xEhmxi3D+QA+bo+hs+YTnZ5eJvOxa34VFiSlilT8DEF83kZc4t1t3arO8PjfDFI+LaTQQkx0gm1QGA==", + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.16.tgz", + "integrity": "sha512-JSIeW+USuMJkkcNbiOdcPkVCeI3TSnXstIVEPpp3HiaKnPRuSbUUKm9TY9o/XpIcPHWUOQItAtC5BiAwFdVITQ==", "license": "MIT", "dependencies": { "file-type": "21.3.0", @@ -1253,9 +1253,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.15", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.15.tgz", - "integrity": "sha512-s+SQxA8yAGu8RapbI5Y3GFuNyvF1ocva2Va+d4K5T7G0eC5kAvYfiHr8iKvZg+rue5NjV7fYQQmEvA5jCTG+CA==", + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.16.tgz", + "integrity": "sha512-tXWXyCiqWthelJjrE0KLFjf0O98VEt+WPVx5CrqCf+059kIxJ8y1Vw7Cy7N4fwQafWNrmFL2AfN87DDMbVAY0w==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1329,14 +1329,14 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.15", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.15.tgz", - "integrity": "sha512-sR42dQ5fPy4NGbnhT4mRIJLL0h2eNY4KpLkcfd27vg5XdTjQQ07wkQORrw6rAkWkBvMRr0dmhAcUBnXpe7ro5Q==", + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.16.tgz", + "integrity": "sha512-IOegr5+ZfUiMKgk+garsSU4MOkPRhm46e6w8Bp1GcO4vCdl9Piz6FlWAzKVfa/U3Hn/DdzSVJOW3TWcQQFdBDw==", "license": "MIT", "dependencies": { "cors": "2.8.6", "express": "5.2.1", - "multer": "2.1.0", + "multer": "2.1.1", "path-to-regexp": "8.3.0", "tslib": "2.8.1" }, @@ -2068,9 +2068,9 @@ "license": "AGPL-3.0-only" }, "node_modules/@remnawave/node-plugins": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@remnawave/node-plugins/-/node-plugins-0.1.1.tgz", - "integrity": "sha512-UxiEG6+hjjCDRjXlAieq4vx9KIJNwI36hdup3vl6fTXYUy4csnPR6JsPC5AhGj1n4B539ifkEr2MuWtwXLfx5w==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@remnawave/node-plugins/-/node-plugins-0.1.3.tgz", + "integrity": "sha512-n5gZGHpC3pJVB99+BeVX9ZadKIyAajHDlMBviyJOepb2R9gB7eICPxIt9YroLS/5/GdkcLNd6ojveE2fvDF4Jw==", "license": "AGPL-3.0-only", "dependencies": { "zod": "3.25.76" @@ -6075,9 +6075,9 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.0.tgz", - "integrity": "sha512-TBm6j41rxNohqawsxlsWsNNh/VdV4QFXcBvRcPhXaA05EZ79z0qJ2bQFpync6JBoHTeNY5Q1JpG7AlTjdlfAEA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", diff --git a/package.json b/package.json index cb495e8..5df4339 100644 --- a/package.json +++ b/package.json @@ -35,17 +35,17 @@ "@henrygd/semaphore": "0.1.0", "@kastov/body-parser-with-zstd": "2.2.2", "@kastov/node-supervisord": ">=2.0.3", - "@nestjs/common": "11.1.15", + "@nestjs/common": "11.1.16", "@nestjs/config": "4.0.3", - "@nestjs/core": "11.1.15", + "@nestjs/core": "11.1.16", "@nestjs/jwt": "11.0.2", "@nestjs/passport": "11.0.5", - "@nestjs/platform-express": "11.1.15", + "@nestjs/platform-express": "11.1.16", "@peculiar/webcrypto": "^1.5.0", "@peculiar/x509": "^1.14.3", "@remnawave/hashed-set": "^0.0.4", "@nestjs/cqrs": "11.0.3", - "@remnawave/node-plugins": "0.1.1", + "@remnawave/node-plugins": "0.1.3", "@remnawave/supervisord-nestjs": "0.3.1", "@remnawave/xtls-sdk": "0.11.1", "@remnawave/xtls-sdk-nestjs": "0.6.1", diff --git a/src/common/utils/generate-api-config.ts b/src/common/utils/generate-api-config.ts index 48feca1..12195cb 100644 --- a/src/common/utils/generate-api-config.ts +++ b/src/common/utils/generate-api-config.ts @@ -15,14 +15,32 @@ import { getServerCerts } from './generate-mtls-certs'; import { getXtlsApiPort } from './get-initial-ports'; import { IPolicyConfig } from './interfaces'; -export const generateApiConfig = ( - config: Record, - isTorrentBlockerEnabled: boolean, +interface IRoutingXrayConfig { + rules: { + ruleTag?: string; + webhook?: { + url: string; + deduplication: number; + }; + [key: string]: unknown; + }[]; +} + +interface IGenerateApiConfigParams { + config: Record; + torrentBlockerState: { + enabled: boolean; + includeRuleTags: Set; + }; internal: { socketPath: string; token: string; - }, -): Record => { + }; +} + +export const generateApiConfig = (args: IGenerateApiConfigParams): Record => { + const { config, torrentBlockerState, internal } = args; + const policyConfig = config.policy as undefined | IPolicyConfig; const serverCerts = getServerCerts(); const hasCapNetAdminResult = hasCapNetAdmin(); @@ -39,7 +57,7 @@ export const generateApiConfig = ( system: XRAY_DEFAULT_POLICY_MODEL.policy.system, }; - return { + const result = { ...config, ...XRAY_DEFAULT_STATS_MODEL, ...XRAY_DEFAULT_API_MODEL, @@ -52,24 +70,44 @@ export const generateApiConfig = ( }), ...(Array.isArray(config.inbounds) ? config.inbounds : []), ], - outbounds: [ - ...(Array.isArray(config.outbounds) ? config.outbounds : []), - ...(isTorrentBlockerEnabled ? [XRAY_TORRENT_BLOCKER_OUTBOUND_MODEL] : []), - ], + outbounds: [...(Array.isArray(config.outbounds) ? config.outbounds : [])], policy: builtPolicy, routing: { ...(config.routing || {}), rules: [ XRAY_ROUTING_RULES_MODEL, - ...(isTorrentBlockerEnabled - ? [ - XRAY_TORRENT_BLOCKER_ROUTING_RULES_MODEL({ - webhookUrl: `/${internal.socketPath}:${XRAY_INTERNAL_FULL_WEBHOOK_PATH}?token=${internal.token}`, - }), - ] - : []), ...((config.routing as { rules?: unknown[] })?.rules || []), ], }, }; + + if (torrentBlockerState.enabled) { + const webhookUrl = buildWebhookUrl(internal); + const routing = result.routing as IRoutingXrayConfig; + + result.outbounds.push(XRAY_TORRENT_BLOCKER_OUTBOUND_MODEL); + + routing.rules.splice(1, 0, XRAY_TORRENT_BLOCKER_ROUTING_RULES_MODEL({ webhookUrl })); + + if (torrentBlockerState.includeRuleTags.size > 0) { + for (const rule of routing.rules) { + if ( + rule.ruleTag && + typeof rule.ruleTag === 'string' && + torrentBlockerState.includeRuleTags.has(rule.ruleTag) + ) { + rule.webhook = { + url: webhookUrl, + deduplication: 30, + }; + } + } + } + } + + return result; +}; + +const buildWebhookUrl = (internal: { socketPath: string; token: string }): string => { + return `/${internal.socketPath}:${XRAY_INTERNAL_FULL_WEBHOOK_PATH}?token=${internal.token}`; }; diff --git a/src/modules/_plugin/plugin.service.ts b/src/modules/_plugin/plugin.service.ts index 479b321..a07fc2b 100644 --- a/src/modules/_plugin/plugin.service.ts +++ b/src/modules/_plugin/plugin.service.ts @@ -95,9 +95,18 @@ export class PluginService { this.logger.log('[PLUGIN] Plugins changed...'); if (currentTorrentBlocker && !pluginData.torrentBlocker?.enabled) { - await this.commandBus.execute( - new RemoveOutboundCommand(XRAY_TORRENT_BLOCKER_OUTBOUND_TAG), - ); + if (!pluginData.torrentBlocker?.includeRuleTags) { + await this.commandBus.execute( + new RemoveOutboundCommand(XRAY_TORRENT_BLOCKER_OUTBOUND_TAG), + ); + } else { + await this.commandBus.execute( + new StopXrayCommand({ + withOnlineCheck: true, + withPluginCleanup: false, + }), + ); + } } else if (!currentTorrentBlocker && pluginData.torrentBlocker?.enabled) { await this.commandBus.execute( new StopXrayCommand({ @@ -158,6 +167,7 @@ export class PluginService { this.state.torrentBlocker.setIgnoredIps(ips); this.state.torrentBlocker.setIgnoredUsers(users); this.state.torrentBlocker.configure(blockDuration); + this.state.torrentBlocker.setIncludeRuleTags(pluginData.torrentBlocker.includeRuleTags); this.logger.log( `[PLUGIN] Torrent-Blocker: blockDuration=${blockDuration}s, ${ips.length} ignored IPs, ${users.length} ignored users`, diff --git a/src/modules/_plugin/queries/get-torrent-blocker-state/get-torrent-blocker-state.handler.ts b/src/modules/_plugin/queries/get-torrent-blocker-state/get-torrent-blocker-state.handler.ts index caaac0d..ae53bb4 100644 --- a/src/modules/_plugin/queries/get-torrent-blocker-state/get-torrent-blocker-state.handler.ts +++ b/src/modules/_plugin/queries/get-torrent-blocker-state/get-torrent-blocker-state.handler.ts @@ -13,10 +13,16 @@ export class GetTorrentBlockerStateHandler implements IQueryHandler(), + }; } } } diff --git a/src/modules/_plugin/queries/get-torrent-blocker-state/get-torrent-blocker-state.query.ts b/src/modules/_plugin/queries/get-torrent-blocker-state/get-torrent-blocker-state.query.ts index dd01e1c..8d00ccb 100644 --- a/src/modules/_plugin/queries/get-torrent-blocker-state/get-torrent-blocker-state.query.ts +++ b/src/modules/_plugin/queries/get-torrent-blocker-state/get-torrent-blocker-state.query.ts @@ -1,6 +1,9 @@ import { Query } from '@nestjs/cqrs'; -export class GetTorrentBlockerStateQuery extends Query { +export class GetTorrentBlockerStateQuery extends Query<{ + enabled: boolean; + includeRuleTags: Set; +}> { constructor() { super(); } diff --git a/src/modules/_plugin/services/states/torrent-blocker.state.ts b/src/modules/_plugin/services/states/torrent-blocker.state.ts index 785be6a..5be5cba 100644 --- a/src/modules/_plugin/services/states/torrent-blocker.state.ts +++ b/src/modules/_plugin/services/states/torrent-blocker.state.ts @@ -5,6 +5,7 @@ export class TorrentBlockerState { private blockDuration: number | null = null; private ignoredIps = new Set(); private ignoredUsers = new Set(); + private includeRuleTags = new Set(); private reports: TorrentBlockerReportModel[] = []; get isEnabled(): boolean { @@ -55,5 +56,15 @@ export class TorrentBlockerState { this.blockDuration = null; this.ignoredIps.clear(); this.ignoredUsers.clear(); + this.includeRuleTags.clear(); + } + + setIncludeRuleTags(tags: string[] | undefined): void { + if (!tags) return; + this.includeRuleTags = new Set(tags); + } + + get includeRuleTagsSet(): Set { + return this.includeRuleTags; } } diff --git a/src/modules/xray-core/xray.service.ts b/src/modules/xray-core/xray.service.ts index 94bab1d..8fab631 100644 --- a/src/modules/xray-core/xray.service.ts +++ b/src/modules/xray-core/xray.service.ts @@ -160,11 +160,11 @@ export class XrayService implements OnApplicationBootstrap { new GetTorrentBlockerStateQuery(), ); - const fullConfig = generateApiConfig( - body.xrayConfig, - isTorrentBlockerEnabled, - this.internal, - ); + const fullConfig = generateApiConfig({ + config: body.xrayConfig, + torrentBlockerState: isTorrentBlockerEnabled, + internal: this.internal, + }); await this.internalService.extractUsersFromConfig(body.internals.hashes, fullConfig);