domain-routing-bird/domain_routing.py
a.kazantsev baa3474d0f 2
2026-07-03 17:06:48 +03:00

385 lines
14 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-адреса,
создает файл с IP-адресами и генерирует BGP-конфигурацию для BIRD.
Использует формат:
route 185.76.151.0/24 reject;
Автор: GigaCode
"""
import argparse
import logging
import socket
import sys
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: {__import__('datetime').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 load_routes(routes_file: str) -> str:
"""
Загрузка существующего файла с маршрутами.
Args:
routes_file: Путь к файлу с маршрутами
Returns:
str: Содержимое файла с маршрутами
"""
try:
with open(routes_file, 'r') as f:
content = f.read()
logger.info(f"Загружен файл с маршрутами из {routes_file}")
return content
except FileNotFoundError:
logger.warning(f"Файл с маршрутами не найден: {routes_file}")
return ""
except IOError as e:
logger.error(f"Ошибка чтения файла {routes_file}: {e}")
raise
def generate_bird_config(
routes_file: str,
output_file: str,
neighbor_ip: str = "192.168.1.1",
neighbor_as: int = 65000,
protocol_name: str = "domain_routing"
) -> str:
"""
Генерация конфигурационного файла для BIRD BGP.
Args:
routes_file: Путь к файлу с маршрутами
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};",
""
]
# Загрузка маршрутов из файла
try:
with open(routes_file, 'r') as f:
routes_content = f.read()
# Извлечение строк с route statements
for line in routes_content.split('\n'):
if line.strip().startswith('route '):
lines.append(f" {line.strip()}")
except FileNotFoundError:
logger.warning(f"Файл с маршрутами не найден: {routes_file}")
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.txt
"""
)
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(
'-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')
# Определение пути к выходному файлу
output_file = args.output or config.get('output_file', '/etc/bird/bird.conf')
# Определение пути к файлу с маршрутами
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)
# Генерация BIRD конфигурации
neighbor_ip = config.get('neighbor_ip', '192.168.1.1')
neighbor_as = int(config.get('neighbor_as', '65000'))
protocol_name = config.get('protocol_name', 'domain_routing')
config_content = generate_bird_config(
routes_file,
output_file,
neighbor_ip,
neighbor_as,
protocol_name
)
logger.info("Генерация BIRD конфигурации завершена")
logger.info(f"Конфигурация BIRD сохранена в {output_file}")
logger.info(f"Файл с маршрутами сохранен в {routes_file}")
logger.info("Для применения изменений выполните: 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())