Last active
June 26, 2026 14:10
-
-
Save pmeulen/476208492a9938f408459747247c9902 to your computer and use it in GitHub Desktop.
Python script to represent a list of CIDR network ranges in the smallest number of CIDR subnets. Subnets can be added and substracted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # Copyright 2018, 2026 SURF B.V. | |
| # Requires python 3.10+ | |
| import argparse, sys | |
| try: | |
| from netaddr import IPNetwork, IPSet | |
| except ImportError: | |
| print("The netaddr module is required. Install it with e.g. 'pip install netaddr'.") | |
| sys.exit(1) | |
| class IPNetworkArg: | |
| """Parse an IPv4 or IPv6 network argument from the command line | |
| The IP network can be any network that is supported by netaddr.IPNetwork. | |
| If no netmask is specified, /32 (for IPv4) or /128 (for IPv6) is assumed. | |
| E.g.: | |
| "192.168.0.0/24" | |
| "192.168.0.2" => "192.168.0.2/32" | |
| "2001:db8:abcd::/48" | |
| "2001:db8:abcd:1::10.0.0.1" => "2001:db8:abcd:1::a00:1/128" | |
| The network address can be prefixed with "+" (default) or "-" to specify whether the network | |
| must be added or removed from the range. | |
| Any leading/trailing whitespace and comments (# ...) are ignored. | |
| An empty string is assigned the None network. | |
| Supported """ | |
| def __init__(self, network: str): | |
| # Remove comment (# ....) and leading/trailing whitespace | |
| network = network.split('#')[0].strip() | |
| if len(network)==0: | |
| self.__ip_network = None | |
| return | |
| self.__add = True | |
| if network.startswith('-'): | |
| network = network[1:] | |
| self.__add = False # I.e remove | |
| elif network.startswith('+'): | |
| network = network[1:] | |
| try: | |
| self.__ip_network = IPNetwork(network) | |
| except Exception as e: | |
| raise argparse.ArgumentTypeError(f"Invalid IP network specification: \"{network}\". Error: {e}") | |
| @property | |
| def ip_network(self) -> IPNetwork | None: | |
| return self.__ip_network | |
| @property | |
| def add(self) -> bool: | |
| return self.__add | |
| def __str__(self): | |
| if self.__ip_network is None: | |
| return "" | |
| return ("+" if self.add else "-") + self.__ip_network.__str__() | |
| def __bool__(self): | |
| return self.__ip_network is not None | |
| parser = argparse.ArgumentParser( | |
| prefix_chars='/', | |
| fromfile_prefix_chars='@', | |
| description='Represent a list of CIDR network ranges in the smallest number of CIDR subnets. ' | |
| 'The subnets are added to and subtracted from the final range in the order they are specified on the command line. ' | |
| 'Example: cidr_merge.py 192.168.0.0/28 -192.168.0.10' | |
| ) | |
| parser.add_argument( | |
| dest='cidr_list', | |
| type=IPNetworkArg, | |
| nargs='+', | |
| help='A list of IP Addresses or IP networks in CIDR format (e.g. "192.168.0.0/24"). ' | |
| 'To subtract a range, prefix it with "-" (e.g. "-192.168.0.3"). ' | |
| 'To load a list from file use "@filename".' | |
| 'Leading/trailing whitespace and comments (# ...) are ignored.', | |
| ) | |
| # Show help and exit when no arguments are provided | |
| if len(sys.argv)==1: | |
| parser.print_help() | |
| parser.exit(0) | |
| # Parse arguments | |
| args = parser.parse_args() | |
| # Merge the CIDR ranges | |
| merged_list=IPSet() | |
| for cidr in args.cidr_list: | |
| if not cidr: | |
| continue | |
| if cidr.add: | |
| merged_list.add(cidr.ip_network) | |
| else: | |
| merged_list.remove(cidr.ip_network) | |
| # Print merged list | |
| for cidr in merged_list.iter_cidrs(): | |
| print(cidr) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment