491 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			491 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/env python3
 | |
| """
 | |
| LogMon - Modularny demon do monitorowania logów i blokowania IP
 | |
| Autor: Mateusz Gruszczyński
 | |
| Wersja: 1.0
 | |
| """
 | |
| 
 | |
| import sys
 | |
| import os
 | |
| import time
 | |
| import signal
 | |
| import logging
 | |
| import argparse
 | |
| import configparser
 | |
| from collections import defaultdict, deque
 | |
| from datetime import datetime, timedelta
 | |
| from pathlib import Path
 | |
| 
 | |
| 
 | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
 | |
| 
 | |
| # Importy z lokalnych modułów
 | |
| from modules import PostfixModule, DovecotModule
 | |
| from backends import CSFBackend, NFTablesBackend, IPTablesBackend, UFWBackend
 | |
| 
 | |
| class LogMonDaemon:
 | |
|     """Główny demon LogMon"""
 | |
|     
 | |
|     def __init__(self, config_file):
 | |
|         """
 | |
|         Inicjalizacja demona
 | |
|         
 | |
|         Args:
 | |
|             config_file: Ścieżka do pliku konfiguracyjnego INI
 | |
|         """
 | |
|         self.config = configparser.ConfigParser()
 | |
|         self.config.read(config_file)
 | |
|         
 | |
|         self.running = False
 | |
|         self.ip_tracker = defaultdict(lambda: deque())
 | |
|         self.banned_ips = {}
 | |
|         
 | |
|         # Konfiguracja logowania
 | |
|         self.setup_logging()
 | |
|         
 | |
|         # Wybór backendu firewall
 | |
|         self.backend = self.load_backend()
 | |
|         
 | |
|         # Ładowanie modułów monitorowania
 | |
|         self.modules = self.load_modules()
 | |
|         
 | |
|         # Obsługa sygnałów systemowych
 | |
|         signal.signal(signal.SIGTERM, self.signal_handler)
 | |
|         signal.signal(signal.SIGINT, self.signal_handler)
 | |
|         signal.signal(signal.SIGHUP, self.signal_reload)
 | |
|         
 | |
|         self.logger.info("LogMon daemon initialized")
 | |
|         
 | |
|     def setup_logging(self):
 | |
|         """Konfiguracja systemu logowania"""
 | |
|         debug = self.config.getboolean('general', 'debug', fallback=False)
 | |
|         log_file = self.config.get('general', 'log_file', 
 | |
|                                    fallback='/var/log/logmon.log')
 | |
|         
 | |
|         level = logging.DEBUG if debug else logging.INFO
 | |
|         
 | |
|         # Usuń istniejące handlery
 | |
|         logging.basicConfig(
 | |
|             level=level,
 | |
|             format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
 | |
|             handlers=[
 | |
|                 logging.FileHandler(log_file),
 | |
|                 logging.StreamHandler(sys.stdout)
 | |
|             ]
 | |
|         )
 | |
|         
 | |
|         self.logger = logging.getLogger('LogMon')
 | |
|         self.logger.info(f"Logging initialized (debug={debug})")
 | |
|         
 | |
|     def load_backend(self):
 | |
|         """Ładuje odpowiedni backend firewall"""
 | |
|         backend_name = self.config.get('general', 'backend', fallback='csf')
 | |
|         
 | |
|         self.logger.info(f"Loading backend: {backend_name}")
 | |
|         
 | |
|         backend_map = {
 | |
|             'csf': CSFBackend,
 | |
|             'nftables': NFTablesBackend,
 | |
|             'iptables': IPTablesBackend,
 | |
|             'ufw': UFWBackend
 | |
|         }
 | |
|         
 | |
|         if backend_name not in backend_map:
 | |
|             raise ValueError(f"Unknown backend: {backend_name}")
 | |
|             
 | |
|         backend_class = backend_map[backend_name]
 | |
|         backend = backend_class(self.config)
 | |
|         
 | |
|         # Sprawdź dostępność
 | |
|         if not backend.test_availability():
 | |
|             self.logger.warning(f"Backend {backend_name} may not be available!")
 | |
|             
 | |
|         return backend
 | |
|         
 | |
