ids.alfacom.it/server/mikrotik.ts
marco370 b7abd340bc Improve logging for Mikrotik requests and IP blocking operations
Enhance logging in `mikrotik.ts` to include request details, response statuses, and timings. Add verbose logging for successful operations and warnings for errors or slow responses. Update `getExistingBlockedIps` to log total entries and specific list counts per router. Modify `addToAddressList` to log successful additions and specific error conditions. Update `bulkBlockIps` to log detailed operation outcomes, including partial and failed IPs, with a final summary. Add router information to the `BLOCK-ALL` log in `routes.ts`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 3945267e-74c4-4c36-912a-462ddd667392
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/6WuDAR4
2026-02-17 08:07:45 +00:00

414 lines
16 KiB
TypeScript

const VERBOSE = process.env.MIKROTIK_DEBUG === '1' || process.env.MIKROTIK_DEBUG === 'true';
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 startTime = Date.now();
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;
}
const elapsed = Date.now() - startTime;
if (VERBOSE) {
const bodyStr = body ? ` body=${JSON.stringify(body)}` : '';
const dataPreview = typeof data === 'string' ? data.substring(0, 200) : JSON.stringify(data).substring(0, 200);
console.log(`[MIKROTIK] ${method} ${url} => HTTP ${response.status} (${elapsed}ms)${bodyStr} response=${dataPreview}`);
} else if (response.status >= 400) {
const dataPreview = typeof data === 'string' ? data.substring(0, 100) : JSON.stringify(data).substring(0, 100);
console.warn(`[MIKROTIK] ${method} ${router.ipAddress}${path} => HTTP ${response.status} (${elapsed}ms) err=${dataPreview}`);
} else if (elapsed > 5000) {
console.warn(`[MIKROTIK] SLOW: ${method} ${router.ipAddress}${path} => HTTP ${response.status} (${elapsed}ms)`);
}
return { status: response.status, data };
} catch (error: any) {
clearTimeout(timeout);
const elapsed = Date.now() - startTime;
const errMsg = error.name === 'AbortError' ? `TIMEOUT after ${timeoutMs}ms` : error.message;
console.error(`[MIKROTIK] ${method} ${url} => ERRORE: ${errMsg} (${elapsed}ms)`);
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<boolean> {
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<Set<string>> {
try {
if (VERBOSE) console.log(`[MIKROTIK] Fetching address-list da router ${router.ipAddress} (list=${listName}, timeout=20s)...`);
const { status, data } = await mikrotikRequest(router, "GET", "/rest/ip/firewall/address-list", undefined, 20000);
if (status === 200 && Array.isArray(data)) {
const ips = new Set<string>();
const allLists = new Map<string, number>();
for (const entry of data) {
const count = allLists.get(entry.list) || 0;
allLists.set(entry.list, count + 1);
if (entry.list === listName) {
ips.add(entry.address);
}
}
const listsInfo = Array.from(allLists.entries()).map(([name, count]) => `${name}:${count}`).join(', ');
console.log(`[MIKROTIK] Router ${router.ipAddress}: ${data.length} entries totali (${listsInfo}), ${ips.size} in list "${listName}"`);
return ips;
}
console.warn(`[MIKROTIK] Router ${router.ipAddress}: risposta inattesa status=${status}, data non e' array`);
return new Set();
} catch (e: any) {
console.error(`[MIKROTIK] Router ${router.ipAddress}: ERRORE fetch address-list: ${e.message}`);
return new Set();
}
}
export async function addToAddressList(
router: RouterConfig,
ipAddress: string,
listName: string = "ddos_blocked",
comment: string = "",
timeoutDuration: string = "1h"
): Promise<BlockResult> {
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) {
if (VERBOSE) console.log(`[BLOCK] OK: ${ipAddress} aggiunto su router ${router.ipAddress} (HTTP ${status})`);
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")) {
if (VERBOSE) console.log(`[BLOCK] SKIP: ${ipAddress} gia' presente su router ${router.ipAddress} (HTTP ${status})`);
return { routerIp: router.ipAddress, success: true, alreadyExists: true };
}
console.warn(`[BLOCK] VERIFICA: ${ipAddress} su router ${router.ipAddress} HTTP ${status} risposta="${text.substring(0, 150)}", verifico lista...`);
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) {
console.log(`[BLOCK] CONFERMATO: ${ipAddress} trovato nella lista di router ${router.ipAddress} dopo verifica`);
return { routerIp: router.ipAddress, success: true, alreadyExists: true };
}
}
}
} catch (verifyErr: any) {
console.error(`[BLOCK] ERRORE verifica: ${ipAddress} su router ${router.ipAddress}: ${verifyErr.message}`);
}
const errMsg = `HTTP ${status}: ${typeof data === "string" ? data : JSON.stringify(data)}`;
console.error(`[BLOCK] FALLITO: ${ipAddress} su router ${router.ipAddress}: ${errMsg}`);
return {
routerIp: router.ipAddress,
success: false,
error: errMsg,
};
}
const errMsg = `HTTP ${status}: ${typeof data === "string" ? data : JSON.stringify(data)}`;
console.error(`[BLOCK] FALLITO: ${ipAddress} su router ${router.ipAddress}: ${errMsg}`);
return {
routerIp: router.ipAddress,
success: false,
error: errMsg,
};
} catch (error: any) {
const errMsg = error.name === 'AbortError' ? `TIMEOUT (8s)` : (error.message || "Connection failed");
console.error(`[BLOCK] ERRORE: ${ipAddress} su router ${router.ipAddress}: ${errMsg}`);
return {
routerIp: router.ipAddress,
success: false,
error: errMsg,
};
}
}
export async function removeFromAddressList(
router: RouterConfig,
ipAddress: string,
listName: string = "ddos_blocked"
): Promise<BlockResult> {
try {
if (VERBOSE) console.log(`[UNBLOCK] Rimozione ${ipAddress} da router ${router.ipAddress} (list=${listName})...`);
const { status, data } = await mikrotikRequest(router, "GET", "/rest/ip/firewall/address-list");
if (status !== 200 || !Array.isArray(data)) {
console.error(`[UNBLOCK] ERRORE: impossibile leggere address-list da router ${router.ipAddress}: HTTP ${status}`);
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) {
console.log(`[UNBLOCK] OK: ${ipAddress} rimosso da router ${router.ipAddress}`);
return { routerIp: router.ipAddress, success: true };
}
console.error(`[UNBLOCK] FALLITO: eliminazione ${ipAddress} da router ${router.ipAddress}: HTTP ${delResult.status}`);
return { routerIp: router.ipAddress, success: false, error: `Delete failed: ${delResult.status}` };
}
}
if (VERBOSE) console.log(`[UNBLOCK] ${ipAddress} non trovato su router ${router.ipAddress} (gia' assente)`);
return { routerIp: router.ipAddress, success: true };
} catch (error: any) {
console.error(`[UNBLOCK] ERRORE: ${ipAddress} su router ${router.ipAddress}: ${error.message}`);
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<BlockResult[]> {
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<BlockResult[]> {
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<string, { ok: number; fail: number; skip: number }>();
for (const r of enabled) {
routerStatus.set(r.ipAddress, { ok: 0, fail: 0, skip: 0 });
}
const existingCache = new Map<string, Set<string>>();
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 }> = [];
const partialIps: string[] = [];
const failedIps: 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 { success: true, skipped: true, routerIp: router.ipAddress };
}
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++;
}
return { success: result.success, skipped: false, routerIp: router.ipAddress, elapsed, error: result.error };
})
);
const perRouterDetail = routerResults.map((r) => {
if (r.status === 'fulfilled') {
const v = r.value;
if (v.skipped) return `${v.routerIp}:SKIP`;
if (v.success) return `${v.routerIp}:OK(${v.elapsed}ms)`;
return `${v.routerIp}:FAIL(${v.elapsed}ms,${v.error})`;
}
return 'REJECTED';
}).join(' | ');
const anySuccess = routerResults.some(
(r) => r.status === "fulfilled" && r.value.success
);
const allSuccess = routerResults.every(
(r) => r.status === "fulfilled" && r.value.success
);
if (anySuccess) {
blocked++;
details.push({ ip, status: "blocked" });
if (!allSuccess) {
partialIps.push(ip);
if (VERBOSE) console.warn(`[BULK-BLOCK] PARZIALE: IP ${ip}: ${perRouterDetail}`);
}
} else {
failed++;
failedIps.push(ip);
details.push({ ip, status: "failed" });
if (VERBOSE) console.error(`[BULK-BLOCK] FALLITO: IP ${ip}: ${perRouterDetail}`);
}
}
const bulkStart = Date.now();
for (let i = 0; i < newIps.length; i += concurrency) {
const batch = newIps.slice(i, i + concurrency);
await Promise.allSettled(batch.map((ip) => processIp(ip)));
const progress = Math.min(i + concurrency, newIps.length);
if (progress === newIps.length || progress % 50 === 0) {
const elapsed = ((Date.now() - bulkStart) / 1000).toFixed(1);
console.log(`[BULK-BLOCK] Progress: ${progress}/${newIps.length} (${elapsed}s, ${blocked} ok, ${failed} fail)`);
}
}
for (const ip of skippedIps) {
details.push({ ip, status: "already_blocked" });
}
const totalElapsed = ((Date.now() - bulkStart) / 1000).toFixed(1);
routerStatus.forEach((st, routerIp) => {
console.log(`[BULK-BLOCK] Router ${routerIp}: ${st.ok} blocked, ${st.fail} failed, ${st.skip} skipped`);
});
console.log(`[BULK-BLOCK] Completato in ${totalElapsed}s: ${blocked} blocked, ${failed} failed, ${skippedIps.length} already_blocked, ${partialIps.length} parziali`);
if (failedIps.length > 0) {
console.error(`[BULK-BLOCK] IP non bloccati su nessun router (${failedIps.length}): ${failedIps.slice(0, 20).join(', ')}${failedIps.length > 20 ? '...' : ''}`);
}
if (partialIps.length > 0) {
console.warn(`[BULK-BLOCK] IP bloccati solo parzialmente (${partialIps.length}): ${partialIps.slice(0, 20).join(', ')}${partialIps.length > 20 ? '...' : ''}`);
}
return { blocked, failed, skipped: skippedIps.length, details };
}