From c74c7d51a3c1b4db5eeed6c909af257f8b87a7d8 Mon Sep 17 00:00:00 2001 From: oxpoe Date: Fri, 6 Nov 2026 11:36:20 +0700 Subject: [PATCH] feat(security): auto-block brute force MikroTik + hotspot MAC - deteksi log hotspot login failed (IP + MAC) - auto-block opsional ke BLACKLIST_BRUTEFORCE 1 hari + drop rule - MAC hotspot dimasukkan ip-binding type blocked - ambang & toggle di Pengaturan > Keamanan - versi 0.6.17 --- CHANGELOG.md | 9 +++ README.md | 2 +- package.json | 2 +- services/securityWatchdogService.js | 90 ++++++++++++++++++++++++++--- version.txt | 2 +- views/admin/settings.ejs | 14 +++++ 6 files changed, 109 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d804197..798ab90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/). +## [0.6.17] — 2026-11-06 — "security-block" + +### Ditambahkan +- **Auto-block brute force MikroTik** (opsional, default nonaktif): + - percobaan login gagal berulang dimasukkan ke firewall `BLACKLIST_BRUTEFORCE` selama 1 hari, + - rule `drop` untuk chain `input` dan `forward` ditambahkan otomatis, + - hotspot brute force juga memblokir MAC penyerang via Hotspot IP-Binding (`type: blocked`). +- Ambang percobaan dapat diatur di **Pengaturan → Keamanan**. + ## [0.6.16] — 2026-11-06 — "wifi-sync" ### Diperbaiki diff --git a/README.md b/README.md index 72f11ac..7b0c07a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

