import os import sys import time import json import uuid import requests import logging from datetime import datetime # Configuration CONFIG_FILE = '/opt/dnsblock/config.json' import logging from logging.handlers import RotatingFileHandler # Configuration CONFIG_FILE = '/opt/dnsblock/config.json' LOG_DIR = '/opt/dnsblock/logs' LOG_FILE = os.path.join(LOG_DIR, 'agent.log') RPZ_FILE_DEFAULT = '/opt/dnsblock/rpz.dnsblock.zone' # Ensure Log Directory Exists if not os.path.exists(LOG_DIR): try: os.makedirs(LOG_DIR) except Exception as e: print(f"Failed to create log directory: {e}") sys.exit(1) # Setup Logging with Rotation handler = RotatingFileHandler(LOG_FILE, maxBytes=5*1024*1024, backupCount=5) formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger = logging.getLogger() logger.setLevel(logging.INFO) logger.addHandler(handler) # Also log to stdout for systemd journal console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(formatter) logger.addHandler(console_handler) def get_machine_id(): """Retrieves a unique machine ID.""" try: # Try reading /etc/machine-id (Linux standard) if os.path.exists('/etc/machine-id'): with open('/etc/machine-id', 'r') as f: return f.read().strip() # Fallback to hardware UUID return str(uuid.getnode()) except Exception as e: logging.error(f"Error getting machine ID: {e}") sys.exit(1) def load_config(): """Loads configuration from JSON file.""" if not os.path.exists(CONFIG_FILE): logging.error(f"Config file not found at {CONFIG_FILE}") sys.exit(1) try: with open(CONFIG_FILE, 'r') as f: return json.load(f) except Exception as e: logging.error(f"Error loading config: {e}") sys.exit(1) def generate_rpz(domains, output_file): """Generates the RPZ zone file.""" try: # Ensure directory exists os.makedirs(os.path.dirname(output_file), exist_ok=True) with open(output_file, 'w') as f: f.write("$TTL 30\n") f.write("@ IN SOA rpz.dnsblock.zone. root.rpz.dnsblock.zone. (\n") f.write(f" {int(time.time())} ; Serial\n") f.write(" 30 ; Refresh\n") f.write(" 15 ; Retry\n") f.write(" 604800 ; Expire\n") f.write(" 30 ) ; Negative Cache TTL\n") f.write(";\n") f.write("@ IN NS localhost.\n") f.write("@ IN A 127.0.0.1\n") f.write(";\n") f.write("; Blocked Domains\n") for domain in domains: # CNAME to . (NXDOMAIN) or specific sinkhole f.write(f"{domain} CNAME .\n") f.write(f"*.{domain} CNAME .\n") logging.info(f"RPZ file generated at {output_file} with {len(domains)} domains.") return True except Exception as e: logging.error(f"Error generating RPZ: {e}") return False def main(): try: logging.info("DNSBlock Agent Starting...") config = load_config() serial_key = config.get('serial_key') api_url = config.get('api_url') rpz_file = config.get('rpz_file', RPZ_FILE_DEFAULT) reload_command = config.get('reload_command') if not serial_key or not api_url: logging.error("Missing serial_key or api_url in config.") sys.exit(1) machine_id = get_machine_id() logging.info(f"Machine ID: {machine_id}") last_checksum = None while True: try: headers = { 'Authorization': f'Bearer {serial_key}', 'X-Machine-ID': machine_id } response = requests.get(f"{api_url}/api/v1/domains", headers=headers, timeout=10) if response.status_code == 200: try: data = response.json() current_checksum = data.get('checksum') if current_checksum != last_checksum: logging.info("Change detected in domain list. Updating RPZ...") domains = data.get('domains', []) if generate_rpz(domains, rpz_file): last_checksum = current_checksum if reload_command: logging.info(f"Executing reload command: {reload_command}") exit_code = os.system(reload_command) if exit_code == 0: logging.info("Service reloaded successfully.") else: logging.error(f"Reload command failed with exit code {exit_code}") else: logging.debug("No changes in domain list.") except json.JSONDecodeError as e: logging.error(f"Failed to decode JSON response. Content preview: {response.text[:200]}") logging.debug(f"Full response: {response.text}") elif response.status_code == 401: logging.error("Unauthorized: Invalid Serial Key.") elif response.status_code == 403: logging.error("Forbidden: Serial Key bound to another machine.") else: logging.warning(f"API Error: {response.status_code} - {response.text}") except Exception as e: logging.error(f"Connection Error: {e}") # Wait for 60 seconds before next check time.sleep(60) except Exception as e: logging.critical(f"Fatal Error: {e}", exc_info=True) sys.exit(1) if __name__ == "__main__": main()