Скрипт агрегации списка CIDR (Python)

Этот Python-скрипт схлопывает список CIDR-блоков в минимальный эквивалентный набор — объединяет смежные и убирает пересекающиеся префиксы через ipaddress.collapse_addresses из стандартной библиотеки — и сообщает, сколько адресов покрывает список. Он не делает сетевых запросов: агрегация уже имеющегося списка CIDR — чисто локальная теоретико-множественная операция.

Как запустить этот скрипт

Зависимости: нет — только стандартная библиотека Python 3.8+ (urllib, json, csv, ipaddress, argparse). pip install не требуется.

  1. Скачайте cidr-aggregate.py ниже.
  2. Подготовьте текстовый файл с одним CIDR-блоком на строку (IPv4 и IPv6 можно смешивать).
  3. Запустите: python3 cidr-aggregate.py prefixes.txt
  4. Прочитайте выведенную сводку по каждому семейству: число на входе, число после схлопывания и общее число покрытых адресов.

Полный исходный код

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""cidr-aggregate.py -- collapse a list of CIDR blocks into the smallest
equivalent set, and count the addresses they cover.

This recipe does NOT call the ip.xakki.pro API -- collapsing/aggregating a
CIDR list you already have is pure local set arithmetic
(ipaddress.collapse_addresses from the standard library), the same honest
"no API needed here" case as subnet-membership.js in this section.

Reads a text file with one CIDR block per line (IPv4 and IPv6 may be mixed
freely -- each family is collapsed separately, since a v4 prefix and a v6
prefix can never overlap or merge). Prints the minimal equivalent CIDR set
per family, plus how many individual addresses the input covers before and
after collapsing (collapsing removes redundant overlap, it never changes
the covered address count).

Zero dependencies -- stdlib only (ipaddress, argparse). Python 3.8+.

Usage:
    python3 cidr-aggregate.py prefixes.txt
