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 = 8000 ): 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 (${enabled.map(r => r.ipAddress).join(', ')})`); const routerStatus = new Map(); for (const r of enabled) { routerStatus.set(r.ipAddress, { ok: 0, fail: 0, skip: 0 }); } const existingCache = new Map>(); await Promise.allSettled( enabled.map(async (router) => { const start = Date.now(); const existing = await getExistingBlockedIps(router, listName); const elapsed = Date.now() - start; existingCache.set(router.ipAddress, existing); console.log(`[BULK-BLOCK] Router ${router.ipAddress}: ${existing.size} IPs already in list (${elapsed}ms)`); }) ); 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)) { const st = routerStatus.get(router.ipAddress); if (st) st.skip++; return true; } const start = Date.now(); const result = await addToAddressList(router, ip, listName, `${commentPrefix} ${ip}`, timeoutDuration); const elapsed = Date.now() - start; const st = routerStatus.get(router.ipAddress); if (result.success) { if (st) st.ok++; } else { if (st) st.fail++; if (elapsed > 5000) { console.warn(`[BULK-BLOCK] SLOW: Router ${router.ipAddress} took ${elapsed}ms for IP ${ip}: ${result.error}`); } } 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" }); } // Report per-router routerStatus.forEach((st, routerIp) => { console.log(`[BULK-BLOCK] Router ${routerIp}: ${st.ok} blocked, ${st.fail} failed, ${st.skip} skipped`); }); console.log(`[BULK-BLOCK] Done: ${blocked} blocked, ${failed} failed, ${skippedIps.length} skipped`); return { blocked, failed, skipped: skippedIps.length, details }; }