# -*- coding: utf-8 -*-
#
# Copyright (c) 2008-2013, Bryan Davis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# - Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# - Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
__all__ = (
'cidr2block',
'hex2ip',
'ip2hex',
'ip2long',
'ip2network',
'long2ip',
'netmask2prefix',
'subnet2block',
'validate_cidr',
'validate_ip',
'validate_netmask',
'validate_subnet',
'BENCHMARK_TESTS',
'BROADCAST',
'CURRENT_NETWORK',
'DUAL_STACK_LITE',
'IETF_PROTOCOL_RESERVED',
'IPV6_TO_IPV4_RELAY',
'LINK_LOCAL',
'LOCALHOST',
'LOOPBACK',
'MAX_IP',
'MIN_IP',
'MULTICAST',
'MULTICAST_INTERNETWORK',
'MULTICAST_LOCAL',
'PRIVATE_NETWORK_10',
'PRIVATE_NETWORK_172_16',
'PRIVATE_NETWORK_192_168',
'RESERVED',
'SHARED_ADDRESS_SPACE',
'TEST_NET_1',
'TEST_NET_2',
'TEST_NET_3',
)
import re
# sniff for python2.x / python3k compatibility "fixes'
try:
basestring = basestring
except NameError:
# 'basestring' is undefined, must be python3k
basestring = str
try:
bin = bin
except NameError:
# builtin bin function doesn't exist
def bin(x):
"""
From http://code.activestate.com/recipes/219300/#c7
"""
if x < 0:
return '-' + bin(-x)
out = []
if x == 0:
out.append('0')
while x > 0:
out.append('01'[x & 1])
x >>= 1
pass
try:
return '0b' + ''.join(reversed(out))
except NameError:
out.reverse()
return '0b' + ''.join(out)
#end bin
# end compatibility "fixes'
#: Regex for validating an IPv4 address
_DOTTED_QUAD_RE = re.compile(r'^(\d{1,3}\.){0,3}\d{1,3}$')
#: Regex for validating a CIDR network
_CIDR_RE = re.compile(r'^(\d{1,3}\.){0,3}\d{1,3}/\d{1,2}$')
#: Mamimum IPv4 integer
MAX_IP = 0xffffffff
#: Minimum IPv4 integer
MIN_IP = 0x0
#: Broadcast messages to the current network (only valid as source address)
#: (`RFC 5735 <https://tools.ietf.org/html/rfc5735>`_)
CURRENT_NETWORK = "0.0.0.0/8"
#: Private network
#: (`RFC 1918 <https://tools.ietf.org/html/rfc1918>`_)
PRIVATE_NETWORK_10 = "10.0.0.0/8"
#: Carrier-grade NAT private network
#: (`RFC 6598 <https://tools.ietf.org/html/rfc6598>`_)
SHARED_ADDRESS_SPACE = "100.64.0.0/10"
#: Loopback addresses on the local host
#: (`RFC 5735 <https://tools.ietf.org/html/rfc5735>`_)
LOOPBACK = "127.0.0.0/8"
#: Common `localhost` address
#: (`RFC 5735 <https://tools.ietf.org/html/rfc5735>`_)
LOCALHOST = "127.0.0.1"
#: Autoconfiguration when no IP address available
#: (`RFC 3972 <https://tools.ietf.org/html/rfc3972>`_)
LINK_LOCAL = "169.254.0.0/16"
#: Private network
#: (`RFC 1918 <https://tools.ietf.org/html/rfc1918>`_)
PRIVATE_NETWORK_172_16 = "172.16.0.0/12"
#: IETF protocol assignments reserved block
#: (`RFC 5735 <https://tools.ietf.org/html/rfc5735>`_)
IETF_PROTOCOL_RESERVED = "192.0.0.0/24"
#: Dual-Stack Lite link address
#: (`RFC 6333 <https://tools.ietf.org/html/rfc6333>`_)
DUAL_STACK_LITE = "192.0.0.0/29"
#: Documentation and example network
#: (`RFC 5737 <https://tools.ietf.org/html/rfc5737>`_)
TEST_NET_1 = "192.0.2.0/24"
#: 6to4 anycast relay
#: (`RFC 3068 <https://tools.ietf.org/html/rfc3068>`_)
IPV6_TO_IPV4_RELAY = "192.88.99.0/24"
#: Private network
#: (`RFC 1918 <https://tools.ietf.org/html/rfc1918>`_)
PRIVATE_NETWORK_192_168 = "192.168.0.0/16"
#: Inter-network communications testing
#: (`RFC 2544 <https://tools.ietf.org/html/rfc2544>`_)
BENCHMARK_TESTS = "198.18.0.0/15"
#: Documentation and example network
#: (`RFC 5737 <https://tools.ietf.org/html/rfc5737>`_)
TEST_NET_2 = "198.51.100.0/24"
#: Documentation and example network
#: (`RFC 5737 <https://tools.ietf.org/html/rfc5737>`_)
TEST_NET_3 = "203.0.113.0/24"
#: Multicast reserved block
#: (`RFC 5771 <https://tools.ietf.org/html/rfc5771>`_)
MULTICAST = "224.0.0.0/4"
#: Link local multicast
#: (`RFC 5771 <https://tools.ietf.org/html/rfc5771>`_)
MULTICAST_LOCAL = "224.0.0.0/24"
#: Forwardable multicast
#: (`RFC 5771 <https://tools.ietf.org/html/rfc5771>`_)
MULTICAST_INTERNETWORK = "224.0.1.0/24"
#: Former Class E address space. Reserved for future use
#: (`RFC 1700 <https://tools.ietf.org/html/rfc1700>`_)
RESERVED = "240.0.0.0/4"
#: Broadcast messages to the current network
#: (only valid as destination address)
#: (`RFC 919 <https://tools.ietf.org/html/rfc919>`_)
BROADCAST = "255.255.255.255"
[docs]def validate_ip(s):
"""Validate a dotted-quad ip address.
The string is considered a valid dotted-quad address if it consists of
one to four octets (0-255) seperated by periods (.).
>>> validate_ip('127.0.0.1')
True
>>> validate_ip('127.0')
True
>>> validate_ip('127.0.0.256')
False
>>> validate_ip(LOCALHOST)
True
>>> validate_ip(None) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: expected string or buffer
:param s: String to validate as a dotted-quad ip address.
:type s: str
:returns: ``True`` if a valid dotted-quad ip address, ``False`` otherwise.
:raises: TypeError
"""
if _DOTTED_QUAD_RE.match(s):
quads = s.split('.')
for q in quads:
if int(q) > 255:
return False
return True
return False
#end validate_ip
[docs]def validate_cidr(s):
"""Validate a CIDR notation ip address.
The string is considered a valid CIDR address if it consists of a valid
IPv4 address in dotted-quad format followed by a forward slash (/) and
a bit mask length (1-32).
>>> validate_cidr('127.0.0.1/32')
True
>>> validate_cidr('127.0/8')
True
>>> validate_cidr('127.0.0.256/32')
False
>>> validate_cidr('127.0.0.0')
False
>>> validate_cidr(LOOPBACK)
True
>>> validate_cidr('127.0.0.1/33')
False
>>> validate_cidr(None) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: expected string or buffer
:param s: String to validate as a CIDR notation ip address.
:type s: str
:returns: ``True`` if a valid CIDR address, ``False`` otherwise.
:raises: TypeError
"""
if _CIDR_RE.match(s):
ip, mask = s.split('/')
if validate_ip(ip):
if int(mask) > 32:
return False
else:
return False
return True
return False
#end validate_cidr
[docs]def validate_netmask(s):
"""Validate that a dotted-quad ip address is a valid netmask.
>>> validate_netmask('0.0.0.0')
True
>>> validate_netmask('128.0.0.0')
True
>>> validate_netmask('255.0.0.0')
True
>>> validate_netmask('255.255.255.255')
True
>>> validate_netmask(BROADCAST)
True
>>> validate_netmask('128.0.0.1')
False
:param s: String to validate as a dotted-quad notation netmask.
:type s: str
:returns: ``True`` if a valid netmask, ``False`` otherwise.
:raises: TypeError
"""
if validate_ip(s):
mask = bin(ip2network(s))[2:]
# all left most bits must be 1, all right most must be 0
seen0 = False
for c in mask:
if '1' == c:
if seen0:
return False
else:
seen0 = True
return True
else:
return False
#end validate_netmask
[docs]def validate_subnet(s):
"""Validate a dotted-quad ip address including a netmask.
The string is considered a valid dotted-quad address with netmask if it
consists of one to four octets (0-255) seperated by periods (.) followed
by a forward slash (/) and a subnet bitmask which is expressed in
dotted-quad format.
>>> validate_subnet('127.0.0.1/255.255.255.255')
True
>>> validate_subnet('127.0/255.0.0.0')
True
>>> validate_subnet('127.0/255')
True
>>> validate_subnet('127.0.0.256/255.255.255.255')
False
>>> validate_subnet('127.0.0.1/255.255.255.256')
False
>>> validate_subnet('127.0.0.0')
False
>>> validate_subnet(None)
Traceback (most recent call last):
...
TypeError: expected string or unicode
:param s: String to validate as a dotted-quad ip address with netmask.
:type s: str
:returns: ``True`` if a valid dotted-quad ip address with netmask,
``False`` otherwise.
:raises: TypeError
"""
if isinstance(s, basestring):
if '/' in s:
start, mask = s.split('/', 2)
return validate_ip(start) and validate_netmask(mask)
else:
return False
raise TypeError("expected string or unicode")
#end validate_subnet
[docs]def ip2long(ip):
"""Convert a dotted-quad ip address to a network byte order 32-bit
integer.
>>> ip2long('127.0.0.1')
2130706433
>>> ip2long('127.1')
2130706433
>>> ip2long('127')
2130706432
>>> ip2long('127.0.0.256') is None
True
:param ip: Dotted-quad ip address (eg. '127.0.0.1').
:type ip: str
:returns: Network byte order 32-bit integer or ``None`` if ip is invalid.
"""
if not validate_ip(ip):
return None
quads = ip.split('.')
if len(quads) == 1:
# only a network quad
quads = quads + [0, 0, 0]
elif len(quads) < 4:
# partial form, last supplied quad is host address, rest is network
host = quads[-1:]
quads = quads[:-1] + [0, ] * (4 - len(quads)) + host
lngip = 0
for q in quads:
lngip = (lngip << 8) | int(q)
return lngip
#end ip2long
[docs]def ip2network(ip):
"""Convert a dotted-quad ip to base network number.
This differs from :func:`ip2long` in that partial addresses as treated as
all network instead of network plus host (eg. '127.1' expands to
'127.1.0.0')
:param ip: dotted-quad ip address (eg. ‘127.0.0.1’).
:type ip: str
:returns: Network byte order 32-bit integer or `None` if ip is invalid.
"""
if not validate_ip(ip):
return None
quads = ip.split('.')
netw = 0
for i in range(4):
netw = (netw << 8) | int(len(quads) > i and quads[i] or 0)
return netw
#end ip2network
[docs]def long2ip(l):
"""Convert a network byte order 32-bit integer to a dotted quad ip
address.
>>> long2ip(2130706433)
'127.0.0.1'
>>> long2ip(MIN_IP)
'0.0.0.0'
>>> long2ip(MAX_IP)
'255.255.255.255'
>>> long2ip(None) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for >>: 'NoneType' and 'int'
>>> long2ip(-1) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: expected int between 0 and 4294967295 inclusive
>>> long2ip(374297346592387463875) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: expected int between 0 and 4294967295 inclusive
>>> long2ip(MAX_IP + 1) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: expected int between 0 and 4294967295 inclusive
:param l: Network byte order 32-bit integer.
:type l: int
:returns: Dotted-quad ip address (eg. '127.0.0.1').
:raises: TypeError
"""
if MAX_IP < l or l < MIN_IP:
raise TypeError(
"expected int between %d and %d inclusive" % (MIN_IP, MAX_IP))
return '%d.%d.%d.%d' % (
l >> 24 & 255, l >> 16 & 255, l >> 8 & 255, l & 255)
#end long2ip
[docs]def ip2hex(addr):
"""Convert a dotted-quad ip address to a hex encoded number.
>>> ip2hex('0.0.0.1')
'00000001'
>>> ip2hex('127.0.0.1')
'7f000001'
>>> ip2hex('127.255.255.255')
'7fffffff'
>>> ip2hex('128.0.0.1')
'80000001'
>>> ip2hex('128.1')
'80000001'
>>> ip2hex('255.255.255.255')
'ffffffff'
:param addr: Dotted-quad ip address.
:type addr: str
:returns: Numeric ip address as a hex-encoded string or ``None`` if
invalid.
"""
netip = ip2long(addr)
if netip is None:
return None
return "%08x" % netip
#end ip2hex
[docs]def hex2ip(hex_str):
"""Convert a hex encoded integer to a dotted-quad ip address.
>>> hex2ip('00000001')
'0.0.0.1'
>>> hex2ip('7f000001')
'127.0.0.1'
>>> hex2ip('7fffffff')
'127.255.255.255'
>>> hex2ip('80000001')
'128.0.0.1'
>>> hex2ip('ffffffff')
'255.255.255.255'
:param hex_str: Numeric ip address as a hex-encoded string.
:type hex_str: str
:returns: Dotted-quad ip address or ``None`` if invalid.
"""
try:
netip = int(hex_str, 16)
except ValueError:
return None
return long2ip(netip)
#end hex2ip
[docs]def cidr2block(cidr):
"""Convert a CIDR notation ip address into a tuple containing the network
block start and end addresses.
>>> cidr2block('127.0.0.1/32')
('127.0.0.1', '127.0.0.1')
>>> cidr2block('127/8')
('127.0.0.0', '127.255.255.255')
>>> cidr2block('127.0.1/16')
('127.0.0.0', '127.0.255.255')
>>> cidr2block('127.1/24')
('127.1.0.0', '127.1.0.255')
>>> cidr2block('127.0.0.3/29')
('127.0.0.0', '127.0.0.7')
>>> cidr2block('127/0')
('0.0.0.0', '255.255.255.255')
:param cidr: CIDR notation ip address (eg. '127.0.0.1/8').
:type cidr: str
:returns: Tuple of block (start, end) or ``None`` if invalid.
:raises: TypeError
"""
if not validate_cidr(cidr):
return None
ip, prefix = cidr.split('/')
prefix = int(prefix)
# convert dotted-quad ip to base network number
network = ip2network(ip)
return _block_from_ip_and_prefix(network, prefix)
#end cidr2block
[docs]def netmask2prefix(mask):
"""Convert a dotted-quad netmask into a CIDR prefix.
>>> netmask2prefix('255.0.0.0')
8
>>> netmask2prefix('255.128.0.0')
9
>>> netmask2prefix('255.255.255.254')
31
>>> netmask2prefix('255.255.255.255')
32
>>> netmask2prefix('0.0.0.0')
0
>>> netmask2prefix('127.0.0.1')
0
:param mask: Netmask in dotted-quad notation.
:type mask: str
:returns: CIDR prefix corresponding to netmask or `0` if invalid.
"""
if validate_netmask(mask):
return bin(ip2network(mask)).count('1')
return 0
#end netmask2prefix
[docs]def subnet2block(subnet):
"""Convert a dotted-quad ip address including a netmask into a tuple
containing the network block start and end addresses.
>>> subnet2block('127.0.0.1/255.255.255.255')
('127.0.0.1', '127.0.0.1')
>>> subnet2block('127/255')
('127.0.0.0', '127.255.255.255')
>>> subnet2block('127.0.1/255.255')
('127.0.0.0', '127.0.255.255')
>>> subnet2block('127.1/255.255.255.0')
('127.1.0.0', '127.1.0.255')
>>> subnet2block('127.0.0.3/255.255.255.248')
('127.0.0.0', '127.0.0.7')
>>> subnet2block('127/0')
('0.0.0.0', '255.255.255.255')
:param subnet: dotted-quad ip address with netmask
(eg. '127.0.0.1/255.0.0.0').
:type subnet: str
:returns: Tuple of block (start, end) or ``None`` if invalid.
:raises: TypeError
"""
if not validate_subnet(subnet):
return None
ip, netmask = subnet.split('/')
prefix = netmask2prefix(netmask)
# convert dotted-quad ip to base network number
network = ip2network(ip)
return _block_from_ip_and_prefix(network, prefix)
#end subnet2block
def _block_from_ip_and_prefix(ip, prefix):
"""Create a tuple of (start, end) dotted-quad addresses from the given
ip address and prefix length.
:param ip: Ip address in block
:type ip: long
:param prefix: Prefix size for block
:type prefix: int
:returns: Tuple of block (start, end)
"""
# keep left most prefix bits of ip
shift = 32 - prefix
block_start = ip >> shift << shift
# expand right most 32 - prefix bits to 1
mask = (1 << shift) - 1
block_end = block_start | mask
return (long2ip(block_start), long2ip(block_end))
#end _block_from_ip_and_prefix
# vim: set sw=4 ts=4 sts=4 et :