diff --git a/libs/contract/api/controllers/plugin.ts b/libs/contract/api/controllers/plugin.ts index 58173c4..eae3801 100644 --- a/libs/contract/api/controllers/plugin.ts +++ b/libs/contract/api/controllers/plugin.ts @@ -1,6 +1,7 @@ export const PLUGIN_CONTROLLER = 'plugin' as const; export const TORRENT_BLOCKER_ROUTE = 'torrent-blocker' as const; +export const ABUSE_BLOCKER_ROUTE = 'abuse-blocker' as const; export const NFTABLES_ROUTE = 'nftables' as const; export const PLUGIN_ROUTES = { @@ -9,6 +10,10 @@ export const PLUGIN_ROUTES = { TORRENT_BLOCKER: { COLLECT: `${TORRENT_BLOCKER_ROUTE}/collect`, }, + ABUSE_BLOCKER: { + COLLECT: `${ABUSE_BLOCKER_ROUTE}/collect`, + REFRESH_BLOCK: `${ABUSE_BLOCKER_ROUTE}/refresh-block`, + }, NFTABLES: { UNBLOCK_IPS: `${NFTABLES_ROUTE}/unblock-ips`, BLOCK_IPS: `${NFTABLES_ROUTE}/block-ips`, diff --git a/libs/contract/api/routes.ts b/libs/contract/api/routes.ts index fa5a5e9..fe2adf7 100644 --- a/libs/contract/api/routes.ts +++ b/libs/contract/api/routes.ts @@ -35,6 +35,10 @@ export const REST_API = { TORRENT_BLOCKER: { COLLECT: `${ROOT}/${CONTROLLERS.PLUGIN_CONTROLLER}/${CONTROLLERS.PLUGIN_ROUTES.TORRENT_BLOCKER.COLLECT}`, }, + ABUSE_BLOCKER: { + COLLECT: `${ROOT}/${CONTROLLERS.PLUGIN_CONTROLLER}/${CONTROLLERS.PLUGIN_ROUTES.ABUSE_BLOCKER.COLLECT}`, + REFRESH_BLOCK: `${ROOT}/${CONTROLLERS.PLUGIN_CONTROLLER}/${CONTROLLERS.PLUGIN_ROUTES.ABUSE_BLOCKER.REFRESH_BLOCK}`, + }, NFTABLES: { UNBLOCK_IPS: `${ROOT}/${CONTROLLERS.PLUGIN_CONTROLLER}/${CONTROLLERS.PLUGIN_ROUTES.NFTABLES.UNBLOCK_IPS}`, BLOCK_IPS: `${ROOT}/${CONTROLLERS.PLUGIN_CONTROLLER}/${CONTROLLERS.PLUGIN_ROUTES.NFTABLES.BLOCK_IPS}`, diff --git a/libs/contract/commands/plugin/abuse-blocker/collect-reports.schema.ts b/libs/contract/commands/plugin/abuse-blocker/collect-reports.schema.ts new file mode 100644 index 0000000..37b77c2 --- /dev/null +++ b/libs/contract/commands/plugin/abuse-blocker/collect-reports.schema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +import { REST_API } from '../../../api'; +import { AbuseBlockerReportSchema } from '../../../models'; + +export namespace CollectAbuseBlockerReportsCommand { + export const url = REST_API.PLUGIN.ABUSE_BLOCKER.COLLECT; + + export const ResponseSchema = z.object({ + response: z.object({ + reports: z.array(AbuseBlockerReportSchema), + }), + }); + + export type Response = z.infer; +} diff --git a/libs/contract/commands/plugin/abuse-blocker/index.ts b/libs/contract/commands/plugin/abuse-blocker/index.ts new file mode 100644 index 0000000..7c9d6e5 --- /dev/null +++ b/libs/contract/commands/plugin/abuse-blocker/index.ts @@ -0,0 +1,2 @@ +export * from './collect-reports.schema'; +export * from './refresh-block.schema'; diff --git a/libs/contract/commands/plugin/abuse-blocker/refresh-block.schema.ts b/libs/contract/commands/plugin/abuse-blocker/refresh-block.schema.ts new file mode 100644 index 0000000..3eb24e7 --- /dev/null +++ b/libs/contract/commands/plugin/abuse-blocker/refresh-block.schema.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; + +import { REST_API } from '../../../api'; + +export namespace RefreshAbuseBlockCommand { + export const url = REST_API.PLUGIN.ABUSE_BLOCKER.REFRESH_BLOCK; + + export const RequestSchema = z.object({ + ip: z.union([z.ipv4(), z.ipv6()]), + timeout: z.int().min(1).max(2592000), + }); + + export type Request = z.infer; + + export const ResponseSchema = z.object({ + response: z.object({ + accepted: z.boolean(), + }), + }); + + export type Response = z.infer; +} diff --git a/libs/contract/commands/plugin/index.ts b/libs/contract/commands/plugin/index.ts index ad0fd7e..8910470 100644 --- a/libs/contract/commands/plugin/index.ts +++ b/libs/contract/commands/plugin/index.ts @@ -1,3 +1,4 @@ +export * from './abuse-blocker'; export * from './nftables'; export * from './sync.command'; export * from './torrent-blocker'; diff --git a/libs/contract/commands/stats/get-system-stats.command.ts b/libs/contract/commands/stats/get-system-stats.command.ts index 9c11261..2a9e440 100644 --- a/libs/contract/commands/stats/get-system-stats.command.ts +++ b/libs/contract/commands/stats/get-system-stats.command.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { NodeSystemStatsSchema } from '../../models'; import { REST_API } from '../../api'; +import { NodeSystemStatsSchema } from '../../models'; export namespace GetSystemStatsCommand { export const url = REST_API.STATS.GET_SYSTEM_STATS; @@ -22,6 +22,19 @@ export namespace GetSystemStatsCommand { }) .nullable(), plugins: z.object({ + abuseBlocker: z.object({ + available: z.boolean(), + enabled: z.boolean(), + reportsCount: z.number(), + trackedUsers: z.number(), + activeIncidents: z.number(), + coverageMode: z.enum(['full', 'partial']), + skippedWebhookRules: z.number(), + evictedUsers: z.number(), + evictedKeys: z.number(), + droppedReports: z.number(), + lastError: z.string().nullable(), + }), torrentBlocker: z.object({ reportsCount: z.number(), }), diff --git a/libs/contract/constants/internal/internal.constants.ts b/libs/contract/constants/internal/internal.constants.ts index 6b66ad7..bedd4fd 100644 --- a/libs/contract/constants/internal/internal.constants.ts +++ b/libs/contract/constants/internal/internal.constants.ts @@ -3,3 +3,9 @@ export const XRAY_INTERNAL_API_PATH = '/get-config'; export const XRAY_INTERNAL_FULL_PATH = `/${XRAY_INTERNAL_API_CONTROLLER}${XRAY_INTERNAL_API_PATH}`; export const XRAY_INTERNAL_WEBHOOK_PATH = '/webhook'; export const XRAY_INTERNAL_FULL_WEBHOOK_PATH = `/${XRAY_INTERNAL_API_CONTROLLER}${XRAY_INTERNAL_WEBHOOK_PATH}`; +export const XRAY_INTERNAL_TORRENT_WEBHOOK_PATH = '/webhook/torrent'; +export const XRAY_INTERNAL_ABUSE_WEBHOOK_PATH = '/webhook/abuse'; +export const XRAY_INTERNAL_COMBINED_WEBHOOK_PATH = '/webhook/combined'; +export const XRAY_INTERNAL_FULL_TORRENT_WEBHOOK_PATH = `/${XRAY_INTERNAL_API_CONTROLLER}${XRAY_INTERNAL_TORRENT_WEBHOOK_PATH}`; +export const XRAY_INTERNAL_FULL_ABUSE_WEBHOOK_PATH = `/${XRAY_INTERNAL_API_CONTROLLER}${XRAY_INTERNAL_ABUSE_WEBHOOK_PATH}`; +export const XRAY_INTERNAL_FULL_COMBINED_WEBHOOK_PATH = `/${XRAY_INTERNAL_API_CONTROLLER}${XRAY_INTERNAL_COMBINED_WEBHOOK_PATH}`; diff --git a/libs/contract/constants/xray/stats.ts b/libs/contract/constants/xray/stats.ts index 315c8ff..435ac56 100644 --- a/libs/contract/constants/xray/stats.ts +++ b/libs/contract/constants/xray/stats.ts @@ -48,7 +48,7 @@ export const XRAY_TORRENT_BLOCKER_ROUTING_RULES_MODEL = ({ outboundTag: 'RW_TB_OUTBOUND_BLOCK', webhook: { url: webhookUrl, - deduplication: 5, + deduplication: 0, }, }); diff --git a/libs/contract/models/abuse-blocker.report.schema.ts b/libs/contract/models/abuse-blocker.report.schema.ts new file mode 100644 index 0000000..32e2dc9 --- /dev/null +++ b/libs/contract/models/abuse-blocker.report.schema.ts @@ -0,0 +1,89 @@ +import { z } from 'zod'; + +import { XrayWebhookSchema } from './xray-webhook.schema'; + +export const AbuseBlockerRuleNameSchema = z.enum(['horizontal_scan', 'destination_sweep']); +export const AbuseBlockerSeveritySchema = z.enum(['suspicious', 'alert', 'blocked']); +export const AbuseBlockerCoverageModeSchema = z.enum(['full', 'partial']); + +export const AbuseBlockerPolicySchema = z.object({ + excludedPorts: z.array(z.int().min(1).max(65535)), + scoreWindowSeconds: z.int().min(1), + incidentCooldownSeconds: z.int().min(0), + suspiciousScore: z.int().min(1), + alertScore: z.int().min(1), + blockScore: z.int().min(1), + initialBlockSeconds: z.int().min(1), + repeatBlockSeconds: z.int().min(1), + repeatWindowSeconds: z.int().min(1), + evidenceLimit: z.int().min(1), + enhancedEvidenceLimit: z.int().min(1), + maxTrackedUsers: z.int().min(1), + maxKeysPerUser: z.int().min(1), + reportBufferSize: z.int().min(1), + horizontalScan: z.object({ + enabled: z.boolean(), + windowSeconds: z.int().min(1), + uniqueDestinations: z.int().min(2), + ipv4Prefix: z.int().min(0).max(32), + ipv6Prefix: z.int().min(0).max(128), + score: z.int().min(1), + }), + destinationSweep: z.object({ + enabled: z.boolean(), + windowSeconds: z.int().min(1), + uniqueDestinations: z.int().min(2), + score: z.int().min(1), + }), +}); + +export const AbuseBlockerReportSchema = z.object({ + eventId: z.uuid(), + userId: z.string().regex(/^\d+$/), + sourceIp: z.union([z.ipv4(), z.ipv6()]), + destinationIp: z.union([z.ipv4(), z.ipv6()]), + destinationPort: z.int().min(1).max(65535), + detectedAt: z.coerce.date(), + detections: z.array( + z.object({ + rule: AbuseBlockerRuleNameSchema, + key: z.string(), + uniqueDestinations: z.int().min(1), + windowSeconds: z.int().min(1), + score: z.int().min(1), + subnet: z.string().nullable(), + }), + ), + score: z.object({ + before: z.int().min(0), + delta: z.int().min(1), + after: z.int().min(1), + windowSeconds: z.int().min(1), + }), + severity: AbuseBlockerSeveritySchema, + evidence: z.array( + z.object({ + destinationIp: z.union([z.ipv4(), z.ipv6()]), + destinationPort: z.int().min(1).max(65535), + lastSeenAt: z.coerce.date(), + }), + ), + actionReport: z.object({ + action: z.enum(['none', 'ip_block']), + blocked: z.boolean(), + blockDuration: z.int().min(0), + willUnblockAt: z.coerce.date().nullable(), + error: z.string().nullable(), + processedAt: z.coerce.date(), + }), + policy: AbuseBlockerPolicySchema, + configFingerprint: z.string().min(1), + coverageMode: AbuseBlockerCoverageModeSchema, + xrayReport: XrayWebhookSchema, +}); + +export type AbuseBlockerRuleName = z.infer; +export type AbuseBlockerSeverity = z.infer; +export type AbuseBlockerCoverageMode = z.infer; +export type AbuseBlockerPolicy = z.infer; +export type AbuseBlockerReportModel = z.infer; diff --git a/libs/contract/models/index.ts b/libs/contract/models/index.ts index 821d616..293e855 100644 --- a/libs/contract/models/index.ts +++ b/libs/contract/models/index.ts @@ -1,4 +1,5 @@ export * from './node-system.schema'; +export * from './abuse-blocker.report.schema'; export * from './torrent-blocker.report.schema'; export * from './xray-webhook.schema'; export * from './node-metadata.schema'; diff --git a/libs/contract/tsconfig.json b/libs/contract/tsconfig.json index 2023c05..197b0f7 100644 --- a/libs/contract/tsconfig.json +++ b/libs/contract/tsconfig.json @@ -10,16 +10,9 @@ "strictPropertyInitialization": false, "skipLibCheck": true, "noEmit": false, - "lib": [ - "es2020" - ], + "lib": ["es2020"], "declaration": true, - "declarationMap": true, + "declarationMap": true }, - "exclude": [ - "scripts/**/*", - "tests/**/*", - "**/*.spec.ts", - "**/*.test.ts" - ] -} \ No newline at end of file + "exclude": ["build/**/*", "scripts/**/*", "tests/**/*", "**/*.spec.ts", "**/*.test.ts"] +}