|     def load_modules(self):
 | |
|         """Ładuje moduły monitorowania"""
 | |
|         modules = []
 | |
|         
 | |
|         # Postfix module
 | |
|         if self.config.getboolean('module_postfix', 'enabled', fallback=False):
 | |
|             try:
 | |
|                 module = PostfixModule(self.config, self)
 | |
|                 modules.append(module)
 | |
|                 self.logger.info("Loaded Postfix module")
 | |
|             except Exception as e:
 | |
|                 self.logger.error(f"Error loading Postfix module: {e}")
 | |
|         # Dovecot module
 | |
|         if self.config.getboolean('module_dovecot', 'enabled', fallback=False):
 | |
|             try:
 | |
|                 module = DovecotModule(self.config, self)
 | |
|                 modules.append(module)
 | |
|                 self.logger.info("Loaded Dovecot module")
 | |
|             except Exception as e:
 | |
|                 self.logger.error(f"Error loading Dovecot module: {e}")        
 | |
|         
 | |
|         # Tutaj można dodać więcej modułów w przyszłości
 | |
|         # if self.config.getboolean('module_ssh', 'enabled', fallback=False):
 | |
|         #     modules.append(SSHModule(self.config, self))
 | |
|         
 | |
|         if not modules:
 | |
|             self.logger.warning("No modules loaded!")
 | |
|             
 | |
|         return modules
 | |
|         
 | |
|     def signal_handler(self, signum, frame):
 | |
|         """Obsługa sygnałów SIGTERM i SIGINT"""
 | |
|         self.logger.info(f"Received signal {signum}, shutting down gracefully...")
 | |
|         self.running = False
 | |
|         
 | |
|     def signal_reload(self, signum, frame):
 | |
|         """Obsługa sygnału SIGHUP - reload konfiguracji"""
 | |
|         self.logger.info("Received SIGHUP, reloading configuration...")
 | |
|         # TODO: Implementacja reload konfiguracji
 | |
|         
 | |
|     def track_failure(self, ip, score=1):
 | |
|         """
 | |
|         Śledzi nieudane próby logowania dla danego IP
 | |
|         
 | |
|         Args:
 | |
|             ip: Adres IP atakującego
 | |
|             score: Punkty za dane niepowodzenie (domyślnie 1)
 | |
|         """
 | |
|         if ip in self.banned_ips:
 | |
|             self.logger.debug(f"IP {ip} already banned, ignoring")
 | |
|             return
 | |
|         
 | |
|         now = datetime.now()
 | |
|         
 | |
|         # Dodaj wpis do trackera
 | |
|         self.ip_tracker[ip].append((now, score))
 | |
|         
 | |
|         # Usuń stare wpisy spoza okna czasowego
 | |
|         time_window = self.config.getint('module_postfix', 'time_window', 
 | |
|                                         fallback=60)
 | |
|         cutoff = now - timedelta(seconds=time_window)
 | |
|         
 | |
|         while self.ip_tracker[ip] and self.ip_tracker[ip][0][0] < cutoff:
 | |
|             self.ip_tracker[ip].popleft()
 | |
|             
 | |
|         # Oblicz całkowity wynik
 | |
|         total_score = sum(score for _, score in self.ip_tracker[ip])
 | |
|         failures_count = len(self.ip_tracker[ip])
 | |
|         
 | |
|         max_failures = self.config.getint('module_postfix', 'max_failures', 
 | |
|                                         fallback=5)
 | |
|         
 | |
|         self.logger.debug(
 | |
|             f"IP {ip}: {failures_count} failures, score {total_score}/{max_failures}"
 | |
|         )
 | |
|         
 | |
|         # Sprawdź czy przekroczono limit
 | |
|         if total_score >= max_failures:
 | |
|             self.ban_ip(ip)
 | |
| 
 | |
|             
 | |
|     def ban_ip(self, ip):
 | |
|         """
 | |
|         Banuje adres IP
 | |
|         
 | |
|         Args:
 | |
|             ip: Adres IP do zbanowania
 | |
|         """
 | |
|         ban_duration = self.config.getint('module_postfix', 'ban_duration', 
 | |
|                                          fallback=86400)
 | |
