Collector daemon, FastAPI server (addresses, diff, collect, sources), SQLite storage with change journal, Docker Compose deployment, tests, documentation and project rules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
116 lines
4.6 KiB
Python
116 lines
4.6 KiB
Python
"""Подготовка списка адресов к выдаче: фильтр по версии 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)
|