domain-routing-bird/domain_routing.py
a.kazantsev 00b3956580 1
2026-07-03 16:51:33 +03:00

303 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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())