|         
 | |
|         self.logger.warning(
 | |
|             f"Banning IP {ip} for {ban_duration} seconds "
 | |
|             f"({ban_duration/3600:.1f} hours)"
 | |
|         )
 | |
|         
 | |
|         if self.backend.ban_ip(ip, ban_duration):
 | |
|             expiry_time = datetime.now() + timedelta(seconds=ban_duration)
 | |
|             self.banned_ips[ip] = expiry_time
 | |
|             
 | |
|             self.logger.info(
 | |
|                 f"Successfully banned {ip} until {expiry_time.strftime('%Y-%m-%d %H:%M:%S')}"
 | |
|             )
 | |
|             
 | |
|             # Wyczyść tracker dla tego IP
 | |
|             if ip in self.ip_tracker:
 | |
|                 del self.ip_tracker[ip]
 | |
|         else:
 | |
|             self.logger.error(f"Failed to ban {ip}")
 | |
|             
 | |
|     def unban_ip(self, ip):
 | |
|         """
 | |
|         Odbania adres IP
 | |
|         
 | |
|         Args:
 | |
|             ip: Adres IP do odbanowania
 | |
|         """
 | |
|         self.logger.info(f"Unbanning expired IP {ip}")
 | |
|         
 | |
|         if self.backend.unban_ip(ip):
 | |
|             if ip in self.banned_ips:
 | |
|                 del self.banned_ips[ip]
 | |
|             self.logger.info(f"Successfully unbanned {ip}")
 | |
|         else:
 | |
|             self.logger.error(f"Failed to unban {ip}")
 | |
|             
 | |
|     def unban_expired(self):
 | |
|         """Usuwa bany, które wygasły"""
 | |
|         now = datetime.now()
 | |
|         expired = [ip for ip, expiry in self.banned_ips.items() 
 | |
|                    if now >= expiry]
 | |
|         
 | |
|         for ip in expired:
 | |
|             self.unban_ip(ip)
 | |
|             
 | |
|     def save_state(self):
 | |
|         """Zapisuje stan banów do pliku (opcjonalnie)"""
 | |
|         # TODO: Implementacja persystencji stanu
 | |
|         pass
 | |
|         
 | |
|     def load_state(self):
 | |
|         """Ładuje stan banów z pliku (opcjonalnie)"""
 | |
|         # TODO: Implementacja persystencji stanu
 | |
|         pass
 | |
|         
 | |
|     def daemonize(self):
 | |
|         """Przechodzi w tryb demona (fork)"""
 | |
|         pid_file = self.config.get('general', 'pid_file', 
 | |
|                                    fallback='/var/run/logmon.pid')
 | |
|         
 | |
|         # Sprawdź czy już działa
 | |
|         if os.path.exists(pid_file):
 | |
|             with open(pid_file, 'r') as f:
 | |
|                 old_pid = int(f.read().strip())
 | |
|                 
 | |
|             # Sprawdź czy proces istnieje
 | |
|             try:
 | |
|                 os.kill(old_pid, 0)
 | |
|                 self.logger.error(f"Daemon already running with PID {old_pid}")
 | |
|                 sys.exit(1)
 | |
|             except OSError:
 | |
|                 # Proces nie istnieje, usuń stary PID file
 | |
|                 os.remove(pid_file)
 | |
|         
 | |
|         # Pierwszy fork
 | |
|         try:
 | |
|             pid = os.fork()
 | |
|             if pid > 0:
 | |
|                 # Parent process
 | |
|                 sys.exit(0)
 | |
|         except OSError as e:
 | |
|             self.logger.error(f"Fork #1 failed: {e}")
 | |
|             sys.exit(1)
 | |
|             
 | |
|         # Odłącz od terminala
 | |
|         os.chdir('/')
 | |
|         os.setsid()
 | |
|         os.umask(0)
 | |
|         
 | |
|         # Drugi fork
 | |
|         try:
 | |
|             pid = os.fork()
 | |
|             if pid > 0:
 | |
|                 # Parent process
 | |
|                 sys.exit(0)
 | |
|         except OSError as e:
 | |
|             self.logger.error(f"Fork #2 failed: {e}")
 | |
