Checking if an IP belongs to a subnetwork in Python

Posted by Diego Assencio on 2015.12.27 under Programming (Python)

This post shows how one can verify if a given IPv4/IPv6 address belongs to a given subnetwork represented in CIDR notation. The solution presented here works both on Python 2.x and 3.x, but users of Python 3.x can also use the netaddr module to achieve the same purpose (as described here).

The key idea is very simple: since IP addresses can be directly interpreted as integers (IPv4 and IPv6 addresses are 32-bit and 128-bit integers respectively), and since a subnetwork is merely a contiguous range of IP addresses, we can determine the integer values of the lower and upper bounds of the address range defined by the given subnetwork and then check if the given IP address falls within this range or not. If is falls within the computed integer range, it is part of the given subnetwork; otherwise, it is outside of it.

The ip_in_subnetwork function on the code below does what we just described. A detailed explanation of the most difficult parts of the code is given right after it.

import socket
import binascii

def ip_in_subnetwork(ip_address, subnetwork):

	Returns True if the given IP address belongs to the
	subnetwork expressed in CIDR notation, otherwise False.
	Both parameters are strings.

	Both IPv4 addresses/subnetworks (e.g. ""
	and "") and IPv6 addresses/subnetworks (e.g.
	"2a02:a448:ddb0::" and "2a02:a448:ddb0::/44") are accepted.

	(ip_integer, version1) = ip_to_integer(ip_address)
	(ip_lower, ip_upper, version2) = subnetwork_to_ip_range(subnetwork)

	if version1 != version2:
		raise ValueError("incompatible IP versions")

	return (ip_lower <= ip_integer <= ip_upper)

def ip_to_integer(ip_address):

	Converts an IP address expressed as a string to its
	representation as an integer value and returns a tuple
	(ip_integer, version), with version being the IP version
	(either 4 or 6).

	Both IPv4 addresses (e.g. "") and IPv6 addresses
	(e.g. "2a02:a448:ddb0::") are accepted.

	# try parsing the IP address first as IPv4, then as IPv6
	for version in (socket.AF_INET, socket.AF_INET6):

			ip_hex = socket.inet_pton(version, ip_address)
			ip_integer = int(binascii.hexlify(ip_hex), 16)

			return (ip_integer, 4 if version == socket.AF_INET else 6)

	raise ValueError("invalid IP address")

def subnetwork_to_ip_range(subnetwork):

	Returns a tuple (ip_lower, ip_upper, version) containing the
	integer	values of the lower and upper IP addresses respectively
	in a subnetwork expressed in CIDR notation (as a string), with
	version being the subnetwork IP version (either 4 or 6).

	Both IPv4 subnetworks (e.g. "") and IPv6
	subnetworks (e.g. "2a02:a448:ddb0::/44") are accepted.

		fragments = subnetwork.split('/')
		network_prefix = fragments[0]
		netmask_len = int(fragments[1])

		# try parsing the subnetwork first as IPv4, then as IPv6
		for version in (socket.AF_INET, socket.AF_INET6):

			ip_len = 32 if version == socket.AF_INET else 128

				suffix_mask = (1 << (ip_len - netmask_len)) - 1
				netmask = ((1 << ip_len) - 1) - suffix_mask
				ip_hex = socket.inet_pton(version, network_prefix)
				ip_lower = int(binascii.hexlify(ip_hex), 16) & netmask
				ip_upper = ip_lower + suffix_mask

				return (ip_lower,
				        4 if version == socket.AF_INET else 6)

	raise ValueError("invalid subnetwork")

A few explanations may be necessary to make this code clear (assuming, of course, that you understand the CIDR notation). To start, notice that on subnetwork_to_ip_range, we compute a "suffix mask", which is a mask whose bits are equal to one if they are in the host part of an IP address and zero if they are part of the network prefix. This mask is the same as the netmask for the given subnetwork with all bits inverted. Since the lowest IP address in a given subnetwork has all bits in the host part equal to zero, it is identical to the network prefix. By summing the network prefix and the suffix mask, we get the largest IP address in the subnetwork.

On both ip_to_integer and subnetwork_to_ip_range, the function socket.inet_pton is used to convert an IP address into its representation as a binary string (a 32-bit/4-byte string for IPv4 and a 128-bit/16-byte string for IPv6). As an example, "" is converted to "\xc0\xa8\x01\x00", whose bytes represent the numbers 192 (0xc0), 168 (0xa8), 1 (0x01) and 0 (0x00) in hexadecimal notation respectively. This binary string (ip_hex) is then converted to an actual hexadecimal string by binascii.hexlify. For instance, "\xc0\xa8\x01\x00" is converted to the ASCII string "c0a80100" (the resulting string is therefore twice as long as the original one since each byte on the original string needs two characters to be represented in hexadecimal). This string is then passed to the int constructor to be converted to an integer value. The second parameter (16) which is passed to int indicates that the input string is an integer value represented in hexadecimal notation.

Finally, notice that both ip_to_integer and subnetwork_to_ip_range try to process their input parameters first as IPv4 addresses/subnetworks, then as IPv6. An exception is thrown only if both attempts fail, but this will only happen if the input parameter does not represent a valid IP address/subnetwork.


No comments posted yet.