"""Подготовка списка адресов к выдаче: фильтр по версии 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)