|             sys.exit(1)
 | |
|             
 | |
|         # Zapisz PID
 | |
|         pid = os.getpid()
 | |
|         with open(pid_file, 'w') as f:
 | |
|             f.write(str(pid))
 | |
|             
 | |
|         self.logger.info(f"Daemon started with PID {pid}")
 | |
|         
 | |
|         # Przekieruj standardowe wyjścia
 | |
|         sys.stdout.flush()
 | |
|         sys.stderr.flush()
 | |
|         
 | |
|         devnull = open(os.devnull, 'r+')
 | |
|         os.dup2(devnull.fileno(), sys.stdin.fileno())
 | |
|         
 | |
|         # Stdout i stderr zostawiamy dla logowania
 | |
|         
 | |
|     def cleanup(self):
 | |
|         """Sprzątanie przed zakończeniem"""
 | |
|         pid_file = self.config.get('general', 'pid_file', 
 | |
|                                    fallback='/var/run/logmon.pid')
 | |
|         
 | |
|         # Usuń PID file
 | |
|         if os.path.exists(pid_file):
 | |
|             try:
 | |
|                 os.remove(pid_file)
 | |
|             except Exception as e:
 | |
|                 self.logger.error(f"Error removing PID file: {e}")
 | |
|                 
 | |
|     def print_status(self):
 | |
|         """Wyświetla status demona"""
 | |
|         print("\n=== LogMon Status ===")
 | |
|         print(f"Backend: {self.backend.__class__.__name__}")
 | |
|         print(f"Modules: {len(self.modules)}")
 | |
|         print(f"Currently banned IPs: {len(self.banned_ips)}")
 | |
|         print(f"Tracked IPs: {len(self.ip_tracker)}")
 | |
|         
 | |
|         if self.banned_ips:
 | |
|             print("\nBanned IPs:")
 | |
|             for ip, expiry in sorted(self.banned_ips.items(), 
 | |
|                                     key=lambda x: x[1]):
 | |
|                 remaining = (expiry - datetime.now()).total_seconds()
 | |
|                 if remaining > 0:
 | |
|                     print(f"  {ip:15s} - expires in {remaining/60:.1f} minutes")
 | |
|                 else:
 | |
|                     print(f"  {ip:15s} - EXPIRED")
 | |
|                     
 | |
|         print("\n")
 | |
|         
 | |
|     def run(self, daemonize=True):
 | |
|         """
 | |
|         Główna pętla demona
 | |
|         
 | |
|         Args:
 | |
|             daemonize: Czy przejść w tryb demona (fork)
 | |
|         """
 | |
|         if daemonize:
 | |
|             self.daemonize()
 | |
|             
 | |
|         self.running = True
 | |
|         self.logger.info("LogMon daemon started")
 | |
|         
 | |
|         # Uruchom moduły
 | |
|         for module in self.modules:
 | |
|             try:
 | |
|                 module.start()
 | |
|             except Exception as e:
 | |
|                 self.logger.error(f"Error starting module: {e}")
 | |
|                 
 | |
|         # Główna pętla
 | |
|         try:
 | |
|             while self.running:
 | |
|                 try:
 | |
|                     # Sprawdź wygasłe bany co 10 sekund
 | |
|                     self.unban_expired()
 | |
|                     
 | |
|                     # Wyświetl status co minutę (tylko w debug mode)
 | |
|                     if self.config.getboolean('general', 'debug', fallback=False):
 | |
|                         if int(time.time()) % 60 == 0:
 | |
|                             self.logger.debug(
 | |
|                                 f"Status: {len(self.banned_ips)} banned, "
 | |
|                                 f"{len(self.ip_tracker)} tracked"
 | |
|                             )
 | |
|                     
 | |
|                     # Krótkie oczekiwanie
 | |
|                     time.sleep(10)
 | |
|                     
 | |
|                 except KeyboardInterrupt:
 | |
|                     self.logger.info("Keyboard interrupt received")
 | |
|                     break
 | |
|                 except Exception as e:
 | |
|                     self.logger.error(f"Error in main loop: {e}", exc_info=True)
 | |
|                     time.sleep(1)
 | |
|                     
 | |
