If you work with computer networks sooner or later you will have to learn how to efficiently work with IP addresses and networks. As you probably guessed from the title of this post, we'll be learning how to create, modify and perform operations on IP objects using Python.

Having to manipulate IP objects is common enough that Python ended up with a built in library dedicated to these tasks. It's name is... you probably guessed it, the name of the module is ipaddress. That's right, all you have to do to start working with IP goodness is one line,import ipaddress, at the top of your source code file.

Working with IP addresses in Python series:

Contents

Overview of ipaddress library and basic usage

Module ipaddress consists of a number of classes and and some module functions that allow us to work with both IPv4 and IPv6 objects.

It's worth mentioning that all of the object types, Address, Network and IPInterface, are hashable so can be used as keys in dictionary.

All of my examples will be based around IPv4 but most of IPv6 functionality is largely the same, with exception of few IPv6 only methods.

I also chose to work with factory convenience functions which are IP version agnostic. This means you can give them either IPv4 or IPv6 address/network and you'll get back object of appropriate class. If you don't use factory functions you'll have to explicitly create objects of classes IPv4* or IPv6* depending on your requirements. E.g.

Instead of:

ipa = ipaddress.ip_address("10.10.1.0")

You'd have to use:

ipa = ipaddress.IPv4Address("10.10.1.0")

This convenience comes at a cost of error messages not being as informative since they need to deal with both IPv4 and IPv6.

Now that we know why use convenience functions, let's see what they are:

  • ipaddress.ip_address(address) - returns object of IPv4Address or IPv6Address class
  • ipaddress.ip_network(address) - returns object of IPv4Network or IPv6Network class
  • ipaddress.ip_interface(address) - returns object of IPv4Interface or IPv6Interface class

It's probably easy to figure out what first two are doing. Third one gives us objects similar to Network objects but with some extra features.

Working with IP addresses

Basic operations

Let's kick off with investigating operations on IP address objects.

>>> ipa = ipaddress.ip_address("10.0.0.133")
>>> ipa
IPv4Address('10.0.0.133')

We create IP address objects by passing a string representing our IP, pretty standard stuff.

Now, what can we do with IP objects?

Well, we can check if it's a global or a private IP:

>>> ipa.is_private
True
>>> ipa.is_global
False

We can even check if it's somewhat special:

>>> ipaddress.ip_address("127.0.0.1").is_loopback
True
>>> ipaddress.ip_address("229.100.0.23").is_multicast
True
>>> ipaddress.ip_address("169.254.0.100").is_link_local
True
>>> ipaddress.ip_address("240.10.0.1").is_reserved
True

We can quickly create reverse DNS pointer, very handy indeed:

>>> ipa.reverse_pointer
'133.0.0.10.in-addr.arpa'

And then there is IP arithmetic:

>>> ipa + 1
IPv4Address('10.0.0.134')
>>> ipa - 5
IPv4Address('10.0.0.128')

And if you have some application, or otherwise need, asking for integer representation of IP, it's as simple as:

>>> int(ipb)
167772296

Which also works both ways, ipaddress will happily take integer and will give us back an object:

>>> ipc = ipaddress.ip_address(3794067923)
>>> ipc
IPv4Address('226.36.225.211')

And yes, it work with IPv6 too, if that's your thing:

>>> ipd = ipaddress.ip_address(105253214244521019198275021855725010304)
>>> ipd
IPv6Address('4f2f:81d:dc95:f464:2f09:a97b:f62d:3d80')

That's mostly it as far as IP addresses go though they'll make appearance again when we talk about some more advanced stuff.

Working with IP networks

Next on our list are IP network objects. We can do much more here, so this will be a longer section. We'll jump right in by creating some objects and then we'll poke around.

Unlike with IP address objects we can use different string representations to create an object:

>>> net_pfx_len = "192.0.2.0/24"
>>> net_mask = "192.0.2.0/255.255.255.0"
>>> net_wcard = "192.0.2.0/0.0.0.255"
>>>
>>> net_o_pfx_len = ipaddress.ip_network(net_pfx_len)
>>> net_o_mask = ipaddress.ip_network(net_mask)
>>> net_o_wcard = ipaddress.ip_network(net_wcard)
>>>
>>> net_o_pfx_len
IPv4Network('192.0.2.0/24')
>>> net_o_mask
IPv4Network('192.0.2.0/24')
>>> net_o_wcard
IPv4Network('192.0.2.0/24')
>>>
>>> net_o_pfx_len == net_o_mask == net_o_wcard
True

