#!/usr/bin/env python3 import os import sqlite3 import logging from functools import wraps from flask import ( Flask, render_template, jsonify, request, session, redirect, url_for, flash ) from flask_cors import CORS from werkzeug.security import check_password_hash import routeros_api # ── Logging ───────────────────────────────────────────────────────────────────── logging.basicConfig( filename='monitoring.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger("mikrotik-monitor") # ── Flask App ─────────────────────────────────────────────────────────────────── app = Flask(__name__, template_folder='templates') app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'mikrotik-monitor-secret-key-2024') # Jika dashboard & API beda origin, aktifkan CORS. Jika sama, ini tak masalah dibiarkan. CORS(app, supports_credentials=True) # ── Auth Config (ENV) ─────────────────────────────────────────────────────────── MONITOR_USER = os.getenv('MONITOR_USER', 'admin') MONITOR_PASS_HASH = os.getenv('MONITOR_PASS_HASH', 'MASUKAN PASSWORD KAMU DISINI') def login_required(f): @wraps(f) def wrapper(*args, **kwargs): if not session.get('user'): return redirect(url_for('login', next=request.path)) return f(*args, **kwargs) return wrapper # ── MikroTik Config ───────────────────────────────────────────────────────────── MIKROTIK_CONFIG = { 'host': os.getenv('MIKROTIK_HOST', 'MASUKAN IP MIKROTIK KAMU'), 'username': os.getenv('MIKROTIK_USER', 'MASUKAN USER MIKROTIK KAMU'), 'password': os.getenv('MIKROTIK_PASS', 'MASUKAN PASSWORD MIKROTIK KAMU'), 'port': int(os.getenv('MIKROTIK_PORT', MASUKAN PORT MIKROTIK KAMU)), } DB_FILE = 'network_monitor.db' # ── SQLite Utils ──────────────────────────────────────────────────────────────── def init_db(): conn = sqlite3.connect(DB_FILE, check_same_thread=False) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS blocked_devices (id INTEGER PRIMARY KEY AUTOINCREMENT, address TEXT UNIQUE, device_name TEXT, blocked_at DATETIME DEFAULT CURRENT_TIMESTAMP, reason TEXT, blocked_by TEXT DEFAULT 'manual')''') conn.commit() conn.close() logger.info("Database initialized") def get_blocked_ips_from_db(): conn = sqlite3.connect(DB_FILE) c = conn.cursor() c.execute('SELECT address FROM blocked_devices') ips = {row[0] for row in c.fetchall() if row and row[0]} conn.close() return ips def purge_blocked_db(): conn = sqlite3.connect(DB_FILE) c = conn.cursor() c.execute('SELECT COUNT(*) FROM blocked_devices') count = c.fetchone()[0] or 0 c.execute('DELETE FROM blocked_devices') conn.commit() conn.close() logger.info("Purged blocked_devices table: %d rows", count) return count # ── MikroTik Client ───────────────────────────────────────────────────────────── class MikrotikMonitor: def __init__(self): self.api = None self.connected = False self.layer7_enabled = False self.connect() def __del__(self): # Tutup connection pool kalau ada (hindari leak) try: if getattr(self, 'connection', None): self.connection.disconnect() except Exception: pass def connect(self): try: self.connection = routeros_api.RouterOsApiPool( MIKROTIK_CONFIG['host'], username=MIKROTIK_CONFIG['username'], password=MIKROTIK_CONFIG['password'], port=MIKROTIK_CONFIG['port'], plaintext_login=True, # timeout=5 ) self.api = self.connection.get_api() self.connected = True self.check_layer7_config() logger.info("✅ Connected to MikroTik %s", MIKROTIK_CONFIG['host']) except Exception as e: logger.error("❌ Failed to connect to MikroTik: %s", e) self.connected = False def check_layer7_config(self): try: l7 = self.api.get_resource('/ip/firewall/layer7-protocol').get() self.layer7_enabled = len(l7) > 0 if self.layer7_enabled: logger.info("✅ Layer7 enabled with %d rules", len(l7)) else: logger.warning("⚠️ Layer7 not configured") except Exception as e: logger.error("Error checking Layer7: %s", e) self.layer7_enabled = False def get_dhcp_leases(self): if not self.connected: return [] try: return self.api.get_resource('/ip/dhcp-server/lease').get() except Exception as e: logger.error("Error getting DHCP leases: %s", e) return [] def get_detected_apps_for_ip(self, ip): detected = [] app_map = [ ('PUBG Mobile', 'PUBG_USERS'), ('Mobile Legends', 'ML_USERS'), ('Free Fire', 'FF_USERS'), ('TikTok', 'TIKTOK_USERS'), ] try: fw = self.api.get_resource('/ip/firewall/address-list') for app, addr_list in app_map: users = fw.get(list=addr_list, address=ip) if users and any(u.get('address') == ip for u in users): detected.append(app) logger.info("Detected apps for %s: %s", ip, detected) except Exception as e: logger.error("Error checking detected apps for %s: %s", ip, e) return detected def block_ip(self, ip): try: fw = self.api.get_resource('/ip/firewall/filter') existing = fw.get(chain='forward', src_address=ip, action='drop') if not existing: fw.add(chain='forward', src_address=ip, action='drop', comment='Blocked by dashboard') logger.info("Blocked IP %s (rule created)", ip) else: logger.info("Blocked IP %s (rule already exists)", ip) conn = sqlite3.connect(DB_FILE) c = conn.cursor() c.execute('INSERT OR IGNORE INTO blocked_devices (address, device_name) VALUES (?, ?)', (ip, "")) conn.commit() conn.close() return True except Exception as e: logger.error("Error blocking IP %s: %s", ip, e) return False def unblock_ip(self, ip): try: fw = self.api.get_resource('/ip/firewall/filter') rules = fw.get() removed_any = False for rule in rules: rule_id = rule.get('.id') or rule.get('id') or rule.get('uuid') or rule.get('_id') if rule.get('src-address', '') == ip and rule.get('action') == 'drop' and rule_id: try: fw.remove(id=rule_id) logger.info("Unblocked %s, removed rule %s", ip, rule_id) removed_any = True except Exception as e: logger.error("Error removing rule %s for %s: %s", rule_id, ip, e) conn = sqlite3.connect(DB_FILE) c = conn.cursor() c.execute('DELETE FROM blocked_devices WHERE address=?', (ip,)) conn.commit() conn.close() if not removed_any: logger.info("No firewall rule found for %s; DB entry removed if existed", ip) return True except Exception as e: logger.error("Error unblocking %s: %s", ip, e) return False def reset_all_filter_counters(self): if not self.connected: logger.error("Not connected to Mikrotik") return False try: fw = self.api.get_resource('/ip/firewall/filter') rules = fw.get() for rule in rules: rule_id = rule.get('.id') or rule.get('id') if rule_id: fw.call('reset-counters', {'numbers': rule_id}) logger.info("Reset counters for all firewall filter rules") return True except Exception as e: logger.error("Error resetting counters: %s", e) return False def clear_all_address_lists(self): if not self.connected: logger.error("Not connected to Mikrotik") return False try: addr = self.api.get_resource('/ip/firewall/address-list') entries = addr.get() for entry in entries: entry_id = entry.get('.id') or entry.get('id') if entry_id: addr.remove(id=entry_id) logger.info("Cleared ALL address-list entries") return True except Exception as e: logger.error("Error clearing address-lists: %s", e) return False # ── NEW: Mass-unblock rules buatan dashboard ──────────────────────────────── def remove_dashboard_drop_rules(self, ips=None, comment_marker='Blocked by dashboard'): """ Hapus semua firewall filter rule action=drop yang dibuat dashboard. Seleksi berdasarkan: - comment mengandung 'Blocked by dashboard', ATAU - src-address ada di set 'ips' (berdasarkan DB) Return: jumlah rule yang dihapus. """ if not self.connected: logger.error("Not connected to Mikrotik") return 0 try: fw = self.api.get_resource('/ip/firewall/filter') rules = fw.get() removed = 0 marker = (comment_marker or '').lower() ips = set(ips or []) for r in rules: if r.get('action') != 'drop': continue rid = r.get('.id') or r.get('id') src = (r.get('src-address') or '').strip() comment = (r.get('comment') or '').lower() if (marker and marker in comment) or (ips and src in ips): if rid: try: fw.remove(id=rid) removed += 1 except Exception as e: logger.error("Error removing rule %s: %s", rid, e) logger.info("Mass-unblock removed %d rules", removed) return removed except Exception as e: logger.error("Error mass-unblock rules: %s", e) return 0 # Per-request instance (mengurangi risiko timeout/connection stale) def get_monitor(): return MikrotikMonitor() # ── Auth Routes ───────────────────────────────────────────────────────────────── @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = (request.form.get('username') or '').strip() password = request.form.get('password') or '' if username == MONITOR_USER and MONITOR_PASS_HASH and check_password_hash(MONITOR_PASS_HASH, password): session['user'] = username nxt = request.args.get('next') or url_for('dashboard') return redirect(nxt) flash('Username atau password salah', 'error') return render_template('login.html') @app.route('/logout') def logout(): session.clear() return redirect(url_for('login')) @app.route('/api/router/reboot', methods=['POST']) @login_required def api_router_reboot(): monitor = get_monitor() if not monitor.connected: return jsonify({'success': False, 'error': 'Not connected to router'}), 500 try: system_resource = monitor.api.get_resource('/system/reboot') system_resource.call('reboot') logger.info("⚠️ Router restart command issued via dashboard") return jsonify({'success': True}) except Exception as e: logger.error(f"Error rebooting router: {e}") return jsonify({'success': False, 'error': str(e)}), 500 # ── API (Protected) ───────────────────────────────────────────────────────────── @app.route('/api/devices') @login_required def api_devices(): monitor = get_monitor() try: leases = monitor.get_dhcp_leases() except Exception as e: logger.error("Gagal ambil DHCP Lease: %s", e) return jsonify([]) conn = sqlite3.connect(DB_FILE) c = conn.cursor() c.execute('SELECT address FROM blocked_devices') blocked_list = {row[0].lower() for row in c.fetchall()} conn.close() devices = [] for lease in leases: mac = lease.get('mac-address', '').lower() ip = lease.get('address', '') detected_apps = monitor.get_detected_apps_for_ip(ip) games_list = ['PUBG Mobile', 'Mobile Legends', 'Free Fire'] games_active = [a for a in detected_apps if a in games_list] devices.append({ "hostname": lease.get('host-name', lease.get('comment', 'Unknown')), "ip": ip, "mac": mac, "detected_apps": detected_apps, "games_active": games_active, "is_blocked": ip and (ip.lower() in blocked_list), "active": lease.get('status', 'unknown').lower() == 'bound' }) return jsonify(devices) @app.route('/api/devices//block', methods=['POST']) @login_required def api_block_device(ip): monitor = get_monitor() return (jsonify({'success': True}) if monitor.block_ip(ip) else (jsonify({'success': False}), 500)) @app.route('/api/devices//unblock', methods=['POST']) @login_required def api_unblock_device(ip): monitor = get_monitor() return (jsonify({'success': True}) if monitor.unblock_ip(ip) else (jsonify({'success': False}), 500)) @app.route('/api/devices/unblock-all', methods=['POST']) @login_required def api_unblock_all(): """UNBLOCK massal: hapus rule 'drop' buatan dashboard + kosongkan DB.""" try: m = get_monitor() db_ips = get_blocked_ips_from_db() removed_rules = m.remove_dashboard_drop_rules(ips=db_ips, comment_marker='Blocked by dashboard') deleted_db = purge_blocked_db() return jsonify({'success': True, 'removed_rules': removed_rules, 'deleted_db': deleted_db}) except Exception as e: logger.error("Error unblock-all: %s", e) return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/firewall/reset-counters', methods=['POST']) @login_required def api_reset_filter_counters(): monitor = get_monitor() return (jsonify({'success': True}) if monitor.reset_all_filter_counters() else (jsonify({'success': False}), 500)) @app.route('/api/firewall/clear-address-lists', methods=['POST']) @login_required def api_clear_all_address_lists(): monitor = get_monitor() return (jsonify({'success': True}) if monitor.clear_all_address_lists() else (jsonify({'success': False}), 500)) @app.route('/api/firewall/stats') @login_required def api_firewall_stats(): monitor = get_monitor() # Filter filter_resource = monitor.api.get_resource('/ip/firewall/filter') filter_rules = filter_resource.get() total_rules = len(filter_rules) to_int = lambda v: int(v) if str(v).isdigit() else 0 total_packets = sum(to_int(r.get('packets', 0)) for r in filter_rules) total_bytes = sum(to_int(r.get('bytes', 0)) for r in filter_rules) # Address-list addr_resource = monitor.api.get_resource('/ip/firewall/address-list') addr_entries = addr_resource.get() total_addrlist = len(addr_entries) addrlist_per_name = {} for listname in ['PUBG_USERS', 'ML_USERS', 'FF_USERS', 'TIKTOK_USERS']: addrlist_per_name[listname] = sum(1 for e in addr_entries if e.get('list') == listname) return jsonify({ "total_rules": total_rules, "total_packets": total_packets, "total_bytes": total_bytes, "total_addrlist": total_addrlist, "addrlist_per_name": addrlist_per_name }) # ── UI (Protected) ────────────────────────────────────────────────────────────── @app.route('/') @login_required def dashboard(): return render_template('dashboard.html', version=os.getenv('APP_VERSION', '1.0.0')) # ── Main ──────────────────────────────────────────────────────────────────────── if __name__ == '__main__': print(""" ╔══════════════════════════════════════════════════════════╗ ║ MIKROTIK NETWORK MONITORING SYSTEM ║ ║ Created by SULTHANULLAH HAQQI HIDAYAT ║ ╚══════════════════════════════════════════════════════════╝ """) if not MONITOR_PASS_HASH: logger.warning("MONITOR_PASS_HASH belum di-set! Login akan gagal.") print("⚠️ Set MONITOR_USER dan MONITOR_PASS_HASH sebelum production.") print(" Generate hash:") print(" python - <<'PY'\nfrom werkzeug.security import generate_password_hash\nprint(generate_password_hash('password_ku'))\nPY\n") init_db() print("\n🌐 Starting web server on http://0.0.0.0:5000\n") app.run(debug=False, host='0.0.0.0', port=5000)