Lisensi: ISC - Versi 0.6.16 (wifi-sync) + Versi 0.6.17 (security-block) Node.js >= 20 Docker Demo diff --git a/package.json b/package.json index 1f13523..9603b58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-isp", - "version": "0.6.16", + "version": "0.6.17", "description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)", "main": "app-customer.js", "scripts": { diff --git a/services/securityWatchdogService.js b/services/securityWatchdogService.js index d42c432..50c3da3 100644 --- a/services/securityWatchdogService.js +++ b/services/securityWatchdogService.js @@ -180,7 +180,8 @@ class SecurityWatchdogService { processedSet.add(logId); const msg = String(l.message || ''); - if (msg.toLowerCase().includes('login failure') || msg.toLowerCase().includes('failure')) { + const low = msg.toLowerCase(); + if (low.includes('login failure') || low.includes('failure') || low.includes('login failed')) { failures.push(l); } } @@ -200,19 +201,31 @@ class SecurityWatchdogService { const match = msg.match(/login failure for user\s+(\S+)\s+from\s+([0-9a-fA-F.:]+)(?:\s+via\s+(\S+))?/i) || msg.match(/failure.*user\s+(\S+).*from\s+([0-9a-fA-F.:]+)/i); - if (match) { - const targetUser = match[1] || 'unknown'; - const attackerIp = String(match[2] || '').replace(/^::ffff:/, '').trim(); - const service = match[3] || 'router-service'; + // Format hotspot: "10.5.50.2 (26:BD:26:2A:C3:47): login failed: invalid username or password" + const hotspotMatch = msg.match(/(?:->:\s*)?([0-9a-fA-F.:]+)\s*\(([0-9a-fA-F:]+)\):\s*login failed/i); - if (!attackerIp || attackerIp === '127.0.0.1' || attackerIp === 'localhost') continue; + let targetUser, attackerIp, service, mac; + if (hotspotMatch) { + attackerIp = String(hotspotMatch[1] || '').replace(/^::ffff:/, '').trim(); + mac = String(hotspotMatch[2] || '').toUpperCase(); + targetUser = 'voucher-hotspot'; + service = 'hotspot'; + } else if (match) { + targetUser = match[1] || 'unknown'; + attackerIp = String(match[2] || '').replace(/^::ffff:/, '').trim(); + service = match[3] || 'router-service'; + } + + if (attackerIp) { + if (attackerIp === '127.0.0.1' || attackerIp === 'localhost') continue; if (!failuresByIp.has(attackerIp)) { - failuresByIp.set(attackerIp, { count: 1, targets: new Set([targetUser]), service }); + failuresByIp.set(attackerIp, { count: 1, targets: new Set([targetUser]), service, macs: new Set(mac ? [mac] : []) }); } else { const item = failuresByIp.get(attackerIp); item.count++; item.targets.add(targetUser); + if (mac) item.macs.add(mac); } } } @@ -257,6 +270,16 @@ class SecurityWatchdogService { await this._notifyAdmin(waMsg); + // 2b. AUTO-BLOCK (opsional, default nonaktif) + if (String(getSetting('security_auto_block_enabled', 'false')) === 'true' && data.count >= (parseInt(getSetting('security_mikrotik_threshold', 5), 10) || 5)) { + try { + const actions = await this._blockMikrotikIp(conn, attackerIp, r.name, data); + if (actions.length) logger.warn('[SecurityWatchdog] Auto-block ' + attackerIp + ': ' + actions.join('; ')); + } catch (blockErr) { + logger.error('[SecurityWatchdog] Auto-block gagal: ' + blockErr.message); + } + } + // 3. CATAT KE AUDIT TRAIL this._recordAudit({ action: 'ROUTER_BRUTE_FORCE', @@ -284,6 +307,59 @@ class SecurityWatchdogService { } } + /** + * Blokir IP penyerang di MikroTik (firewall address-list + drop rule). + * Hanya dipanggil bila security_auto_block_enabled=true. + */ + async _blockMikrotikIp(conn, ip, routerName, data) { + const actions = []; + const safeIp = String(ip || '').trim(); + if (!safeIp || safeIp === '127.0.0.1' || safeIp === 'localhost') return actions; + // Hotspot brute force: blokir juga MAC di IP-Binding + if (data.macs && data.macs.size) { + const bindMenu = conn.client.menu('/ip/hotspot/ip-binding'); + const bindings = await bindMenu.get(); + for (const mac of data.macs) { + const found = bindings.find(b => String(b['mac-address'] || b.macAddress || '').toUpperCase() === mac); + if (!found) { + await bindMenu.add({ macAddress: mac, type: 'blocked', comment: 'AI_BLOCKED: hotspot brute force (' + data.count + 'x)' }).catch(function () {}); + actions.push('MAC ' + mac + ' diblokir di Hotspot IP-Binding'); + } + } + } + + const listMenu = conn.client.menu('/ip/firewall/address-list'); + const rows = await listMenu.get(); + const exists = rows.find(a => a.address === safeIp && (a.list === 'BLACKLIST_BRUTEFORCE' || a.list === 'BLACKLIST_ANOMALY')); + if (!exists) { + await listMenu.add({ + list: 'BLACKLIST_BRUTEFORCE', + address: safeIp, + timeout: '1d', + comment: 'AI_BLOCKED: ' + String(data.service || 'router').toUpperCase() + ' (' + data.count + 'x gagal)' + }); + actions.push('IP ' + safeIp + ' masuk BLACKLIST_BRUTEFORCE (1 hari)'); + } + + // Pastikan rule drop pada chain input & forward + const filterMenu = conn.client.menu('/ip/firewall/filter'); + const filters = await filterMenu.get(); + const hasDrop = (chain) => filters.some(f => f.chain === chain && f.action === 'drop' && + (f['src-address-list'] === 'BLACKLIST_BRUTEFORCE' || f.srcAddressList === 'BLACKLIST_BRUTEFORCE')); + for (const chain of ['input', 'forward']) { + if (!hasDrop(chain)) { + await filterMenu.add({ + chain, + action: 'drop', + srcAddressList: 'BLACKLIST_BRUTEFORCE', + comment: 'AI_AUTO_DROP_BLACKLIST_BRUTEFORCE' + }).catch(function () {}); + actions.push('rule drop ' + chain + ' ditambahkan'); + } + } + return actions; + } + /** * Ringkasan status keamanan saat ini */ diff --git a/version.txt b/version.txt index a12cadd..ec92ab1 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.6.16 \ No newline at end of file +0.6.17 \ No newline at end of file diff --git a/views/admin/settings.ejs b/views/admin/settings.ejs index b8c2397..3e05944 100644 --- a/views/admin/settings.ejs +++ b/views/admin/settings.ejs @@ -564,6 +564,20 @@ +

+
+ + +
Jika aktif, IP dengan percobaan login gagal berulang dimasukkan ke firewall blacklist 1 hari.
+
+
+ + +
+