Files

115 lines
4.6 KiB
Python
Raw Permalink Normal View History

2026-09-21 07:29:38 +03:00
"""Подготовка списка адресов к выдаче: фильтр по версии IP, агрегация CIDR, форматы конфигураций."""
import datetime
import ipaddress
import logging
logger = logging.getLogger(__name__)
FORMATS = ("json", "nftables", "mikrotik", "bird", "frr")
def select_strings(values, ip_version="all"):
"""Путь по умолчанию: строки как есть (без разбора), только фильтр версии и сортировка."""
if ip_version == "4":
values = [v for v in values if ":" not in v]
elif ip_version == "6":
values = [v for v in values if ":" in v]
return sorted(set(values))
def select_networks(values, ip_version="all", aggregate=False):
"""Разбирает значения в сети; возвращает (v4, v6). Одиночные IP становятся /32 и /128."""
v4, v6 = [], []
for value in set(values):
try:
net = ipaddress.ip_network(value, strict=False)
except ValueError:
logger.warning("Skipping invalid address: %s", value)
continue
(v4 if net.version == 4 else v6).append(net)
if ip_version == "4":
v6 = []
elif ip_version == "6":
v4 = []
if aggregate:
v4 = list(ipaddress.collapse_addresses(v4))
v6 = list(ipaddress.collapse_addresses(v6))
return sorted(v4), sorted(v6)
def networks_to_strings(v4, v6):
"""Для JSON: /32 и /128 печатаются как «голый» IP, как и в неагрегированном выводе."""
return [str(n.network_address) if n.prefixlen == n.max_prefixlen else str(n) for n in [*v4, *v6]]
def _block(header, items, footer):
# Без запятой после последнего элемента: BIRD её не принимает
last = len(items) - 1
return [header, *(f" {item}{',' if i < last else ''}" for i, item in enumerate(items)), footer]
def _nftables(v4, v6, name, comment):
lines = [f"# {comment}", f"add table inet {name}"]
for suffix, nets, addr_type in (("v4", v4, "ipv4_addr"), ("v6", v6, "ipv6_addr")):
if not nets:
continue
set_name = f"{name}_{suffix}"
lines += [
# auto-merge: без него nft отвергает пересекающиеся интервалы
f"add set inet {name} {set_name} {{ type {addr_type}; flags interval; auto-merge; }}",
f"flush set inet {name} {set_name}",
*_block(f"add element inet {name} {set_name} {{", nets, "}"),
]
return lines
def _mikrotik(v4, v6, name, comment):
lines = [f"# {comment}"]
for path, nets in (("/ip firewall address-list", v4), ("/ipv6 firewall address-list", v6)):
if not nets:
continue
lines.append(f"{path} remove [find list={name}]")
lines += [f"{path} add list={name} address={net}" for net in nets]
return lines
def _bird(v4, v6, name, comment):
lines = [f"# {comment}"]
for suffix, nets in (("V4", v4), ("V6", v6)):
if nets:
lines += _block(f"define {name.upper()}_{suffix} = [", nets, "];")
return lines
def _frr(v4, v6, name, comment):
lines = [f"! {comment}"]
for family, suffix, nets in (("ip", "v4", v4), ("ipv6", "v6", v6)):
if not nets:
continue
list_name = f"{name}_{suffix}"
lines.append(f"no {family} prefix-list {list_name}")
lines += [f"{family} prefix-list {list_name} seq {5 * i} permit {net}"
for i, net in enumerate(nets, start=1)]
return lines
_RENDERERS = {"nftables": _nftables, "mikrotik": _mikrotik, "bird": _bird, "frr": _frr}
def render(fmt, v4, v6, name, ip_version="all", aggregate=False):
"""Возвращает готовый скрипт конфигурации (идемпотентный: повторное применение заменяет список)."""
stamp = datetime.datetime.now().isoformat(timespec="seconds")
comment = f"generated {stamp}, ip_version={ip_version}, aggregate={str(aggregate).lower()}"
return "\n".join(_RENDERERS[fmt](v4, v6, name, comment)) + "\n"
def build_output(values, fmt="json", ip_version="all", aggregate=False, name="ripe"):
"""JSON -> список строк, остальные форматы -> текст конфигурации."""
if fmt == "json" and not aggregate:
return select_strings(values, ip_version)
v4, v6 = select_networks(values, ip_version, aggregate)
if fmt == "json":
return networks_to_strings(v4, v6)
return render(fmt, v4, v6, name, ip_version, aggregate)