Skip to content

Commit 342905f

Browse files
committed
Add support for IPv6 in IpRange and IpRangeList.
1 parent b77e11a commit 342905f

3 files changed

Lines changed: 302 additions & 78 deletions

File tree

iptools/__init__.py

Lines changed: 62 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
'IpRangeList',
3131
)
3232

33+
3334
# sniff for python2.x / python3k compatibility "fixes'
3435
try:
3536
basestring = basestring
@@ -49,10 +50,22 @@ def next(iterable):
4950
except ImportError:
5051
# python <2.6 doesn't have abc classes to extend
5152
Sequence = object
52-
5353
# end compatibility "fixes'
5454

55+
5556
from . import ipv4
57+
from . import ipv6
58+
59+
60+
def _address2long(address):
61+
"""
62+
Convert an address string to a long.
63+
"""
64+
parsed = ipv4.ip2long(address)
65+
if parsed is None:
66+
parsed = ipv6.ip2long(address)
67+
return parsed
68+
#end _addess2long
5669

5770

5871
class IpRange (Sequence):
@@ -71,6 +84,10 @@ class IpRange (Sequence):
7184
False
7285
>>> 2130706433 in r
7386
True
87+
>>> '::ffff:192.0.2.128' in r
88+
False
89+
>>> '::ffff:c000:0280' in r
90+
False
7491
>>> r = IpRange('127/24')
7592
>>> print(r)
7693
('127.0.0.0', '127.0.0.255')
@@ -86,6 +103,21 @@ class IpRange (Sequence):
86103
>>> r = IpRange('127/255.255.255.0')
87104
>>> print(r)
88105
('127.0.0.0', '127.0.0.255')
106+
>>> r = IpRange('::ffff:0000:0000', '::ffff:ffff:ffff')
107+
>>> '::ffff:192.0.2.128' in r
108+
True
109+
>>> '::ffff:c000:0280' in r
110+
True
111+
>>> 281473902969472 in r
112+
True
113+
>>> '192.168.2.128' in r
114+
False
115+
>>> 2130706433 in r
116+
False
117+
>>> r = IpRange('::ffff:ffff:0000/120')
118+
>>> for ip in r:
119+
... print(ip) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
120+
::ffff:ffff:0 ... ::ffff:ffff:6d ... ::ffff:ffff:ff
89121
90122
91123
:param start: Ip address in dotted quad format, CIDR notation, subnet
@@ -108,6 +140,10 @@ def __init__(self, start, end=None):
108140
# CIDR notation range
109141
start, end = ipv4.cidr2block(start)
110142

143+
elif ipv6.validate_cidr(start):
144+
# CIDR notation range
145+
start, end = ipv6.cidr2block(start)
146+
111147
elif ipv4.validate_subnet(start):
112148
# Netmask notation range
113149
start, end = ipv4.subnet2block(start)
@@ -116,11 +152,15 @@ def __init__(self, start, end=None):
116152
# degenerate range
117153
end = start
118154

119-
start = ipv4.ip2long(start)
120-
end = ipv4.ip2long(end)
155+
start = _address2long(start)
156+
end = _address2long(end)
121157
self.startIp = min(start, end)
122158
self.endIp = max(start, end)
123159
self._len = self.endIp - self.startIp + 1
160+
161+
self._ipver = ipv4
162+
if self.endIp > ipv4.MAX_IP:
163+
self._ipver = ipv6
124164
#end __init__
125165

126166
def __repr__(self):
@@ -133,7 +173,8 @@ def __repr__(self):
133173
"IpRange('127.0.0.0', '127.0.0.255')"
134174
"""
135175
return "IpRange(%r, %r)" % (
136-
ipv4.long2ip(self.startIp), ipv4.long2ip(self.endIp))
176+
self._ipver.long2ip(self.startIp),
177+
self._ipver.long2ip(self.endIp))
137178
#end __repr__
138179

139180
def __str__(self):
@@ -146,7 +187,8 @@ def __str__(self):
146187
"('127.0.0.0', '127.0.0.255')"
147188
"""
148189
return (
149-
ipv4.long2ip(self.startIp), ipv4.long2ip(self.endIp)).__repr__()
190+
self._ipver.long2ip(self.startIp),
191+
self._ipver.long2ip(self.endIp)).__repr__()
150192
#end __str__
151193

152194
def __eq__(self, other):
@@ -195,10 +237,10 @@ def __hash__(self):
195237

