From 20bdf72f818ac01ebb127d454a130b89f1f41a9c Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Mon, 16 Feb 2026 11:05:13 +0000 Subject: [PATCH] Integrate Mikrotik router management for IP blocking and unblocking Introduces `server/mikrotik.ts` to manage router connections and IP blocking/unblocking via API calls, replacing direct calls to an external ML backend. Updates `server/routes.ts` to utilize these new functions for whitelisting and unblocking IPs. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 23a20497-0848-4aec-aef2-4a9483164195 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/B8f0CIv --- server/mikrotik.ts | 317 +++++++++++++++++++++++++++++++++++++++++++++ server/routes.ts | 161 +++++++++++++---------- 2 files changed, 411 insertions(+), 67 deletions(-) create mode 100644 server/mikrotik.ts diff --git a/server/mikrotik.ts b/server/mikrotik.ts new file mode 100644 index 0000000..a132014 --- /dev/null +++ b/server/mikrotik.ts @@ -0,0 +1,317 @@ +interface RouterConfig { + id: string; + ipAddress: string; + apiPort: number; + username: string; + password: string; + enabled: boolean; +} + +interface BlockResult { + routerIp: string; + routerName?: string; + success: boolean; + alreadyExists?: boolean; + error?: string; +} + +async function mikrotikRequest( + router: RouterConfig, + method: string, + path: string, + body?: any, + timeoutMs: number = 10000 +): Promise<{ status: number; data: any }> { + const useHttps = router.apiPort === 443; + const protocol = useHttps ? "https" : "http"; + const url = `${protocol}://${router.ipAddress}:${router.apiPort}${path}`; + const auth = Buffer.from(`${router.username}:${router.password}`).toString("base64"); + + const origTlsReject = process.env.NODE_TLS_REJECT_UNAUTHORIZED; + if (useHttps) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + + try { + const fetchOptions: RequestInit = { + method, + headers: { + "Authorization": `Basic ${auth}`, + "Content-Type": "application/json", + }, + signal: controller.signal, + }; + + if (body) { + fetchOptions.body = JSON.stringify(body); + } + + const response = await fetch(url, fetchOptions); + clearTimeout(timeout); + + let data: any; + const text = await response.text(); + try { + data = JSON.parse(text); + } catch { + data = text; + } + + return { status: response.status, data }; + } catch (error: any) { + clearTimeout(timeout); + if (useHttps && origTlsReject !== undefined) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = origTlsReject; + } else if (useHttps) { + delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; + } + throw error; + } finally { + if (useHttps) { + if (origTlsReject !== undefined) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = origTlsReject; + } else { + delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; + } + } + } +} + +export async function testRouterConnection(router: RouterConfig): Promise { + try { + const { status } = await mikrotikRequest(router, "GET", "/rest/system/identity"); + return status === 200; + } catch { + return false; + } +} + +export async function getExistingBlockedIps( + router: RouterConfig, + listName: string = "ddos_blocked" +): Promise> { + try { + const { status, data } = await mikrotikRequest(router, "GET", "/rest/ip/firewall/address-list", undefined, 20000); + if (status === 200 && Array.isArray(data)) { + const ips = new Set(); + for (const entry of data) { + if (entry.list === listName) { + ips.add(entry.address); + } + } + return ips; + } + return new Set(); + } catch (e: any) { + console.error(`[MIKROTIK] Failed to get address-list from ${router.ipAddress}: ${e.message}`); + return new Set(); + } +} + +export async function addToAddressList( + router: RouterConfig, + ipAddress: string, + listName: string = "ddos_blocked", + comment: string = "", + timeoutDuration: string = "1h" +): Promise { + try { + const { status, data } = await mikrotikRequest(router, "POST", "/rest/ip/firewall/address-list/add", { + list: listName, + address: ipAddress, + comment: comment || `IDS block ${new Date().toISOString()}`, + timeout: timeoutDuration, + }); + + if (status === 200 || status === 201) { + return { routerIp: router.ipAddress, success: true }; + } + + if (status === 400 || status === 409) { + const text = typeof data === "string" ? data.toLowerCase() : JSON.stringify(data).toLowerCase(); + if (text.includes("already") || text.includes("exists") || text.includes("duplicate") || text.includes("failure: already")) { + return { routerIp: router.ipAddress, success: true, alreadyExists: true }; + } + try { + const verifyResult = await mikrotikRequest(router, "GET", "/rest/ip/firewall/address-list"); + if (verifyResult.status === 200 && Array.isArray(verifyResult.data)) { + for (const entry of verifyResult.data) { + if (entry.address === ipAddress && entry.list === listName) { + return { routerIp: router.ipAddress, success: true, alreadyExists: true }; + } + } + } + } catch {} + return { + routerIp: router.ipAddress, + success: false, + error: `HTTP ${status}: ${typeof data === "string" ? data : JSON.stringify(data)}`, + }; + } + + return { + routerIp: router.ipAddress, + success: false, + error: `HTTP ${status}: ${typeof data === "string" ? data : JSON.stringify(data)}`, + }; + } catch (error: any) { + return { + routerIp: router.ipAddress, + success: false, + error: error.message || "Connection failed", + }; + } +} + +export async function removeFromAddressList( + router: RouterConfig, + ipAddress: string, + listName: string = "ddos_blocked" +): Promise { + try { + const { status, data } = await mikrotikRequest(router, "GET", "/rest/ip/firewall/address-list"); + if (status !== 200 || !Array.isArray(data)) { + return { routerIp: router.ipAddress, success: false, error: "Failed to read address list" }; + } + + for (const entry of data) { + if (entry.address === ipAddress && entry.list === listName) { + const entryId = entry[".id"]; + const delResult = await mikrotikRequest(router, "DELETE", `/rest/ip/firewall/address-list/${entryId}`); + if (delResult.status === 200 || delResult.status === 204) { + return { routerIp: router.ipAddress, success: true }; + } + return { routerIp: router.ipAddress, success: false, error: `Delete failed: ${delResult.status}` }; + } + } + + return { routerIp: router.ipAddress, success: true }; + } catch (error: any) { + return { routerIp: router.ipAddress, success: false, error: error.message }; + } +} + +export async function blockIpOnAllRouters( + routers: RouterConfig[], + ipAddress: string, + listName: string = "ddos_blocked", + comment: string = "", + timeoutDuration: string = "1h" +): Promise { + const enabled = routers.filter((r) => r.enabled); + const results = await Promise.allSettled( + enabled.map((r) => addToAddressList(r, ipAddress, listName, comment, timeoutDuration)) + ); + + return results.map((r, i) => + r.status === "fulfilled" ? r.value : { routerIp: enabled[i].ipAddress, success: false, error: String(r.reason) } + ); +} + +export async function unblockIpOnAllRouters( + routers: RouterConfig[], + ipAddress: string, + listName: string = "ddos_blocked" +): Promise { + const enabled = routers.filter((r) => r.enabled); + const results = await Promise.allSettled( + enabled.map((r) => removeFromAddressList(r, ipAddress, listName)) + ); + + return results.map((r, i) => + r.status === "fulfilled" ? r.value : { routerIp: enabled[i].ipAddress, success: false, error: String(r.reason) } + ); +} + +export async function bulkBlockIps( + routers: RouterConfig[], + ipList: string[], + listName: string = "ddos_blocked", + commentPrefix: string = "IDS bulk-block", + timeoutDuration: string = "1h", + concurrency: number = 10 +): Promise<{ blocked: number; failed: number; skipped: number; details: Array<{ ip: string; status: string }> }> { + const enabled = routers.filter((r) => r.enabled); + if (enabled.length === 0) { + return { blocked: 0, failed: 0, skipped: 0, details: [] }; + } + + console.log(`[BULK-BLOCK] Starting: ${ipList.length} IPs on ${enabled.length} routers`); + + const existingCache = new Map>(); + await Promise.allSettled( + enabled.map(async (router) => { + const existing = await getExistingBlockedIps(router, listName); + existingCache.set(router.ipAddress, existing); + console.log(`[BULK-BLOCK] Router ${router.ipAddress}: ${existing.size} IPs already in list`); + }) + ); + + const newIps: string[] = []; + const skippedIps: string[] = []; + + for (const ip of ipList) { + let alreadyOnAll = true; + for (const router of enabled) { + const existing = existingCache.get(router.ipAddress) || new Set(); + if (!existing.has(ip)) { + alreadyOnAll = false; + break; + } + } + if (alreadyOnAll) { + skippedIps.push(ip); + } else { + newIps.push(ip); + } + } + + console.log(`[BULK-BLOCK] ${skippedIps.length} already blocked, ${newIps.length} new to block`); + + let blocked = 0; + let failed = 0; + const details: Array<{ ip: string; status: string }> = []; + + async function processIp(ip: string) { + const routerResults = await Promise.allSettled( + enabled.map(async (router) => { + const existing = existingCache.get(router.ipAddress) || new Set(); + if (existing.has(ip)) return true; + const result = await addToAddressList(router, ip, listName, `${commentPrefix} ${ip}`, timeoutDuration); + return result.success; + }) + ); + + const anySuccess = routerResults.some( + (r) => r.status === "fulfilled" && r.value === true + ); + + if (anySuccess) { + blocked++; + details.push({ ip, status: "blocked" }); + } else { + failed++; + details.push({ ip, status: "failed" }); + } + } + + for (let i = 0; i < newIps.length; i += concurrency) { + const batch = newIps.slice(i, i + concurrency); + await Promise.allSettled(batch.map((ip) => processIp(ip))); + + if ((i + concurrency) % 50 === 0 || i + concurrency >= newIps.length) { + console.log(`[BULK-BLOCK] Progress: ${Math.min(i + concurrency, newIps.length)}/${newIps.length}`); + } + } + + for (const ip of skippedIps) { + details.push({ ip, status: "already_blocked" }); + } + + console.log(`[BULK-BLOCK] Done: ${blocked} blocked, ${failed} failed, ${skippedIps.length} skipped`); + + return { blocked, failed, skipped: skippedIps.length, details }; +} diff --git a/server/routes.ts b/server/routes.ts index 9e885e8..4517d3a 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -4,6 +4,7 @@ import { storage } from "./storage"; import { insertRouterSchema, insertDetectionSchema, insertWhitelistSchema, insertPublicListSchema, networkAnalytics, routers, detections, networkLogs } from "@shared/schema"; import { db } from "./db"; import { desc, eq, gte, sql } from "drizzle-orm"; +import { blockIpOnAllRouters, unblockIpOnAllRouters, bulkBlockIps, testRouterConnection } from "./mikrotik"; export async function registerRoutes(app: Express): Promise { // Routers @@ -139,28 +140,16 @@ export async function registerRoutes(app: Express): Promise { const validatedData = insertWhitelistSchema.parse(req.body); const item = await storage.createWhitelist(validatedData); - // Auto-unblock from routers when adding to whitelist - const mlBackendUrl = process.env.ML_BACKEND_URL || 'http://localhost:8000'; - const mlApiKey = process.env.IDS_API_KEY; try { - const headers: Record = { 'Content-Type': 'application/json' }; - if (mlApiKey) { - headers['X-API-Key'] = mlApiKey; - } - const unblockResponse = await fetch(`${mlBackendUrl}/unblock-ip`, { - method: 'POST', - headers, - body: JSON.stringify({ ip_address: validatedData.ipAddress }) - }); - if (unblockResponse.ok) { - const result = await unblockResponse.json(); - console.log(`[WHITELIST] Auto-unblocked ${validatedData.ipAddress} from ${result.unblocked_from} routers`); - } else { - console.warn(`[WHITELIST] Failed to auto-unblock ${validatedData.ipAddress}: ${unblockResponse.status}`); + const allRouters = await storage.getAllRouters(); + const enabledRouters = allRouters.filter(r => r.enabled); + if (enabledRouters.length > 0) { + const results = await unblockIpOnAllRouters(enabledRouters as any, validatedData.ipAddress); + const unblocked = results.filter(r => r.success).length; + console.log(`[WHITELIST] Auto-unblocked ${validatedData.ipAddress} from ${unblocked}/${enabledRouters.length} routers`); } } catch (unblockError) { - // Don't fail if ML backend is unavailable - console.warn(`[WHITELIST] ML backend unavailable for auto-unblock: ${unblockError}`); + console.warn(`[WHITELIST] Auto-unblock failed for ${validatedData.ipAddress}:`, unblockError); } res.json(item); @@ -169,7 +158,6 @@ export async function registerRoutes(app: Express): Promise { } }); - // Unblock IP from all routers (proxy to ML backend) app.post("/api/unblock-ip", async (req, res) => { try { const { ipAddress, listName = "ddos_blocked" } = req.body; @@ -178,31 +166,31 @@ export async function registerRoutes(app: Express): Promise { return res.status(400).json({ error: "IP address is required" }); } - const mlBackendUrl = process.env.ML_BACKEND_URL || 'http://localhost:8000'; - const mlApiKey = process.env.IDS_API_KEY; - const headers: Record = { 'Content-Type': 'application/json' }; - if (mlApiKey) { - headers['X-API-Key'] = mlApiKey; + const allRouters = await storage.getAllRouters(); + const enabledRouters = allRouters.filter(r => r.enabled); + + if (enabledRouters.length === 0) { + return res.status(400).json({ error: "Nessun router abilitato" }); } - const response = await fetch(`${mlBackendUrl}/unblock-ip`, { - method: 'POST', - headers, - body: JSON.stringify({ ip_address: ipAddress, list_name: listName }) + const results = await unblockIpOnAllRouters(enabledRouters as any, ipAddress, listName); + const successCount = results.filter(r => r.success).length; + + await db.update(detections) + .set({ blocked: false }) + .where(eq(detections.sourceIp, ipAddress)); + + console.log(`[UNBLOCK] ${ipAddress} rimosso da ${successCount}/${enabledRouters.length} router`); + + res.json({ + message: `IP ${ipAddress} sbloccato da ${successCount} router`, + unblocked_from: successCount, + total_routers: enabledRouters.length, + results: results.map(r => ({ router: r.routerIp, success: r.success, error: r.error })) }); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`[UNBLOCK] ML backend error for ${ipAddress}: ${response.status} - ${errorText}`); - return res.status(response.status).json({ error: errorText || "Failed to unblock IP" }); - } - - const result = await response.json(); - console.log(`[UNBLOCK] Successfully unblocked ${ipAddress} from ${result.unblocked_from || 0} routers`); - res.json(result); } catch (error: any) { console.error('[UNBLOCK] Error:', error); - res.status(500).json({ error: error.message || "Failed to unblock IP from routers" }); + res.status(500).json({ error: error.message || "Errore sblocco IP" }); } }); @@ -639,36 +627,75 @@ export async function registerRoutes(app: Express): Promise { app.post("/api/ml/block-all-critical", async (req, res) => { try { - const { min_score = 80 } = req.body; - - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 300000); // 5 min timeout - - const response = await fetch(`${ML_BACKEND_URL}/block-all-critical`, { - method: "POST", - headers: getMLBackendHeaders(), - body: JSON.stringify({ min_score }), - signal: controller.signal, - }); - - clearTimeout(timeout); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - return res.status(response.status).json({ - error: errorData.detail || "Block all critical failed", + const { min_score = 80, list_name = "ddos_blocked" } = req.body; + + const allRouters = await storage.getAllRouters(); + const enabledRouters = allRouters.filter(r => r.enabled); + + if (enabledRouters.length === 0) { + return res.status(400).json({ error: "Nessun router abilitato" }); + } + + const unblockedDetections = await db.execute( + sql`SELECT DISTINCT source_ip, MAX(CAST(risk_score AS FLOAT)) as max_score, MAX(anomaly_type) as anomaly_type + FROM detections + WHERE CAST(risk_score AS FLOAT) >= ${min_score} + AND blocked = false + AND source_ip NOT IN (SELECT ip_address FROM whitelist WHERE active = true) + GROUP BY source_ip + ORDER BY max_score DESC` + ); + + const rows = (unblockedDetections as any).rows || unblockedDetections; + + if (!rows || rows.length === 0) { + return res.json({ + message: "Nessun IP critico da bloccare", + blocked: 0, + failed: 0, + total_critical: 0, + skipped: 0 }); } - - const data = await response.json(); - res.json(data); + + const ipList = rows.map((r: any) => r.source_ip); + console.log(`[BLOCK-ALL] Avvio blocco massivo: ${ipList.length} IP con score >= ${min_score} su ${enabledRouters.length} router`); + + const result = await bulkBlockIps( + enabledRouters as any, + ipList, + list_name, + `IDS bulk-block (score>=${min_score})`, + "1h", + 10 + ); + + if (result.blocked > 0) { + const blockedIps = result.details + .filter(d => d.status === "blocked") + .map(d => d.ip); + + const batchSize = 200; + for (let i = 0; i < blockedIps.length; i += batchSize) { + const batch = blockedIps.slice(i, i + batchSize); + const ipValues = batch.map(ip => `'${ip.replace(/'/g, "''")}'`).join(','); + await db.execute( + sql`UPDATE detections SET blocked = true, blocked_at = NOW() WHERE source_ip IN (${sql.raw(ipValues)}) AND blocked = false` + ); + } + console.log(`[BLOCK-ALL] Database aggiornato: ${blockedIps.length} IP marcati come bloccati`); + } + + res.json({ + message: `Blocco massivo completato: ${result.blocked} IP bloccati, ${result.failed} falliti, ${result.skipped} giĆ  bloccati`, + blocked: result.blocked, + failed: result.failed, + skipped: result.skipped, + total_critical: ipList.length, + details: result.details.slice(0, 100) + }); } catch (error: any) { - if (error.name === 'AbortError') { - return res.status(504).json({ error: "Timeout - operazione troppo lunga" }); - } - if (error.code === 'ECONNREFUSED') { - return res.status(503).json({ error: "ML backend non disponibile" }); - } + console.error('[BLOCK-ALL] Error:', error); res.status(500).json({ error: error.message || "Errore blocco massivo" }); } });