I know there are some similar questions up here, but they mostly either want to find the range itself (which uses some libraries, like the example that stackoverflow says is
This doesn't work in general, because string comparison is in collating order, not the numerical values of the four fields. For instance, '1.1.2.2' > '1.1.128.1' -- the critical spot in the 5th character, '1' vs '2'.
If you want to compare the fields, try separating into lists:
ip_vals = [int(x) for x in ip_range.split('.')]
ip_vals is now a list of the values; you can compare the lists and get the results I think you want.
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:ffffdd:1100::/72',
... 'aaaa:bbbb:cccc:ffffdd::/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:ffffdd: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:ffffdd:11af:23af::',"
... "'aaaa:bbbb:cccc:ffffdd::/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:ffffdd:11af:23af::',"
... "'aaaa:bbbb:cccc:ffffdd::/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.
You can't really do string comparisons on a dot separated list of numbers because your test will simply fail on input say 1.1.99.99
as '9'
is simply greater than '2'
>>> '1.1.99.99' < '1.1.255.255'
False
So instead you can convert the input into tuples of integers through comprehension expression
def convert_ipv4(ip):
return tuple(int(n) for n in ip.split('.'))
Note the lack of type checking, but if your input is a proper IP address it will be fine. Since you have a 2-tuple of IP addresses, you can create a function that takes both start and end as argument, pass that tuple in through argument list, and return that with just one statement (as Python allows chaining of comparisons). Perhaps like:
def check_ipv4_in(addr, start, end):
return convert_ipv4(start) < convert_ipv4(addr) < convert_ipv4(end)
Test it out.
>>> ip_range = ('1.1.0.0', '1.1.255.255')
>>> check_ipv4_in('1.1.99.99', *ip_range)
True
With this method you can lazily expand it to IPv6, though the conversion to and from hex (instead of int) will be needed instead.
Your code compares strings, not numbers. I would suggest using tuples instead:
>>> ip_range = [(1,1,0,0), (1,1,255,255)]
>>> testip = (1,1,2,2)
>>> testip > ip_range[0] and testip < ip_range[1]
True
>>> testip = (1,3,1,1)
>>> testip > ip_range[0] and testip < ip_range[1]
False
For python 2 & 3
use:
from ipaddress import ip_network, ip_address
def in_cidr(ip, cidr):
return ip_address(ip) in ip_network(cidr)
Demo
For pyhton 2.7
install using:
pip install ipaddress
In Python 3.3 and later, you should be using the ipaddress
module.
from ipaddress import ip_network, ip_address
net = ip_network("1.1.0.0/16")
print(ip_address("1.1.2.2") in net) # True