Increase auto-block timeout to 300s, update systemd service timeout to 480s, and reduce individual MikroTik request timeout to 8s. Add per-router logging for blocking operations. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 455f4d8c-e90c-45d5-a7f1-e5f98b1345d3 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/cJuycQ5
344 lines
11 KiB
TypeScript
344 lines
11 KiB
TypeScript
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<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 {
|
|
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>();
|
|
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<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) {
|
|
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<BlockResult> {
|
|
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<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 }> = [];
|
|
|
|
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 };
|
|
}
|