196238
def _cast(self, item):
197239
if isinstance(item, basestring):
198-
item = ipv4.ip2long(item)
199-
if type(item) not in [type(1), type(ipv4.MAX_IP)]:
240+
item = _address2long(item)
241+
if type(item) not in [type(1), type(ipv4.MAX_IP), type(ipv6.MAX_IP)]:
200242
raise TypeError(
201-
"expected dotted-quad ip address or 32-bit integer")
243+
"expected ip address, 32-bit integer or 128-bit integer")
202244
return item
203245
#end _cast
204246

@@ -226,7 +268,7 @@ def index(self, item):
226268
offset = item - self.startIp
227269
if offset >= 0 and offset < self._len:
228270
return offset
229-
raise ValueError('%s is not in range' % ipv4.long2ip(item))
271+
raise ValueError('%s is not in range' % self._ipver.long2ip(item))
230272
#end index
231273

232274
def count(self, item):
@@ -249,7 +291,7 @@ def __contains__(self, item):
249291
>>> 'invalid' in r
250292
Traceback (most recent call last):
251293
...
252-
TypeError: expected dotted-quad ip address or 32-bit integer
294+
TypeError: expected ip address, 32-bit integer or 128-bit integer
253295
254296
255297
:param item: Dotted-quad ip address.
@@ -307,15 +349,15 @@ def __getitem__(self, index):
307349
if stop > self._len:
308350
raise IndexError('stop index out of range')
309351
return IpRange(
310-
ipv4.long2ip(self.startIp + start),
311-
ipv4.long2ip(self.startIp + stop - 1))
352+
self._ipver.long2ip(self.startIp + start),
353+
self._ipver.long2ip(self.startIp + stop - 1))
312354

313355
else:
314356
if index < 0:
315357
index = self._len + index
316358
if index < 0 or index >= self._len:
317359
raise IndexError('index out of range')
318-
return ipv4.long2ip(self.startIp + index)
360+
return self._ipver.long2ip(self.startIp + index)
319361
#end __getitem__
320362

