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