294 lines
11 KiB
Python
Executable file
294 lines
11 KiB
Python
Executable file
#!/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())
|