|         finally:
 | |
|             # Zatrzymaj moduły
 | |
|             self.logger.info("Stopping modules...")
 | |
|             for module in self.modules:
 | |
|                 try:
 | |
|                     module.stop()
 | |
|                 except Exception as e:
 | |
|                     self.logger.error(f"Error stopping module: {e}")
 | |
|                     
 | |
|             # Sprzątanie
 | |
|             self.cleanup()
 | |
|             
 | |
|             self.logger.info("LogMon daemon stopped")
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     """Główna funkcja programu"""
 | |
|     parser = argparse.ArgumentParser(
 | |
|         description='LogMon - Log Monitoring and IP Blocking Daemon',
 | |
|         formatter_class=argparse.RawDescriptionHelpFormatter,
 | |
|         epilog="""
 | |
| Przykłady użycia:
 | |
|   %(prog)s -c /etc/logmon/config.ini          # Uruchom jako demon
 | |
|   %(prog)s -c /etc/logmon/config.ini -f       # Uruchom w foreground
 | |
|   %(prog)s -c /etc/logmon/config.ini -f -d    # Uruchom w foreground z debugiem
 | |
|   
 | |
| Sygnały:
 | |
|   SIGTERM, SIGINT - graceful shutdown
 | |
|   SIGHUP          - reload konfiguracji (TODO)
 | |
|         """
 | |
|     )
 | |
|     
 | |
|     parser.add_argument(
 | |
|         '-c', '--config',
 | |
|         default='/etc/logmon/config.ini',
 | |
|         help='Ścieżka do pliku konfiguracyjnego (domyślnie: /etc/logmon/config.ini)'
 | |
|     )
 | |
|     
 | |
|     parser.add_argument(
 | |
|         '-f', '--foreground',
 | |
|         action='store_true',
 | |
|         help='Uruchom w foreground (nie demonizuj)'
 | |
|     )
 | |
|     
 | |
|     parser.add_argument(
 | |
|         '-d', '--debug',
 | |
|         action='store_true',
 | |
|         help='Włącz tryb debug (nadpisuje ustawienie z config.ini)'
 | |
|     )
 | |
|     
 | |
|     parser.add_argument(
 | |
|         '-t', '--test',
 | |
|         action='store_true',
 | |
|         help='Test konfiguracji i wyjście'
 | |
|     )
 | |
|     
 | |
|     parser.add_argument(
 | |
|         '--version',
 | |
|         action='version',
 | |
|         version='LogMon 1.0'
 | |
|     )
 | |
|     
 | |
|     args = parser.parse_args()
 | |
|     
 | |
|     # Sprawdź czy plik konfiguracyjny istnieje
 | |
|     if not os.path.exists(args.config):
 | |
|         print(f"Error: Configuration file not found: {args.config}", file=sys.stderr)
 | |
|         sys.exit(1)
 | |
|         
 | |
|     # Sprawdź uprawnienia root
 | |
|     if os.geteuid() != 0 and not args.test:
 | |
|         print("Error: This program must be run as root", file=sys.stderr)
 | |
|         sys.exit(1)
 | |
|         
 | |
|     try:
 | |
|         # Inicjalizacja demona
 | |
|         daemon = LogMonDaemon(args.config)
 | |
|         
 | |
|         # Nadpisz debug jeśli podano w argumentach
 | |
|         if args.debug:
 | |
|             daemon.config.set('general', 'debug', 'true')
 | |
|             daemon.setup_logging()
 | |
|             
 | |
|         # Tryb testowy
 | |
|         if args.test:
 | |
|             print("Configuration test successful")
 | |
|             daemon.print_status()
 | |
|             sys.exit(0)
 | |
|             
 | |
|         # Uruchom demona
 | |
|         daemon.run(daemonize=not args.foreground)
 | |
|         
 | |
|     except KeyboardInterrupt:
 | |
|         print("\nInterrupted by user")
 | |
|         sys.exit(0)
 | |
|     except Exception as e:
 | |
|         print(f"Fatal error: {e}", file=sys.stderr)
 | |
|         import traceback
 | |
|         traceback.print_exc()
 | |
|         sys.exit(1)
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     main()
 | 