No matter which format we use the end effect will be the same. Different vendors use different representation so it's nice that ipaddress module supports them all.

Let's keep poking around.

Getting back string representation in particular format:

>>> ipn = ipaddress.ip_network("10.0.0.0/16")
>>> ipn.with_prefixlen
'10.0.0.0/16'
>>> ipn.with_hostmask
'10.0.0.0/0.0.255.255'
>>> ipn.with_netmask
'10.0.0.0/255.255.0.0'

Depending on your requirements you can quickly get IP network in required format without having to write custom code.

You can also get network part and mask parts separately. Again, with support for different formats.

>>> ipn.network_address
IPv4Address('10.0.0.0')
>>> ipn.prefixlen
16
>>> ipn.hostmask
IPv4Address('0.0.255.255')
>>> ipn.netmask
IPv4Address('255.255.0.0')

With these newly acquired powers you can generate some Cisco ACLs using your objects:

>>> ace_fmt = "permit ip {} {} {} {}"
>>> src_net = ipaddress.ip_network("10.2.0.0/24")
>>>
>>> dst_nets = [ipaddress.ip_network("10.1.{}.0/24".format(n)) for n in range(0,11,2)]
>>> aces = "\n".join([ace_fmt.format(src_net.network_address, src_net.hostmask, 
... d.network_address, d.hostmask) for d in dst_nets])
>>> print(aces)
permit ip 10.2.0.0 0.0.0.255 10.1.0.0 0.0.0.255
permit ip 10.2.0.0 0.0.0.255 10.1.2.0 0.0.0.255
permit ip 10.2.0.0 0.0.0.255 10.1.4.0 0.0.0.255
permit ip 10.2.0.0 0.0.0.255 10.1.6.0 0.0.0.255
permit ip 10.2.0.0 0.0.0.255 10.1.8.0 0.0.0.255
permit ip 10.2.0.0 0.0.0.255 10.1.10.0 0.0.0.255

So far so good, but that's still not showing the true power of the module. The following examples will dig into more advanced functionality.

Network host IPs

With network object in hand we can ask for hosts that are contained in the network:

>>> ipn192 = ipaddress.ip_network("192.168.0.0/16")
>>> ipn192.hosts()
<generator object _BaseNetwork.hosts at 0x0302FCF0

Method hosts() returns an iterator, which means that you will have to go over it with for loop or convert into a list.

Generally I would advise against converting it into list as that might be very resources intensives for larger networks.

This might not be a big problem for IPv4 but with IPv6 we might accidentally run into some performance issues.

If we only need few hosts we can use next() or for loop with islice if hosts come from the middle of the range:

ipn192h = ipn192.hosts()
>>> next(ipn192h)
IPv4Address('192.168.0.1')
>>> next(ipn192h)
IPv4Address('192.168.0.2')
>>> next(ipn192h)
IPv4Address('192.168.0.3')

>>> from itertools import islice
>>> for hip in islice(ipn192.hosts(), 19, 25):
...     print(hip)
...     
192.168.0.20
192.168.0.21
192.168.0.22
192.168.0.23
192.168.0.24
192.168.0.25

As an alternative, it's also possible to access specific host IP using index operator:

>>> ipn192[3]
IPv4Address('192.168.0.3')
>>> ipn192[60]
IPv4Address('192.168.0.60')

Note that hosts() only returns usable IPs, if you want to iterate over all hosts you can do it directly over the network object:

>>> for hip in islice(ipn192, 0, 5):
...     print(hip)  
...     
192.168.0.0
192.168.0.1
192.168.0.2
192.168.0.3
192.168.0.4

And then there is broadcast address which we can get via broadcast_address property:

>>> ipn192.broadcast_address
IPv4Address('192.168.255.255')

As you can see ipaddress module is starting to show its power. We'll now focus on some other useful operations.

Subnets, supernets and checks

We'll sart with something very handy. Quickly checking for overlap between networks:

>>> ipn1 = ipaddress.ip_network("10.10.1.32/28")
>>> ipn2 = ipaddress.ip_network("10.10.1.32/27")
>>> ipn3 = ipaddress.ip_network("10.10.1.48/29")
>>> ipn1.overlaps(ipn2)
True
>>> ipn1.overlaps(ipn3)
False
>>> ipn2.overlaps(ipn3)
True
>>> ipn3.overlaps(ipn2)
True

You could use it to optimise your ACLs, or perhaps check if you have some routing overlap?

As you can see overlaps is a two-way relationship, if ipn1 overlaps ipn2 then it follows that ipn2 overlaps ipn1. But we can be more granular than that and explicitly ask if one network is a subnet, or supernet, of the other:

>>> ipn2.supernet_of(ipn3)
True
>>> ipn3.supernet_of(ipn2)
False
>>> ipn3.subnet_of(ipn2)
True

So we are able to check different properties and do comparison between networks. We can do more though. We can get supernet of our network or generate subnets of specific sizes, and even get subnets left after specific network is removing from the parent.

Getting supernets is quick and easy but so powerful:

>>> nchild = ipaddress.ip_network("172.20.15.160/29")
>>> nchild.supernet(prefixlen_diff=3)
IPv4Network('172.20.15.128/26')
>>> nchild.supernet(new_prefix=23)
IPv4Network('172.20.14.0/23')

How good is that? You can instantly get supernet with specific prefix length that will contain you network.

Getting subnet is similar except we will get iterator over resulting subnets.

>>> nparent = ipaddress.ip_network("10.25.2.0/23")
>>> list(nparent.subnets())
[IPv4Network('10.25.2.0/24'), IPv4Network('10.25.3.0/24')]

By default we will get iterator over largest subnets possible. If you want specific size you can again use prefixlen_diff and new_prefix arguments:

>>> list(nparent.subnets(prefixlen_diff=2))
[IPv4Network('10.25.2.0/25'), IPv4Network('10.25.2.128/25'), IPv4Network('10.25.3.0/25'), IPv4Network('10.25.3.128/25')]
>>> from pprint import pprint
>>> pprint(list(nparent.subnets(new_prefix=27)))
[IPv4Network('10.25.2.0/27'),
 IPv4Network('10.25.2.32/27'),
 IPv4Network('10.25.2.64/27'),
 IPv4Network('10.25.2.96/27'),
 IPv4Network('10.25.2.128/27'),
 IPv4Network('10.25.2.160/27'),
 IPv4Network('10.25.2.192/27'),
 IPv4Network('10.25.2.224/27'),
 IPv4Network('10.25.3.0/27'),
 IPv4Network('10.25.3.32/27'),
 IPv4Network('10.25.3.64/27'),
 IPv4Network('10.25.3.96/27'),
 IPv4Network('10.25.3.128/27'),
 IPv4Network('10.25.3.160/27'),
 IPv4Network('10.25.3.192/27'),
 IPv4Network('10.25.3.224/27')]

There is one more interesting method that can be used with address objects: address_exclude. This will return iterator over network objects left after given network is removed from the parent.

So for instance, we have 10.2.0.0/23 and we assign 10.2.1.0/25 from it. We now want to know what networks are left for future use after we took our /25.

ipn10 = ipaddress.ip_network("10.2.0.0/23")
>>> [n for n in ipn10.address_exclude(ipaddress.ip_network("10.2.1.0/25"))]
[IPv4Network('10.2.0.0/24'), IPv4Network('10.2.1.128/25')]

And there you go, we are now left with 10.2.0.0/24 and 10.2.1.128/25.

Lastly, checking if IP address belongs to IP network is done with in operator:

>>> ipaddress.ip_address("10.2.1.2") in ipn10
True
>>> ipaddress.ip_address("10.5.1.2") in ipn10
False

Summary

This is it for part 1 in the series showing how to work with IP addresses in Python.

In part 2 I will discuss last of the major objects provided by the library, Ipinterface.

I will also talk about module level functions and we will then have a look at comparing and sorting objects to finally finish with discussion on validation and exception handling.

Happy IPing!

References

Official documentation for ipaddress Python module. Available at: https://docs.python.org/3/library/ipaddress.html
"An introduction to the ipaddress module". Available at: https://docs.python.org/3/howto/ipaddress.html
GitHub repo with source code for this post. Available at: https://github.com/progala/ttl255.com/tree/master/python/working-w-ipaddress-part1