IPv4 & IPv6 DIY CIDR Membership and Ranges
There's the ipaddress
module that provides all the functionality one should ever need. The below is not based on it - it just shows another way it could be done.
Building Blocks
def ipv4_mask(cidr):
mask = 2**32 - 2**(32 - int(cidr))
return (mask >> sh & 0xff for sh in (24, 16, 8, 0))
def ipv6_mask(cidr):
mask = 2**128 - 2**(128 - int(cidr))
return (mask >> sh & 0xff for sh in range(120, -1, -8))
def ipv4_bytes(ip):
return (int(b) for b in ip.split('.'))
def ipv6_bytes(ip):
words = ip.split(':')
filled = False
for word in words:
if word:
yield int(word[:-2] or '0', 16)
yield int(word[-2:], 16)
elif filled:
yield 0
yield 0
else:
filled = True
for _ in range(9 - len(words)):
yield 0
yield 0
All the basic functions are very simple aside from the IPv6 bytes function. The different formats for IPv6 addresses require more logic to parse than the simple IPv4 format. For instance, loopback can be represented as ::1
. Or runs of 0's can be expressed with adjacent colons, like: aaaa::1111
represents aaaa:0:0:0:0:0:0:1111
.
Membership Checks
To determine if an IP is within the range of IP's as defined by the IP and CIDR netmask bit specifier, it's unnecessary to calculate the beginning and end addresses if you apply the netmask as it's intended (as a mask). The two functions below are examples of how this is done for determining if an IPv4 address is a member of a CIDR notated network IP. And another showing an IPv6 test to determine if one subnet is within another.
Using the above as building blocks, we can construct custom functions for ipv4 or ipv6.:
def ipv4_cidr_member_of(ip1, ip2):
ip2, m = ip2.split('/')
return not any((a ^ b) & m
for a, b, m in
zip(ipv4_bytes(ip1),
ipv4_bytes(ip2),
ipv4_mask(m)))
def ipv6_cidr_subnet_of(ip1, ip2):
ip1, m1 = ip1.split('/')
ip2, m2 = ip2.split('/')
return int(m1) >= int(m2) and \
not any((a ^ b) & m
for a, b, m in
zip(ipv6_bytes(ip1),
ipv6_bytes(ip2),
ipv6_mask(m2)))
>>> ipv6_cidr_subnet_of('aaaa:bbbb:cccc:dddd:1100::/72',
... 'aaaa:bbbb:cccc:dddd::/64')
True
>>> ipv4_cidr_member_of('11.22.33.44', '11.22.33.0/24')
True
>>>
With this approach, comparisons generally involve XOR-ing two IP bytes, then AND-ing with the net mask. An IPv4 algorithm can be converted to IPv6 simply by changing the functions beginning with 'ipv4_
' to 'ipv6_
' and vice versa. The algorithms for either IPv4 or IPv6 are the same at this level using the building blocks.
Using the building blocks, custom functions could be created for things like determining if two CIDR notated IP addresses are both on the same network, or if one is within the same network as the other - that would be similar to the ...subnet_of()
function in logic.
Ranges
Keeping in mind that it's not necessary to calculate the ranges of a subnet to determine membership if you treat the mask as a true mask; if for whatever reason you want the range, the IP and netmask can be applied to get it in a similar way to the other examples above.
>>> def ipv4_cidr_range_bytes(ip):
... ip, m = ip.split('/')
... ip = list(ipv4_bytes(ip))
... m = list(ipv4_mask(m))
... start = [ b & m for b, m in zip(ip, m)]
... end = [(b | ~m) & 0xff for b, m in zip(ip, m)]
... return start, end
...
>>> ipv4_cidr_range_bytes('11.22.34.0/23')
([11, 22, 34, 0], [11, 22, 35, 255])
>>>
>>> # For IPv6, the above function could have been converted to look
>>> # just like it, but let's mix it up for fun with a single pass
>>> # over the data with zip(), then group into bytes objects with map()
>>>
>>> def ipv6_cidr_range_bytes(ip):
... ip, m = ip.split('/')
... s, e = map(lambda *x: bytes(x),
... *((b & m, (b | ~m) & 0xff)
... for b, m in zip(ipv6_bytes(ip),
... ipv6_mask(m))))
... return s, e
...
>>> ipv6_cidr_range_bytes('aaaa:bbbb:cccc:dddd:1100::/72')
(b'\xaa\xaa\xbb\xbb\xcc\xcc\xdd\xdd\x11\x00\x00\x00\x00\x00\x00\x00',
b'\xaa\xaa\xbb\xbb\xcc\xcc\xdd\xdd\x11\xff\xff\xff\xff\xff\xff\xff')
Efficiency
The functions appear to be slightly faster than using ipaddress
objects and methods:
>>> # Using the ipaddress module:
>>> timeit.timeit("a = ip_network('192.168.1.0/24'); "
"b = ip_network('192.168.1.128/30'); "
... "b.subnet_of(a)", globals=globals(), number=10**4)
0.2772132240352221
>>>
>>> # Using this code:
>>> timeit.timeit("ipv4_cidr_subnet_of('192.168.1.128/30', '192.168.1.0/24')",
... globals=globals(), number=10**4)
0.07261682399985148
>>>
Caching
If the same comparisons are repetitive in an application - the same IP's recur often, functools.lru_cache
can be used to decorate the functions and possibly gain some more efficiency:
from functools import lru_cache
@lru_cache
def ipv6_cidr_member_of(ip1, ip2):
ip1 = ipv6_bytes(ip1)
ip2, m = ip2.split('/')
ip2 = ipv6_bytes(ip2)
m = ipv6_mask(m)
return not any((a ^ b) & m for a, b, m in zip(ip1, ip2, m))
This caches the parameters and return values, so when the same ip1
is checked for membership again in ip2
, the cache quickly returns the last value calculated and the function body doesn't need to redo the operation.
>>> # Without caching:
>>> timeit.timeit("ipv6_cidr_member_of('aaaa:bbbb:cccc:dddd:11af:23af::',"
... "'aaaa:bbbb:cccc:dddd::/64')",
... globals=globals(), number=5)
0.00011115199959021993
>>> # 11.115199959021993e-05 <- the above time in sci. notation.
>>>
>>> # With caching (@lru_cach applied).
>>> timeit.timeit("ipv6_cidr_member_of('aaaa:bbbb:cccc:dddd:11af:23af::',"
... "'aaaa:bbbb:cccc:dddd::/64')",
... globals=globals(), number=5)
4.458599869394675e-05
This test just shows 5 cycles. The higher the ratio of cache hits to misses, the higher the gain in efficiency.