#!/usr/bin/env python3 """ Domain-based Routing Script for BIRD BGP Этот скрипт читает список доменов из текстового файла, разрешает их в IPv4-адреса, и создает файл с маршрутами в формате BIRD. Использует формат: route 185.76.151.0/24 reject; Пользователь должен подключить сгенерированный файл через include-директиву в конфигурации BIRD (например: include "/path/to/routes.conf";). Автор: GigaCode """ import argparse import logging import socket import sys from datetime import datetime from pathlib import Path from typing import List, Optional # Настройка логирования 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 = {} for line in f: line = line.strip() if line and not line.startswith('#'): if ':' in line: key, value = line.split(':', 1) config[key.strip()] = value.strip() logger.info(f"Конфигурация загружена из {config_path}") return config except FileNotFoundError: logger.error(f"Конфигурационный файл не найден: {config_path}") raise except Exception as e: logger.error(f"Ошибка парсинга конфигурации: {e}") raise def load_domains(domains_file: str) -> List[str]: """ Загрузка списка доменов из текстового файла (по одному домену на строку). Args: domains_file: Путь к файлу со списком доменов Returns: List[str]: Список доменов """ try: domains = [] with open(domains_file, 'r') as f: for line in f: line = line.strip() # Пропускаем пустые строки и комментарии if line and not line.startswith('#'): domains.append(line) logger.info(f"Загружено {len(domains)} доменов из {domains_file}") return domains except FileNotFoundError: logger.error(f"Файл со списком доменов не найден: {domains_file}") raise except Exception as e: logger.error(f"Ошибка чтения файла доменов: {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_routes_file(resolved_domains: dict, routes_file: str) -> str: """ Генерация файла с IP-адресами в формате BIRD route statements. Args: resolved_domains: Словарь {домен: [ip_адреса]} routes_file: Путь к выходному файлу с маршрутами Returns: str: Сгенерированное содержимое файла с маршрутами """ lines = [ "# Generated by domain-routing-bird", f"# Generated at: {datetime.now().isoformat()}", "" ] # Генерация route 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"route {network} reject;") routes_content = "\n".join(lines) # Сохранение в файл try: with open(routes_file, 'w') as f: f.write(routes_content) logger.info(f"Файл с маршрутами сохранен в {routes_file}") except IOError as e: logger.error(f"Ошибка записи файла {routes_file}: {e}") raise return routes_content 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.txt Скрипт генерирует файл с маршрутами (routes.conf), который должен быть подключен к BIRD через include-директиву в конфигурации BIRD. Пример добавления в /etc/bird/bird.conf: include "/path/to/routes.conf"; """ ) parser.add_argument( '-c', '--config', default='config.yaml', help='Путь к конфигурационному файлу (по умолчанию: config.yaml)' ) parser.add_argument( '-d', '--domains', help='Путь к файлу со списком доменов (переопределяет настройку из config)' ) parser.add_argument( '-r', '--routes', help='Путь к файлу с маршрутами (переопределяет настройку из 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.txt') # Определение пути к файлу с маршрутами routes_file = args.routes or config.get('routes_file', 'routes.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)} доменов") # Генерация файла с маршрутами logger.info("Генерация файла с маршрутами...") generate_routes_file(resolved_domains, routes_file) logger.info("Генерация файла с маршрутами завершена") logger.info(f"Файл с маршрутами сохранен в {routes_file}") logger.info("") logger.info("Для применения изменений:") logger.info(f"1. Добавьте 'include \"{routes_file}\";' в вашу конфигурацию BIRD") logger.info("2. Выполните: sudo birdc configure") return 0 except FileNotFoundError as e: logger.error(f"Файл не найден: {e}") return 1 except Exception as e: logger.error(f"Неожиданная ошибка: {e}") return 1 if __name__ == '__main__': sys.exit(main())