{"id":115,"date":"2014-04-08T13:14:39","date_gmt":"2014-04-08T18:14:39","guid":{"rendered":"https:\/\/adamkuj.net\/blog\/?p=115"},"modified":"2021-05-11T09:00:34","modified_gmt":"2021-05-11T14:00:34","slug":"a-utility-to-perform-ipv4-ipv6-prefix-aggregation","status":"publish","type":"post","link":"https:\/\/adamkuj.net\/blog\/2014\/04\/08\/a-utility-to-perform-ipv4-ipv6-prefix-aggregation\/","title":{"rendered":"A utility to perform IPv4 &#038; IPv6 prefix aggregation"},"content":{"rendered":"<p>I&#8217;m a fan of the <a href=\"http:\/\/manpages.ubuntu.com\/manpages\/man1\/aggregate.1.html\" title=\"AGGREGATE(1)\">aggregate<\/a> program, which, in its own words, is used to &#8220;optimise a list of route prefixes to help make nice short filters&#8221;. However, it doesn&#8217;t work for IPv6! It was written by Joe Abley back in the 1990&#8217;s, and is useful in lots of scripts, but the lack of IPv6 support is starting to be an issue for me.<\/p>\n<p>Using the <a href=\"https:\/\/pypi.python.org\/pypi\/IPy\/\" title=\"IPy\">IPy python module<\/a>, I&#8217;ve created a similar tool that handles both IPv4 and IPv6 prefixes. Like the old tool, it can handle host addresses that don&#8217;t have a netmask specified, and can truncate the network portion to match the netmask if they are mismatched. I also added support for pre-1993 classful networking (since IOS and Quagga\/Zebra still seem to leave the mask off such networks, two decades post-CIDR).<\/p>\n<p>It doesn&#8217;t have all the features of the old aggregate command (i.e. &#8211;max-opt-lengh, etc), but it has all the features I used, and some that I had always wished for.<\/p>\n<p><!--more--><\/p>\n<p>[python collapse=&#8221;true&#8221; title=&#8221;aggregate.py&#8221; language=&#8221;true&#8221;]<br \/>\n#!\/usr\/bin\/python<\/p>\n<p>&#8220;&#8221;&#8221;<br \/>\nALK 2014-04-08<br \/>\naggregate.py: IPv4 &amp; IPv6 replacement (in spirit) for Joe Abley&#8217;s &#8216;aggregate&#8217; command<br \/>\nnote: not a drop-in replacement for the original &#8216;aggregate&#8217; command &#8211; command line flags are different<br \/>\nrequirements: IPy class (debian: python-ipy package)<\/p>\n<p>input: list of IPv4 and\/or IPv6 addresses and\/or subnets (filename or STDIN)<br \/>\noutput: aggregated list if subnets (STDOUT)<\/p>\n<p>options:<br \/>\n&#8211;host|-h: convert host addresses w\/o masks to IPv4 \/32 or IPv6 \/128 networks when sending output<br \/>\ni.e. 192.0.2.10 becomes 192.0.2.10\/32<br \/>\ni.e. 2001:db8:abcd:1234::d00d becomes 2001:db8:abcd:1234::d00d\/128)<br \/>\nnote: hosts (w\/o mask specified) are *always* permitted in the input, and will be auto-aggregated<br \/>\n&#8211;cidr|-c: convert IPv4 classfull networks without netmasks to proper cidr subnets<br \/>\ni.e. Class A: 10.0.0.0 becomes 10.0.0.0\/8 (but 10.1.0.0 fails, unless -h is used to make it 10.1.0.0\/32)<br \/>\ni.e. Class B: 172.24.0.0 becomes 172.24.0.0\/16 (but 172.24.10.0 fails, unless -h is used to make a \/32)<br \/>\ni.e. Class C: 192.0.0.0 becomes 192.0.0.0\/24)<br \/>\nif -h and -c are used concurrenty:<br \/>\n-c is evaluated first (otherwise anything w\/o a mask would be considered a host prefix)<br \/>\n&#8211;truncate|-t: prune the network portion to match the netmask<br \/>\n(i.e. 1.2.3.4\/24 becomes 1.2.3.0\/24)<br \/>\n(i.e. 2001:db8:abcd:1234::dead:beef\/64 becomes 2001:db8:abcd:1234::\/64<br \/>\n&#8211;ipv4|-4 or &#8211;ipv6|-6: display only IPv4 or only IPv6 prefixes<br \/>\n&#8211;ignore|-i: ignore lines that can&#8217;t be parsed (otherwise, program will exit w\/ error)<br \/>\n&#8220;&#8221;&#8221;<\/p>\n<p>from IPy import IP, IPSet<br \/>\nimport argparse<br \/>\nimport fileinput<br \/>\nimport sys<\/p>\n<p>parser = argparse.ArgumentParser(conflict_handler=&#8217;resolve&#8217;)<\/p>\n<p>parser.add_argument(&#8216;-t&#8217;, &#8216;&#8211;truncate&#8217;, action=&#8217;store_true&#8217;, dest=&#8217;truncate&#8217;, help=&#8217;truncate the network portion of the prefix to match the netmask (i.e. 192.0.2.10\/24 becomes 192.0.2.0\/24)&#8217;, default=False)<br \/>\nparser.add_argument(&#8216;-h&#8217;, &#8216;&#8211;host&#8217;, action=&#8217;store_true&#8217;, dest=&#8217;host&#8217;, help=&#8217;convert host addresses w\/o masks to IPv4 \/32 or IPv6 \/128 networks&#8217;, default=None)<br \/>\nparser.add_argument(&#8216;-c&#8217;, &#8216;&#8211;cidr&#8217;, dest=&#8217;cidr&#8217;, action=&#8217;store_true&#8217;, help=&#8217;add netmasks to classful IPv4 network definitions&#8217;)<br \/>\nparser.add_argument(&#8216;-i&#8217;, &#8216;&#8211;ignore-errors&#8217;, dest=&#8217;ignore&#8217;, action=&#8217;store_true&#8217;, help=&#8217;discard malformed input lines instead of exiting&#8217;, default=False)<br \/>\nversion = parser.add_mutually_exclusive_group()<br \/>\nversion.add_argument(&#8216;-4&#8217;, &#8216;&#8211;ipv4&#8242;, dest=&#8217;ipv6&#8242;, action=&#8217;store_false&#8217;, help=&#8217;display only IPv4 prefixes (default: show IPv4 &amp; IPv6)&#8217;, default=True)<br \/>\nversion.add_argument(&#8216;-6&#8217;, &#8216;&#8211;ipv6&#8242;, dest=&#8217;ipv4&#8242;, action=&#8217;store_false&#8217;, help=&#8217;display only IPv6 prefixes (default: show IPv4 &amp; IPv6)&#8217;, default=True)<br \/>\nparser.add_argument(&#8216;args&#8217;, nargs=argparse.REMAINDER, help='&lt;input file&gt; or STDIN&#8217;)<br \/>\nargs=parser.parse_args()<\/p>\n<p>s = IPSet() # the set of aggregated addresses<\/p>\n<p>try:<br \/>\nfor line in fileinput.input(args.args):<br \/>\ntry:<br \/>\nip = IP(line,make_net=args.truncate) # read the line as an IP network; truncate if -t was specified<br \/>\nexcept ValueError as err: # exception if the line can&#8217;t be parsed as an IP prefix<br \/>\nif args.ignore == False:<br \/>\nprint(err)<br \/>\nprint &#8220;exiting&#8230;&#8221;<br \/>\nsys.exit(1)<\/p>\n<p># process -c option<br \/>\nif args.cidr:<br \/>\nif ip in IP(&#8216;0.0.0.0\/1&#8217;) and ( ip.int() &amp; 0x00ffffff == 0x00000000 ) and ip.prefixlen() == 32:<br \/>\nip=ip.make_net(8)<br \/>\nif ip in IP(&#8216;128.0.0.0\/2&#8217;) and ( ip.int() &amp; 0x0000ffff == 0x00000000 ) and ip.prefixlen() == 32:<br \/>\nip=ip.make_net(16)<br \/>\nif ip in IP(&#8216;192.0.0.0\/3&#8217;) and ( ip.int() &amp; 0x000000ff == 0x00000000 ) and ip.prefixlen() == 32:<br \/>\nip=ip.make_net(24)<\/p>\n<p># process -h option<br \/>\nif args.host:<br \/>\nip.NoPrefixForSingleIp = None<\/p>\n<p>s.add(ip) # add the IP into the set, automatically aggregating as necessary<\/p>\n<p>except KeyboardInterrupt:  # show usage if user exits w\/ CTRL-C<br \/>\nprint<br \/>\nparser.print_help()<br \/>\nsys.exit(1)<\/p>\n<p># send the results to STDOUT<br \/>\nfor prefix in s:<br \/>\nif args.ipv4 &amp; (prefix.version() == 4):<br \/>\nprint (prefix)<br \/>\nif args.ipv6 &amp; (prefix.version() == 6):<br \/>\nprint (prefix)<\/p>\n<p>[\/python]<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;m a fan of the aggregate program, which, in its own words, is used to &#8220;optimise a list of route prefixes to help make nice short filters&#8221;. However, it doesn&#8217;t work for IPv6! It was written by Joe Abley back in the 1990&#8217;s, and is useful in lots of scripts, but the lack of IPv6 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[18,17,19,20,15],"class_list":["post-115","post","type-post","status-publish","format-standard","hentry","category-networking-tools","tag-aggregation","tag-cidr","tag-ipv6","tag-prefix-list","tag-python"],"_links":{"self":[{"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/posts\/115","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/comments?post=115"}],"version-history":[{"count":22,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/posts\/115\/revisions"}],"predecessor-version":[{"id":318,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/posts\/115\/revisions\/318"}],"wp:attachment":[{"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/media?parent=115"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/categories?post=115"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/adamkuj.net\/blog\/wp-json\/wp\/v2\/tags?post=115"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}