#!/usr/bin/env python3 """ Domain-based Routing Script for BIRD BGP Этот скрипт читает список доменов из YAML-файла, разрешает их в IPv4-адреса и генерирует конфигурацию для BIRD BGP с network statements в формате /24 reject. Автор: GigaCode """ import argparse import logging import socket import sys from pathlib import Path from typing import List, Optional import yaml # Настройка логирования logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) def load_config(config_path: str) -> dict: """ Загрузка конфигурационного файла YAML. Args: config_path: Путь к конфигурационному файлу Returns: dict: Словарь с параметрами конфигурации """ try: with open(config_path, 'r') as f: config = yaml.safe_load(f) logger.info(f"Конфигурация загружена из {config_path}") return config except FileNotFoundError: logger.error(f"Конфигурационный файл не найден: {config_path}") raise except yaml.YAMLError as e: logger.error(f"Ошибка парсинга YAML конфигурации: {e}") raise def load_domains(domains_file: str) -> List[str]: """ Загрузка списка доменов из YAML-файла. Args: domains_file: Путь к файлу со списком доменов Returns: List[str]: Список доменов """ try: with open(domains_file, 'r') as f: domains = yaml.safe_load(f) if domains is None: domains = [] # Фильтрация комментариев (если они есть в списке) domains = [d for d in domains if isinstance(d, str) and d.strip()] logger.info(f"Загружено {len(domains)} доменов из {domains_file}") return domains except FileNotFoundError: logger.error(f"Файл со списком доменов не найден: {domains_file}") raise except yaml.YAMLError as e: logger.error(f"Ошибка парсинга YAML файла доменов: {e}") raise def resolve_domain(domain: str, dns_server: Optional[str] = None) -> List[str]: """ Разрешение домена в IPv4-адреса. Args: domain: Имя домена dns_server: Не используется в текущей реализации (используется системный DNS) Returns: List[str]: Список IPv4-адресов для домена """ try: # Используем getaddrinfo для разрешения домена # Параметр socket.AF_INET ограничивает результат IPv4-адресами records = socket.getaddrinfo(domain, None, socket.AF_INET) ip_addresses = list(set([record[4][0] for record in records])) if ip_addresses: logger.debug(f"Домен {domain} разрешен в IP: {ip_addresses}") return ip_addresses else: logger.warning(f"Не удалось разрешить домен {domain}") return [] except socket.gaierror as e: logger.warning(f"Ошибка разрешения домена {domain}: {e}") return [] except Exception as e: logger.error(f"Неожиданная ошибка при разрешении {domain}: {e}") return [] def resolve_domains(domains: List[str], dns_server: Optional[str] = None) -> dict: """ Разрешение всех доменов в IPv4-адреса. Args: domains: Список доменов dns_server: Не используется в текущей реализации Returns: dict: Словарь {домен: [ip_адреса]} """ results = {} for domain in domains: ip_addresses = resolve_domain(domain, dns_server) results[domain] = ip_addresses return results def generate_bird_config( resolved_domains: dict, output_file: str, neighbor_ip: str = "192.168.1.1", neighbor_as: int = 65000, protocol_name: str = "domain_routing" ) -> str: """ Генерация конфигурационного файла для BIRD BGP. Args: resolved_domains: Словарь {домен: [ip_адреса]} output_file: Путь к выходному файлу neighbor_ip: IP-адрес BGP-Neighbor neighbor_as: AS номер соседа protocol_name: Имя BGP-протокола Returns: str: Сгенерированная конфигурация """ lines = [ "# Generated by domain-routing-bird", f"# Generated at: {__import__('datetime').datetime.now().isoformat()}", "", f"protocol bgp {protocol_name} {{", " import all;", " export all;", f" neighbor {neighbor_ip} as {neighbor_as};", "" ] # Генерация network statements для каждого IP-адреса networks_added = set() for domain, ip_list in resolved_domains.items(): for ip in ip_list: # Создаем /24 сеть из IP-адреса (последний октет заменяется на 0) ip_parts = ip.split('.') if len(ip_parts) == 4: network = f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}.0/24" if network not in networks_added: networks_added.add(network) lines.append(f" network {network} reject;") lines.append("") lines.append("}") config = "\n".join(lines) # Сохранение в файл try: with open(output_file, 'w') as f: f.write(config) logger.info(f"Конфигурация BIRD сохранена в {output_file}") except IOError as e: logger.error(f"Ошибка записи файла {output_file}: {e}") raise return config def parse_arguments() -> argparse.Namespace: """ Парсинг аргументов командной строки. Returns: argparse.Namespace: Объект с аргументами """ parser = argparse.ArgumentParser( description="Domain-based routing для BIRD BGP", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Пример usage: %(prog)s -c config.yaml %(prog)s --config config.yaml --domains domains.yaml """ ) parser.add_argument( '-c', '--config', default='config.yaml', help='Путь к конфигурационному файлу (по умолчанию: config.yaml)' ) parser.add_argument( '-d', '--domains', help='Путь к файлу со списком доменов (переопределяет настройку из config)' ) parser.add_argument( '-o', '--output', help='Путь к выходному файлу BIRD (переопределяет настройку из config)' ) parser.add_argument( '-v', '--verbose', action='store_true', help='Включить подробный вывод (DEBUG уровень логирования)' ) return parser.parse_args() def main() -> int: """ Основная функция скрипта. Returns: int: Код возврата (0 - успех, 1 - ошибка) """ args = parse_arguments() # Настройка уровня логирования if args.verbose: logger.setLevel(logging.DEBUG) try: # Загрузка конфигурации config = load_config(args.config) # Определение пути к файлу доменов domains_file = args.domains or config.get('domains_file', 'domains.yaml') # Определение пути к выходному файлу output_file = args.output or config.get('output_file', '/etc/bird/bird.conf') # DNS-сервер (не используется в текущей реализации) dns_server = config.get('dns_server') # Загрузка доменов domains = load_domains(domains_file) if not domains: logger.warning("Список доменов пуст. Завершение.") return 0 # Разрешение доменов в IP-адреса logger.info("Разрешение доменов в IP-адреса...") resolved_domains = resolve_domains(domains, dns_server) # Статистика по разрешенным доменам resolved_count = sum(1 for ips in resolved_domains.values() if ips) logger.info(f"Успешно разрешено {resolved_count} из {len(domains)} доменов") # Генерация BIRD конфигурации neighbor_ip = config.get('neighbor_ip', '192.168.1.1') neighbor_as = config.get('neighbor_as', 65000) protocol_name = config.get('protocol_name', 'domain_routing') config_content = generate_bird_config( resolved_domains, output_file, neighbor_ip, neighbor_as, protocol_name ) logger.info("Генерация BIRD конфигурации завершена") logger.info(f"Конфигурация сохранена в {output_file}") logger.info("Для применения изменений выполните: sudo birdc configure") return 0 except FileNotFoundError as e: logger.error(f"Файл не найден: {e}") return 1 except yaml.YAMLError as e: logger.error(f"Ошибка YAML: {e}") return 1 except Exception as e: logger.error(f"Неожиданная ошибка: {e}") return 1 if __name__ == '__main__': sys.exit(main())