"""

import argparse
import ipaddress
import sys


def main():
    parser = argparse.ArgumentParser(description="Collapse a list of CIDR blocks into the minimal equivalent set.")
    parser.add_argument("input_file", help="Path to a text file, one CIDR block per line.")
    args = parser.parse_args()

    v4_networks, v6_networks = [], []
    errors = []
    with open(args.input_file, encoding="utf-8") as f:
        for line_no, raw_line in enumerate(f, start=1):
            line = raw_line.strip()
            if not line or line.startswith("#"):
                continue
            try:
                # strict=False: "10.1.2.3/24" is accepted and normalized to
                # "10.1.2.0/24" rather than rejected for having host bits set
                # -- this recipe aggregates real-world lists, which routinely
                # contain that shape.
                network = ipaddress.ip_network(line, strict=False)
            except ValueError as e:
                errors.append("line {}: {!r} -- {}".format(line_no, line, e))
                continue
            (v6_networks if network.version == 6 else v4_networks).append(network)

    if errors:
        print("skipped {} malformed line(s):".format(len(errors)), file=sys.stderr)
        for err in errors:
            print("  " + err, file=sys.stderr)

    for family, networks in (("IPv4", v4_networks), ("IPv6", v6_networks)):
        if not networks:
            continue
        collapsed = list(ipaddress.collapse_addresses(networks))
        addresses_before = sum(n.num_addresses for n in networks)
        addresses_after = sum(n.num_addresses for n in collapsed)
        print("\n{}: {} input block(s) -> {} collapsed block(s)".format(family, len(networks), len(collapsed)))
        print("  addresses covered: {} (collapsing never changes this total, "
              "it only removes redundant overlap)".format(addresses_after))
        if addresses_before != addresses_after:
            # Only possible if the input itself double-counted overlapping
            # ranges before collapsing folded them together.
            print("  (raw sum of individual input blocks was {}, {} of which was overlap)".format(
                addresses_before, addresses_before - addresses_after))
        for network in collapsed:
            print("  " + str(network))

    if not v4_networks and not v6_networks:
        print("no valid CIDR blocks found in {}".format(args.input_file), file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

Скрипт распространяется по лицензии MIT (заголовок SPDX-License-Identifier: MIT в начале файла) — копируйте, изменяйте и используйте свободно в своём проекте, указывать авторство не обязательно.

Скрипт агрегации списка CIDR (Python)

Записи IPv4 и IPv6 схлопываются ОТДЕЛЬНО и никогда не объединяются друг с другом — префикс v4 и префикс v6 занимают совершенно разные адресные пространства, поэтому скрипт сначала распределяет вход по семействам, а затем вызывает collapse_addresses отдельно для каждого. «10.1.2.3/24» принимается (strict=False) и нормализуется до «10.1.2.0/24», а не отклоняется из-за установленных битов хоста, поскольку реальные экспортированные списки префиксов регулярно содержат такую запись.

Схлопывание никогда не меняет общее число покрытых адресов — оно только убирает избыточное пересечение (/24, полностью входящий в /16 из того же списка, не добавляет ничего нового) и объединяет ровно смежные блоки одинакового размера в общий родительский префикс (два /24, вместе образующие ровный /23). Скрипт выводит и число блоков после схлопывания, и общее число адресов, а также отдельно сообщает, какая часть исходной суммы входа была чистым пересечением, чтобы было видно, что именно и почему уменьшилось.

Некорректные строки собираются в список ошибок ВМЕСТЕ с номером строки, а не прерывают весь прогон — одна опечатка в экспорте на 10 000 строк не должна стоить вам результатов по остальным 9999. Пустые строки и строки, начинающиеся с «#», молча трактуются как комментарии/пробелы, а не ошибки, так что скрипт принимает те же слегка аннотированные файлы списков префиксов, которые реально хранятся в системах контроля версий.

Глоссарий

collapse_addresses
Функция collapse_addresses из стандартной библиотеки ipaddress Python, объединяющая список сетей в минимальный эквивалентный набор непересекающихся CIDR-блоков — вся логика агрегации в этом скрипте сводится к одному её вызову.
strict=False
Опция ipaddress.ip_network, принимающая префикс с установленными битами хоста (например, 10.1.2.3/24) и нормализующая его до адреса сети, вместо того чтобы выбрасывать ошибку.
Смежные блоки
Два CIDR-блока одинакового размера, чьи адреса идут подряд и вместе точно заполняют общий родительский префикс на один бит короче (например, 10.0.0.0/24 + 10.0.1.0/24 = 10.0.0.0/23).
Избыточное пересечение
Меньший блок, полностью входящий в больший из того же списка — не добавляет новых адресов и полностью исчезает после схлопывания.
num_addresses
Собственное свойство объекта сети ipaddress, дающее число адресов — у /24 их 256, у /32 — 1 — суммируется здесь для вывода общего покрытия до и после схлопывания.
Список префиксов
Обычный текстовый экспорт CIDR-блоков (например, ACL, allow-list, вход для BGP-фильтра) — сырые данные, с которыми работает этот рецепт и geofeed-parse.py из этого раздела.

Аналог в API

Этот скрипт не делает сетевых запросов и не имеет эквивалента в API — вся задача локальные вычисления/разбор уже имеющихся данных, отвечать здесь нашему (или любому) API не на что. Если нужны ещё и живые факты об IP (геолокация, ASN, признаки угроз/фрода) — это описано в документации API:

Читать документацию API

Часто задаваемые вопросы

Меняет ли схлопывание набор покрываемых адресов?
Нет — множество покрываемых адресов математически идентично до и после; схлопывание меняет только то, сколькими CIDR-блоками выражается это же множество.
Можно ли смешивать IPv4 и IPv6 в одном входном файле?
Да — скрипт автоматически распределяет их по семействам и выводит отдельную сводку для каждого; между собой они никогда не сравниваются и не объединяются.
Что происходит с некорректной строкой, например с опечаткой в октете?
Она попадает в список ошибок с номером строки и выводится в stderr; остальная часть файла обрабатывается как обычно.
Зачем агрегировать список CIDR вместо использования как есть?
У правил firewall/ACL/BGP-фильтра часто есть жёсткие лимиты по количеству, а меньший, дедуплицированный набор правил быстрее вычисляется и проще для проверки человеком, чем избыточный с пересекающимися записями.
Обращается ли этот рецепт к API ip.xakki.pro?
Нет, вообще не обращается — см. раздел «Аналог в API» выше о причинах: это чистая локальная теоретико-множественная операция над уже имеющимся списком, искать в API нечего.

Понравился инструмент? Бесплатный аккаунт нашего API даёт 1000 запросов в день.Смотреть документацию API →