domain-routing-bird/domain_routing.py
a.kazantsev 70d5b4f73b update
2026-07-03 17:33:29 +03:00

294 lines
11 KiB
Python
Executable file
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
Этот скрипт читает список доменов из текстового файла, разрешает их в 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())