321363
def __iter__(self):
@@ -335,7 +377,7 @@ def __iter__(self):
335377
"""
336378
i = self.startIp
337379
while i <= self.endIp:
338-
yield ipv4.long2ip(i)
380+
yield self._ipver.long2ip(i)
339381
i += 1
340382
#end __iter__
341383
#end class IpRange
@@ -345,29 +387,12 @@ class IpRangeList (object):
345387
"""
346388
List of IpRange objects.
347389
348-
Converts a list of dotted quad ip address and/or CIDR addresses into a
349-
list of IpRange objects. This list can perform ``in`` and ``not in`` tests
350-
and iterate all of the addresses in the range.
351-
352-
This can be used to convert django's conf.settings.INTERNAL_IPS list into
353-
a smart object which allows CIDR notation.
354-
355-
356-
>>> INTERNAL_IPS = IpRangeList(
357-
... '127.0.0.1','10/8',('192.168.0.1','192.168.255.255'))
358-
>>> '127.0.0.1' in INTERNAL_IPS
359-
True
360-
>>> '10.10.10.10' in INTERNAL_IPS
361-
True
362-
>>> '192.168.192.168' in INTERNAL_IPS
363-
True
364-
>>> '172.16.0.1' in INTERNAL_IPS
365-
False
366-
390+
Converts a list of ip address and/or CIDR addresses into a list of IpRange
391+
objects. This list can perform ``in`` and ``not in`` tests and iterate all
392+
of the addresses in the range.
367393
368-
:param \*args: List of ip addresses in dotted quad format or CIDR
369-
notation and/or ``(start, end)`` tuples of ip addresses in dotted quad
370-
format.
394+
:param \*args: List of ip addresses or CIDR notation and/or
395+
``(start, end)`` tuples of ip addresses.
371396
:type \*args: list of str and/or tuple
372397
"""
373398
def __init__(self, *args):
@@ -420,7 +445,7 @@ def __contains__(self, item):
420445
>>> 'invalid' in r
421446
Traceback (most recent call last):
422447
...
423-
TypeError: expected dotted-quad ip address or 32-bit integer
448+
TypeError: expected ip address, 32-bit integer or 128-bit integer
424449
425450
426451
:param item: Dotted-quad ip address.

iptools/ipv4.py

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,47 @@
2424
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
2525
# POSSIBILITY OF SUCH DAMAGE.
2626

27-
# __all__ = (
28-
# 'cidr2block',
29-
# 'hex2ip',
30-
# 'ip2hex',
31-
# 'ip2long',
32-
# 'ip2network',
33-
# 'long2ip',
34-
# 'netmask2prefix',
35-
# 'subnet2block',
36-
# 'validate_cidr',
37-
# 'validate_ip',
38-
# 'validate_netmask',
39-
# 'validate_subnet',
40-
# 'BENCHMARK_TESTS',
41-
# 'BROADCAST',
42-
# 'CURRENT_NETWORK',
43-
# 'DUAL_STACK_LITE',
44-
# 'IETF_PROTOCOL_RESERVED',
45-
# 'IPV6_TO_IPV4_RELAY',
46-
# 'LINK_LOCAL',
47-
# 'LOCALHOST',
48-
# 'LOOPBACK',
49-
# 'MAX_IP',
50-
# 'MIN_IP',
51-
# 'MULTICAST',
52-
# 'MULTICAST_INTERNETWORK',
53-
# 'MULTICAST_LOCAL',
54-
# 'PRIVATE_NETWORK_10',
55-
# 'PRIVATE_NETWORK_172_16',
56-
# 'PRIVATE_NETWORK_192_168',
57-
# 'RESERVED',
58-
# 'SHARED_ADDRESS_SPACE',
59-
# 'TEST_NET_1',
60-
# 'TEST_NET_2',
61-
# 'TEST_NET_3',
62-
# )
27+
__all__ = (
28+
'cidr2block',
29+
'hex2ip',
30+
'ip2hex',
31+
'ip2long',
32+
'ip2network',
33+
'long2ip',
34+
'netmask2prefix',
35+
'subnet2block',
36+
'validate_cidr',
37+
'validate_ip',
38+
'validate_netmask',
39+
'validate_subnet',
40+
'BENCHMARK_TESTS',
41+
'BROADCAST',
42+
'CURRENT_NETWORK',
43+
'DUAL_STACK_LITE',
44+
'IETF_PROTOCOL_RESERVED',
45+
'IPV6_TO_IPV4_RELAY',
46+
'LINK_LOCAL',
47+
'LOCALHOST',
48+
'LOOPBACK',
49+
'MAX_IP',
50+
'MIN_IP',
51+
'MULTICAST',
52+
'MULTICAST_INTERNETWORK',
53+
'MULTICAST_LOCAL',
54+
'PRIVATE_NETWORK_10',
55+
'PRIVATE_NETWORK_172_16',
56+
'PRIVATE_NETWORK_192_168',
57+
'RESERVED',
58+
'SHARED_ADDRESS_SPACE',
59+
'TEST_NET_1',
60+
'TEST_NET_2',
61+
'TEST_NET_3',
62+
)
63+
6364

6465
import re
6566

67+
6668
# sniff for python2.x / python3k compatibility "fixes'
6769
try:
6870
basestring = basestring
@@ -93,9 +95,9 @@ def bin(x):
9395
out.reverse()
9496
return '0b' + ''.join(out)
9597
#end bin
96-
9798
# end compatibility "fixes'
9899

100+
99101
#: Regex for validating an IPv4 address
100102
_DOTTED_QUAD_RE = re.compile(r'^(\d{1,3}\.){0,3}\d{1,3}$')
101103

@@ -227,9 +229,9 @@ def validate_ip(s):
227229
def validate_cidr(s):
228230
"""Validate a CIDR notation ip address.
229231
230-
The string is considered a valid CIDR address if it consists of one to
231-
four octets (0-255) seperated by periods (.) followed by a forward slash
232-
(/) and a bit mask length (1-32).
232+
The string is considered a valid CIDR address if it consists of a valid
233+
IPv4 address in dotted-quad format followed by a forward slash (/) and
234+
a bit mask length (1-32).
233235
234236
235237
>>> validate_cidr('127.0.0.1/32')
@@ -242,6 +244,8 @@ def validate_cidr(s):
242244
False
243245
>>> validate_cidr(LOOPBACK)
244246
True
247+
>>> validate_cidr('127.0.0.1/33')
248+
False
245249
>>> validate_cidr(None) #doctest: +IGNORE_EXCEPTION_DETAIL
246250
Traceback (most recent call last):
247251
...
@@ -439,7 +443,7 @@ def long2ip(l):
439443
:returns: Dotted-quad ip address (eg. '127.0.0.1').
440444
:raises: TypeError
441445
"""
442-
if MAX_IP < l or l < 0:
446+
if MAX_IP < l or l < MIN_IP:
443447
raise TypeError(
444448
"expected int between %d and %d inclusive" % (MIN_IP, MAX_IP))
445449
return '%d.%d.%d.%d' % (

0 commit comments

Comments
 (0)