feat(node-contract): add abuse blocker contracts

This commit is contained in:
l0nelynx 2026-08-15 10:51:37 +03:00
parent 487e83bebc
commit 7cc16cd83e
No known key found for this signature in database
12 changed files with 165 additions and 13 deletions

View file

@ -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`,

View file

@ -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}`,

View file

@ -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<typeof ResponseSchema>;
}

View file

@ -0,0 +1,2 @@
export * from './collect-reports.schema';
export * from './refresh-block.schema';

View file

@ -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<typeof RequestSchema>;
export const ResponseSchema = z.object({
response: z.object({
accepted: z.boolean(),
}),
});
export type Response = z.infer<typeof ResponseSchema>;
}

View file

@ -1,3 +1,4 @@
export * from './abuse-blocker';
export * from './nftables';
export * from './sync.command';
export * from './torrent-blocker';

View file

@ -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(),
}),

View file

@ -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}`;

View file

@ -48,7 +48,7 @@ export const XRAY_TORRENT_BLOCKER_ROUTING_RULES_MODEL = ({
outboundTag: 'RW_TB_OUTBOUND_BLOCK',
webhook: {
url: webhookUrl,
deduplication: 5,
deduplication: 0,
},
});

View file

@ -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<typeof AbuseBlockerRuleNameSchema>;
export type AbuseBlockerSeverity = z.infer<typeof AbuseBlockerSeveritySchema>;
export type AbuseBlockerCoverageMode = z.infer<typeof AbuseBlockerCoverageModeSchema>;
export type AbuseBlockerPolicy = z.infer<typeof AbuseBlockerPolicySchema>;
export type AbuseBlockerReportModel = z.infer<typeof AbuseBlockerReportSchema>;

View file

@ -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';

View file

@ -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"
]
}
"exclude": ["build/**/*", "scripts/**/*", "tests/**/*", "**/*.spec.ts", "**/*.test.ts"]
}