chiark / gitweb /
Import release 0.1.3
[secnet.git] / ipaddr.py
diff --git a/ipaddr.py b/ipaddr.py
new file mode 100644 (file)
index 0000000..83f5a17
--- /dev/null
+++ b/ipaddr.py
@@ -0,0 +1,1243 @@
+# ipaddr.py -- handle IP addresses and set of IP addresses.
+# Copyright (C) 1996-2000 Cendio Systems AB
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""IP address manipulation.
+
+This module is useful if you need to manipulate IP addresses or sets
+of IP addresses.
+
+The main classes are:
+
+    ipaddr   -- a single IP address.
+    netmask  -- a netmask.
+    network  -- an IP address/netmask combination.  It is often, but
+               not always, better to use the ip_set class instead.
+    ip_set   -- a set of IP addresses, that may or may not be adjacent.
+
+So, what can you do with this module?  As a simple example of the kind
+of things this module can do, this code computes the set of all IP
+addresses except 127.0.0.0/8 and prints it, expressed as a union of
+network/netmask pairs.
+
+    import ipaddr
+
+    s = ipaddr.ip_set()
+    s.add_network(ipaddr.network('127.0.0.0', '255.0.0.0',
+                                 ipaddr.DEMAND_FILTER))
+    for nw in s.complement().as_list_of_networks():
+       print nw.ip_str() + '/' + nw.mask.netmask_bits_str
+
+Errors are reported by raising an exception from the following
+exception hierarcy:
+
+Exception       # The standard Python base exception class.
+ |
+ +-- BadType    # Only raised if the programmer makes an error.
+ +-- IpError    # Base class for errors that depend on the data.
+      |
+      +-- SetNotRepresentable
+      +-- BrokenIpAddress
+      |    |
+      |    +-- PartNegative
+      |    +-- PartOverflow
+      |
+      +-- BrokenNetmask
+      |    |
+      |    +-- NeedOneBit
+      |    +-- NeedMoreBits
+      |    +-- NeedLessBits
+      |
+      +-- BrokenNetwork
+           |
+           +-- EmptyIpAddress
+           +-- EmptyNetmask
+           +-- BrokenNetworkAddress
+           +-- NetworkAddressClash
+           +-- BroadcastAddressClash
+  
+BadType may be raised at any time if the programmer makes an error
+(such as passing a dictionary to a function that expects a string).
+SetNotRepresentable may be raised by ip_set.as_str_range().  All other
+exceptions are raised from the constructors and helper functions only.
+
+The following constants are present in this module:
+
+    DEMAND_NONE                See class network.
+    DEMAND_FILTER      See class network.
+    DEMAND_NETWORK     See class network.
+    DEMAND_INTERFACE   See class network.
+
+    hostmask           A netmask object with all 32 bits set.
+    complete_network   A network object representing all IP addresses.
+    complete_set       An ip_set object representing all IP addresses.
+    broadcast_network  A network object representing 255.255.255.255.
+    broadcast_set      An ip_set object representing 255.255.255.255.
+    
+The as_ipaddr function can be used when you have an object that you
+know are an ipaddr or network, and you want to get the ipaddr part.
+
+All the other functions in this module are internal helper functions,
+and they should not be used.
+
+The internal representation used for IP addresses is currently a long
+number.  That may change in the future, so where the internal
+representation is visible, you should do nothing with it except
+compare it to None.
+
+This module was developed by Cendio Systems AB for use in the Fuego
+Firewall.  Bug reports can be sent to Per Cederqvist <ceder@cendio.se>
+who is currently acting as maintainer for this module.
+
+Brief history:
+    1997-03-11      Module created, and used internally.
+    2000-03-09 1.0: First non-public beta release outside of Cendio Systems.
+    2000-03-17 1.1: First public release under the GNU GPL license.
+
+"""
+
+
+import copy
+import string
+import types
+
+# The error messages are marked with a call to this function, so that
+# they can easily be found and translated.
+def _(s):
+    return s
+
+# The exception hierarchy.
+class IpError(Exception):
+    """Base class for errors that are cause by errors in input data.
+    """
+    def __str__(self):
+       return self.format % self.args
+
+class SetNotRepresentable(IpError):
+    format = _("The set of IP addresses cannot be represented "
+              "as a single network range")
+
+class BrokenIpAddress(IpError):
+    format = _("Felaktigt IP-nummer")
+
+class PartNegative(BrokenIpAddress):
+    format = _("En komponent i IP-numret är negativ")
+
+class PartOverflow(BrokenIpAddress):
+    format = _("En komponent i IP-numret är större än 255")
+
+class BrokenNetmask(IpError):
+    format = _("Felaktig nätmask")
+
+class NeedOneBit(BrokenNetmask):
+    format = _("Minst en bit måste vara ettställd")
+
+class NeedMoreBits(BrokenNetmask):
+    format = _("Minst %d bitar måste vara ettställda")
+
+class NeedLessBits(BrokenNetmask):
+    format = _("Högst %d bitar får vara ettställda")
+
+class BrokenNetwork(IpError):
+    """Base class for errors regarding network objects.
+    """
+
+class EmptyIpAddress(BrokenNetwork):
+    format = _("IP-nummer ej ifyllt")
+
+class EmptyNetmask(BrokenNetwork):
+    format = _("Nätmask ej ifylld")
+
+class BrokenNetworkAddress(BrokenNetwork):
+    format = _("Med denna nätmask är %s ett otillåtet nätverksnummer; "
+              "menar du %s?")
+
+class NetworkAddressClash(BrokenNetwork):
+    format = _("Med denna nätmask krockar Fuegons adress med nätnumret")
+
+class BroadcastAddressClash(BrokenNetwork):
+    format = _("Med denna nätmask krockar Fuegons adress "
+              "med broadcastadressen")
+
+class BadType(Exception):
+    """An object of an unexpected type was passed to a function.
+    """
+    pass
+
+# These constants are used with netmasks and networks to specify what
+# the code expects.
+#
+#  DEMAND_NONE: netmask 0-32 (inclusive)
+#  DEMAND_FILTER: netmask 0-32, the host part must be all zeroes
+#  DEMAND_NETWORK: netmask 1-32, the host part must be all zeroes
+#  DEMAND_INTERFACE: netmask 1-30, the host part must *not* be all zeroes
+
+DEMAND_NONE = 1
+DEMAND_FILTER = 2
+DEMAND_NETWORK = 3
+DEMAND_INTERFACE = 4
+
+def bits_to_intrep(bits):
+    """Convert BITS to the internal representation.
+
+    BITS should be a number in the range 0-32 (inclusive).
+
+    """
+    return pow(2L, 32) - pow(2L, 32-bits)
+
+
+def intrep_with_bit_set(bit):
+    """Return an internal representation with bit BIT set.
+
+    BIT should be a number in the range 1-32, where bit 1 is the
+    leftmost.  Examples:
+
+      intrep_with_bit_set(1) --> the internal representation of 128.0.0.0
+      intrep_with_bit_set(32) --> the internal representation of 0.0.0.1
+    """
+    assert 0 < bit and bit <= 32
+
+    return pow(2L, 32-bit)
+
+
+__ONES = {0:0, 128:1, 192:2, 224:3,
+         240:4, 248:5, 252:6, 254:7}
+
+def tuple_to_bits(mask):
+    """Convert MASK to bits.
+
+    MASK should be a tuple of four integers in the range 0-255 (inclusive).
+
+    Raises BrokenNetmask if MASK is not a valid netmask.
+    """
+
+    if mask == None:
+       return None
+    else:
+       (a, b, c, d) = mask
+
+       if a == 255 and b == 255 and c == 255 and d == 255:
+           return 32
+
+       try:
+           if a == 255 and b == 255 and c == 255:
+               return 24 + __ONES[d]
+           elif a == 255 and b == 255  and d == 0:
+               return 16 + __ONES[c]
+           elif a == 255 and c == 0  and d == 0:
+               return 8 + __ONES[b]
+           elif b == 0 and c == 0  and d == 0:
+               return __ONES[a]
+       except KeyError:
+           pass
+
+       raise BrokenNetmask()
+
+
+def intrep_to_dotted_decimal(t):
+    """Convert T to dotted-decimal notation.
+
+    T should be the internal representation used py ipaddr.py.
+    """
+
+    return (str(int(t>>24)) + '.' + str(int((t>>16) & 255))
+           + '.' + str(int((t>>8) & 255)) + '.' + str(int(t & 255)))
+
+
+def as_ipaddr(nwip):
+    """Return the IP address object of NWIP.
+
+    NWIP may be an ipaddr object, which is returned unchanged,
+    or a network object, in which case the ipaddr part of it is
+    returned.
+    """
+
+    if isinstance(nwip, ipaddr):
+       return nwip
+    elif isinstance(nwip, network):
+       return nwip.ip
+    else:
+       raise BadType('Expected a network or ipaddr object', nwip)
+
+
+class ipaddr:
+    """Handle IP addresses.
+
+    Sample use:
+
+        ip1 = ipaddr('12.3.5.1')
+       ip2 = ipaddr([12, 3, 5, 1])
+       print ip1.ip_str()
+       >>> '12.3.5.1'
+       print ip1.intrep
+       >>> 201524481L
+       print ip2.ip_str()
+       >>> '12.3.5.1'
+       print ip2.intrep
+       >>> 201524481L
+
+    An ipaddr object can have two states: empty or good.
+    The status can be examined like this:
+
+       if ip.intrep == None:
+           handle_empty(m.user_input())
+       else:
+           handle_good(ip)
+
+    All other members should only be used in the good state.  The
+    value stored in the intrep member should only be compared against
+    None.  The type and value of it is an internal detail that may
+    change in the future.
+
+    """
+
+    def __init__(self, ip):
+       """Create an ipaddr from IP (a string, tuple or list).
+
+       The empty string or None may be given; it is handled as the
+       empty IP number.
+       """
+
+       if type(ip) == types.StringType:
+           self.__user_input = ip
+           ip = string.strip(ip)
+       else:
+           self.__user_input = None
+
+       # The empty IP number?
+
+       if ip == '' or ip == None:
+           self.__ip_str = ''
+           self.intrep = None
+           if ip == None:
+               self.__user_input = ''
+           return
+
+       if type(ip) == types.StringType:
+
+           # Convert a string.
+
+           try:
+               [a, b, c, d] = map(string.atoi, string.splitfields(ip, '.'))
+           except:
+               raise BrokenIpAddress()
+
+           if a < 0 or b < 0 or c < 0 or d < 0:
+               raise PartNegative()
+
+           if a > 255 or b > 255 or c > 255 or d > 255:
+               raise PartOverflow()
+
+           self.intrep = (long(a) << 24) + (b << 16) + (c << 8) + d
+
+       else:
+           assert type(ip) == types.LongType
+           self.intrep = ip
+
+       self.__ip_str = None
+
+    def ip_str(self):
+       if self.__ip_str == None:
+           self.__ip_str = intrep_to_dotted_decimal(self.intrep)
+       return self.__ip_str
+
+    def user_input(self):
+       if self.__user_input == None:
+           # This object was constructed from a tuple.  Generate a string.
+           self.__user_input = self.ip_str()
+       return self.__user_input
+
+    def compare(self, other):
+       """Compare this IP address with OTHER.
+
+       Returns -1, 0 or 1 if this IP address is less than, equal to,
+       or greater than OTHER (which should be an ipaddr object).
+       """
+       # FIXME: should we rename this __cmp__?  It needs to handle
+       # other types of the OTHER argument first.
+
+       if self.intrep == other.intrep:
+           return 0
+       if self.intrep < other.intrep:
+           return -1
+       else:
+           return 1
+
+    def __str__(self):
+        if self.intrep is None:
+            return "<ipaddr empty>"
+        else:
+            return "<ipaddr %s>" % self.ip_str()
+
+    def __repr__(self):
+       if self.intrep is None:
+           return "ipaddr.ipaddr('')"
+       else:
+           return "ipaddr.ipaddr('%s')" % self.ip_str()
+
+
+class netmask:
+    """Handle netmasks.
+
+    Sample use:
+
+       # Four ways to initialize a netmask.
+        nm1 = netmask('255.255.128.0', DEMAND_NONE)
+       nm2 = netmask([255, 255, 128, 0], DEMAND_NONE)
+       nm3 = netmask('17', DEMAND_NONE)
+       nm4 = netmask(17, DEMAND_NONE)
+       print nm1.netmask_str()
+       >>> '255.255.128.0'
+       print nm1.intrep
+       >>> (255, 255, 128, 0)
+       print nm1.netmask_bits
+       >>> 17
+       print nm1.netmask_bits_str
+       >>> '17'
+
+    A netmask can have two states: empty or good.  The state
+    can be examined like this:
+
+       if m.intrep == None:
+           handle_empty(m.user_input())
+       else:
+           handle_good(m)
+
+    All other members should be used only in the good state.
+
+    """
+
+    def __check_range(self, bits, minbits, maxbits):
+       if bits < minbits:
+           if minbits == 1:
+               raise NeedOneBit()
+           else:
+               raise NeedMoreBits(minbits)
+       elif bits > maxbits:
+           raise NeedLessBits(maxbits)
+
+
+    def __set_from_bits(self, bits, minbits, maxbits):
+       self.__check_range(bits, minbits, maxbits)
+       self.intrep = bits_to_intrep(bits)
+       self.netmask_bits = bits
+
+
+    def __set_from_tuple(self, tpl, minbits, maxbits):
+       bits = tuple_to_bits(tpl)
+       self.__check_range(bits, minbits, maxbits)
+       self.intrep = bits_to_intrep(bits)
+       self.netmask_bits = bits
+
+    DEMANDS = {DEMAND_NONE:(0,32),
+              DEMAND_FILTER:(0,32),
+              DEMAND_NETWORK:(1,32),
+              DEMAND_INTERFACE:(1,30)}
+
+    def __init__(self, mask, demand):
+       """Create a netmask from MASK (a string, tuple or number) and DEMAND.
+
+       The empty string or None may be given; it is handled as the
+       empty netmask.
+
+       See class network for a description of the DEMAND parameter.
+       """
+
+       (minbits, maxbits) = self.DEMANDS[demand]
+       self.demand = demand
+
+       if type(mask) == types.StringType:
+           self.__user_input = mask
+           mask = string.strip(mask)
+       else:
+           self.__user_input = None
+
+       if mask == '' or mask == None:
+
+           # Handle empty netmasks.
+
+           self.__netmask_str = ''
+           self.intrep = None
+           self.netmask_bits_str = ''
+           self.netmask_bits = None
+           if self.__user_input == None:
+               self.input = ''
+           return
+
+       # Decode the MASK argument and set self.netmask_bits
+       # and self.intrep.
+
+       if type(mask) == types.StringType:
+
+           # Is this a string containing a single number?
+           try:
+               bits = string.atoi(mask)
+           except (OverflowError, ValueError):
+               bits = None
+
+           if bits != None:
+
+               # This is a string containing a single number.
+
+               self.__set_from_bits(bits, minbits, maxbits)
+
+           else:
+
+               # Interpret the netmask as a dotted four-tuple.
+               try:
+                   [a, b, c, d] = map(string.atoi,
+                                      string.splitfields(mask, '.'))
+               except:
+                   raise BrokenNetmask()
+
+               self.__set_from_tuple((a, b, c, d), minbits, maxbits)
+
+       elif type(mask) == types.IntType:
+
+           # This is a number, representing the number of bits in the mask.
+
+           self.__set_from_bits(mask, minbits, maxbits)
+
+       else:
+
+           # This is a tuple or list.
+
+           if len(mask) != 4:
+               raise BadType('Wrong len of tuple/list')
+
+           (a, b, c, d) = (mask[0], mask[1], mask[2], mask[3])
+
+           self.__set_from_tuple((a, b, c, d), minbits, maxbits)
+
+       self.__netmask_str = None
+       self.netmask_bits_str = repr(self.netmask_bits)
+
+    def netmask_str(self):
+       if self.__netmask_str == None:
+           self.__netmask_str = intrep_to_dotted_decimal(self.intrep)
+       return self.__netmask_str
+
+    def user_input(self):
+       if self.__user_input == None:
+           # This object was constructed from a tuple or an integer.
+           self.__user_input = self.ip_str()
+       return self.__user_input
+
+    def __str__(self):
+        if self.intrep is None:
+            return "<netmask empty>"
+        else:
+            return "<netmask /%d>" % self.netmask_bits
+
+    def __repr__(self):
+        if self.intrep is None:
+            return "ipaddr.netmask('')"
+        else:
+            return "ipaddr.netmask(%d, %d)" % (self.netmask_bits, self.demand)
+
+
+hostmask = netmask(32, DEMAND_NONE)
+       
+
+class network:
+    """Designate a network or host.
+
+    The constructor takes three arguments: the IP number part, the
+    netmask part, and a demand parameter.  See class ipaddr and class
+    netmask for a description of the first two arguments.  The demand
+    argument can be one of the following constants:
+
+    DEMAND_NONE
+        No special demands.
+    DEMAND_FILTER
+        The host part must be all zeroes.
+    DEMAND_NETWORK
+        The netmask must be 1-32
+       The host part must be all zeroes.
+    DEMAND_INTERFACE
+        The netmask must be 1-30
+       The host part must *not* be all zeroes (the network address)
+        or all ones (the broadcast address).
+
+    The following members exist and are set by the constructor:
+
+      ip.user_input()          # a caching function
+      ip_str()                 # a caching function
+      ip.intrep
+      mask.user_input()                # a caching function
+      mask.netmask_str()       # a caching function
+      mask.intrep
+      mask.netmask_bits
+      mask.netmask_bits_str
+      network_str()            # a caching function
+      network_intrep
+      broadcast_str()          # a caching function
+      broadcast_intrep
+      host_part_str()          # a caching function
+      host_part_intrep
+
+    """
+
+    def __init__(self, ip, mask, demand):
+       self.ip = ipaddr(ip)
+       self.mask = netmask(mask, demand)
+
+       if self.ip.intrep == None:
+           raise EmptyIpAddress()
+
+       if self.mask.intrep == None:
+           raise EmptyNetmask()
+
+       self._precompute()
+
+    def _precompute(self):
+       self.__lower_str = None
+       self.__upper_str = None
+
+       self.network_intrep = self.ip.intrep & self.mask.intrep
+       self.broadcast_intrep = (self.network_intrep |
+                               (pow(2L, 32)-1-self.mask.intrep))
+       self.host_part_intrep = self.ip.intrep - self.network_intrep
+
+       self.__network_str = None
+       self.__broadcast_str = None
+       self.__host_part_str = None
+
+       demand = self.mask.demand
+
+       if demand == DEMAND_NONE:
+           pass
+       elif demand == DEMAND_FILTER or demand == DEMAND_NETWORK:
+           if self.host_part_intrep != 0L:
+               raise BrokenNetworkAddress(self.ip_str(), self.network_str())
+       elif demand == DEMAND_INTERFACE:
+           if self.host_part_intrep == 0L:
+               raise NetworkAddressClash()
+           elif self.broadcast_intrep == self.ip.intrep:
+               raise BroadcastAddressClash()
+       else:
+           raise BadType('Bad value for the demand parameter', demand)
+
+    def network_str(self):
+       if self.__network_str == None:
+           self.__network_str = intrep_to_dotted_decimal(self.network_intrep)
+       return self.__network_str
+
+    def broadcast_str(self):
+       if self.__broadcast_str == None:
+           self.__broadcast_str = intrep_to_dotted_decimal(
+               self.broadcast_intrep)
+       return self.__broadcast_str
+
+    def host_part_str(self):
+       if self.__host_part_str == None:
+           self.__host_part_str = intrep_to_dotted_decimal(
+               self.host_part_intrep)
+       return self.__host_part_str
+
+    def overlaps(self, other):
+       """Returns true if the network overlaps with OTHER.
+
+       OTHER must be a network object or an ipaddr object.  If it
+       is empty this method will always return false.
+
+       """
+
+       if self.network_intrep == None:
+           return 0
+
+       if isinstance(other, ipaddr):
+
+           if other.intrep == None:
+               return 0
+
+           return (self.mask.intrep & other.intrep) == self.network_intrep
+       else:
+           if other.network_intrep == None:
+               return 0
+
+           mask = self.mask.intrep & other.mask.intrep
+           return (mask & self.ip.intrep) == (mask & other.ip.intrep)
+
+    def intersection(self, other):
+       """Return the intersection of the network and OTHER.
+
+       The return value is a network object with DEMAND_FILTER.  If
+       the intersection is empty this method will return None.
+
+       OTHER must be a network object or an ipaddr object.  The
+       intersection will be empty if it is empty.
+       """
+
+       if self.network_intrep == None:
+           return None
+
+       if isinstance(other, ipaddr):
+
+           if other.intrep == None:
+               return None
+
+           prefix_mask = self.mask.intrep
+           short_net = self.network_intrep
+           long_ip = other.intrep
+           result = network(other.intrep, 32, DEMAND_FILTER)
+       else:
+           if other.network_intrep == None:
+               return None
+           
+           if self.mask.netmask_bits < other.mask.netmask_bits:
+               prefix_mask = self.mask.intrep
+               short_net = self.network_intrep
+               long_ip = other.network_intrep
+               result = network(other.network_intrep, other.mask.netmask_bits,
+                                DEMAND_FILTER)
+           else:
+               prefix_mask = other.mask.intrep
+               short_net = other.network_intrep
+               long_ip = self.network_intrep
+               result = network(self.network_intrep, self.mask.netmask_bits,
+                                DEMAND_FILTER)
+
+       if (long_ip & prefix_mask) != (short_net & prefix_mask):
+           return None
+
+       return result
+
+    def is_subset(self, nwip):
+       """Return true if NWIP is a subset of this network.
+
+       NWIP must be a network object or an ipaddr object.
+       """
+
+       if not self.overlaps(nwip):
+           return 0
+
+       if isinstance(nwip, ipaddr):
+           return 1
+
+       return nwip.mask.netmask_bits <= self.mask.netmask_bits
+
+    def is_same_set(self, nwip):
+       """Return true if NWIP contains the same set as this network.
+
+       NWIP must be a network object or an ipaddr object.
+       """
+
+       if isinstance(nwip, ipaddr):
+           return (self.mask.netmask_bits == 32
+                   and self.ip.intrep == nwip.intrep)
+       else:
+           return (self.mask.netmask_bits == nwip.mask.netmask_bits
+                   and self.network_intrep == nwip.network_intrep)
+
+    def subtract(self, nwip):
+       """Create a list of new network objects by subtracting NWIP from self.
+
+       The result consists of networks that together span all
+       IP addresses that are present in self, except those that are
+       present in NWIP.  (The result may be empty or contain several
+       disjoint network objects.)
+
+       Don't use this!  This method is slow.  The ip_set class can do
+       this kind of things in a more efficient way.
+       """
+
+       if not self.overlaps(nwip):
+           # No overlap at all, so NWIP cannot affect the result.
+           return [self]
+
+       if isinstance(nwip, ipaddr):
+           bits = 32
+           intrep = nwip.intrep
+       else:
+           assert isinstance(nwip, network)
+           bits = nwip.mask.netmask_bits
+           intrep = nwip.ip.intrep
+       nets = []
+       while bits > self.mask.netmask_bits:
+           nets.append(network(compute_neighbor(intrep, bits),
+                               bits, DEMAND_FILTER))
+           bits = bits - 1
+       return nets
+
+    def subtract_nwips(self, nwips):
+       """Create a list of new network objects by subtracting NWIPS.
+
+       The result consists of networks that together span all
+       IP addresses that are present in self, except those that are
+       present in NWIPS.  (The result may be empty or contain
+       several disjoint network objects.)  NWIPS should be a list
+       of network or ipaddr objects.
+
+       Don't use this!  This method is slow.  The ip_set class can do
+       this kind of things in a more efficient way.
+       """
+
+       subtracted = [self]
+       for s in nwips:
+           # precondition<A>: SUBTRACTED is a list of networks
+           tmp = []
+           for nw in subtracted:
+               tmp = tmp + nw.subtract(s)
+           subtracted = tmp
+           # postcondition: SUBTRACTED is a list of networks that
+           # spans all IP addresses that were present in
+           # precondition<A>, except those that are present in S.
+
+       return subtracted
+
+    def __compute_lower_upper(self):
+       if self.__lower_str != None:
+           return
+       assert self.network_intrep != None and self.broadcast_intrep != None
+
+       self.__lower_str = intrep_to_dotted_decimal(self.network_intrep + 1)
+       self.__upper_str = intrep_to_dotted_decimal(self.broadcast_intrep - 1)
+
+    def lower_host(self):
+       self.__compute_lower_upper()
+       return self.__lower_str
+
+    def upper_host(self):
+       self.__compute_lower_upper()
+       return self.__upper_str
+
+    def __repr__(self):
+       return _("{network %s/%d}") % (self.ip_str(), self.mask.netmask_bits)
+
+    def ip_str(self):
+       return self.ip.ip_str()
+
+
+class ip_set:
+    def __init__(self, nwip=None):
+       """Create an ip_set.
+
+       If the optional argument NWIP is supplied, the set is
+       initialized to it, otherwise the created set will be empty.
+       NWIP must be a network or ipaddr object.
+       """
+
+       # [[0L, 3L], [5L, 7L]] means 0.0.0.0/29 \ 0.0.0.4/32
+       self.__set = []
+
+       if nwip != None:
+           self.append(nwip)
+
+    def subtract_set(self, other):
+       """Remove all IP-numbers in OTHER from this.
+
+       OTHER should be an ip_set object.
+       """
+
+       self.subtract_list(other.__set)
+
+    def subtract_ips(self, ips):
+       """Remove all IP-numbers in IPS from this.
+
+       IPS should be a list of ipaddr objects.
+       """
+
+       for ip in ips:
+           self.subtract_list([[ip.intrep, ip.intrep]])
+
+    def subtract_list(self, other):
+       # Don't use this method directly, unless you are the test suite.
+       ix = 0
+       iy = 0
+       while ix < len(self.__set) and iy < len(other):
+           if self.__set[ix][1] < other[iy][0]:
+               # The entire range survived.
+               ix = ix + 1
+           elif self.__set[ix][0] > other[iy][1]:
+               # The entire other range is unused, so discard it.
+               iy = iy + 1
+           elif self.__set[ix][0] >= other[iy][0]:
+               if self.__set[ix][1] <= other[iy][1]:
+                   # The entire range is subtracted.
+                   del self.__set[ix]
+               else:
+                   # The start of the range is subtracted, but
+                   # the rest of the range may survive.  (As a matter
+                   # of fact, at least one number *will* survive,
+                   # since there should be a gap between other[iy][1]
+                   # and other[iy+1][0], but we don't use that fact.)
+                   self.__set[ix][0] = other[iy][1] + 1
+                   iy = iy + 1
+           else:
+               # The first part of the range survives.
+               end = self.__set[ix][1]
+               assert self.__set[ix][1] >= other[iy][0]
+               self.__set[ix][1] = other[iy][0] - 1
+               ix = ix + 1
+               if end > other[iy][1]:
+                   # The part that extends past the subtractor may survive.
+                   self.__set[ix:ix] = [[other[iy][1] + 1, end]]
+               # Retain the subtractor -- it may still kill some
+               # other range.
+
+    def add_set(self, other):
+       """Add all IP-numbers in OTHER to this.
+
+       OTHER should be an ip_set object.
+       """
+
+       self.add_list(other.__set)
+
+    def add_list(self, other):
+       # Don't use this method directly, unless you are the test suite.
+       ix = 0
+       iy = 0
+       res = []
+       while ix < len(self.__set) or iy < len(other):
+           # Remove the first range
+           if ix < len(self.__set):
+               if iy < len(other):
+                   if self.__set[ix][0] < other[iy][0]:
+                       rng = self.__set[ix]
+                       ix = ix + 1
+                   else:
+                       rng = other[iy]
+                       iy = iy + 1
+               else:
+                   rng = self.__set[ix]
+                   ix = ix + 1
+           else:
+               rng = other[iy]
+               iy = iy + 1
+
+           # Join this range to the list we already have collected.
+           if len(res) == 0:
+               # This is the first element.
+               res.append(rng)
+           elif rng[0] <= res[-1][1] + 1:
+               # This extends (or is consumed by) the last range.
+               res[-1][1] = max(res[-1][1], rng[1])
+           else:
+               # There is a gap between the previous range and this one.
+               res.append(rng)
+
+       self.__set = res
+
+    def append(self, nwip):
+       """Add NWIP to this.
+
+       NWIP should be a network object or ipaddr object.
+       """
+
+       if isinstance(nwip, network):
+           self.add_network(nwip)
+       else:
+           self.add_ipaddr(nwip)
+
+    def add_network(self, nw):
+       """Add NW to this.
+
+       NW should be a network object.
+       """
+       self.add_list([[nw.network_intrep, nw.broadcast_intrep]])
+
+    def add_range(self, lo_ip, hi_ip):
+       """Add the range of IP numbers specified by LO_IP and HI_IP to this.
+
+       LO_IP and HI_IP should be ipaddr objects.  They specify a
+       range of IP numbers.  Both LO_IP and HI_IP are included in the
+       range.
+       """
+
+       assert lo_ip.intrep != None
+       assert hi_ip.intrep != None
+       assert lo_ip.intrep <= hi_ip.intrep
+       self.add_list([[lo_ip.intrep, hi_ip.intrep]])
+
+    def add_ipaddr(self, ip):
+       """Add IP to this.
+
+       IP should be an ipaddr object.
+       """
+
+       assert ip.intrep != None
+       self.add_list([[ip.intrep, ip.intrep]])
+
+    def complement(self):
+       """Return everything not contained in this ip_set.
+
+       The return value is a new ip_set.  This is not modified.
+       """
+
+       pre = -1L
+       lst = []
+       for [lo, hi] in self.__set:
+           if lo != 0:
+               lst.append([pre+1, lo-1])
+           pre = hi
+       if pre < pow(2L, 32) - 1:
+           lst.append([pre+1, pow(2L, 32) - 1])
+       res = ip_set()
+       res.add_list(lst)
+       return res
+
+    def intersection(self, other):
+       """Return the intersection of this and OTHER.
+
+       The return value is a new ip_set.  This is not modified.
+       OTHER should be an ip_set, network or ipaddr object.
+       """
+
+       res = []
+       ix = 0
+       iy = 0
+       x = copy.deepcopy(self.__set)
+
+       if isinstance(other, ip_set):
+           y = copy.deepcopy(other.__set)
+       elif isinstance(other, network):
+           y = [[other.network_intrep, other.broadcast_intrep]]
+       elif isinstance(other, ipaddr):
+           y = [[other.intrep, other.intrep]]
+       else:
+           raise BadType('expected an ip_set, network or ipaddr argument')
+
+       while ix < len(x) and iy < len(y):
+           if x[ix][1] < y[iy][0]:
+               # The first entry on x doesn't overlap with anything on y.
+               ix = ix + 1
+           elif x[ix][0] > y[iy][1]:
+               # The first entry on y doesn't overlap with anything on x.
+               iy = iy + 1
+           else:
+               # Some overlap exists.
+
+               # Trim away any leading edges.
+               if x[ix][0] < y[iy][0]:
+                   # x starts before y
+                   x[ix][0] = y[iy][0]
+               elif x[ix][0] > y[iy][0]:
+                   # y starts before x
+                   y[iy][0] = x[ix][0]
+
+               # The ranges start at the same point (at least after
+               # the trimming).
+               if x[ix][1] == y[iy][1]:
+                   # The ranges are equal.
+                   res.append(x[ix])
+                   ix = ix + 1
+                   iy = iy + 1
+               elif x[ix][1] < y[iy][1]:
+                   # x is the smaller range
+                   res.append(x[ix])
+                   ix = ix + 1
+               else:
+                   # y is the smaller range
+                   res.append(y[iy])
+                   iy = iy + 1
+
+       result = ip_set()
+       result.add_list(res)
+       return result
+
+
+    def as_list_of_networks(self):
+       """Return this set as a list of networks.
+
+       The returned value is a list of network objects, that are
+       created with DEMAND_FILTER.  This method may be expensive, so
+       it should only be used when necessary.
+       """
+
+       bm = []
+       for [a, b] in self.__set:
+
+           lomask = 1L
+           lobit = 1L
+           himask = pow(2L, 32)-2
+           bits = 32
+           while a <= b:
+               if a & lomask != 0L:
+                   bm.append((bits, a))
+                   a = a + lobit
+               elif b & lomask != lomask:
+                   bm.append((bits, b & himask))
+                   b = b - lobit
+               else:
+                   lomask = (lomask << 1) | 1
+                   lobit = lobit << 1
+                   himask = himask ^ lobit
+                   bits = bits - 1
+                   assert(bits >= 0)
+       bm.sort()
+       res = []
+       for (mask, ip) in bm:
+           res.append(network(ip, mask, DEMAND_FILTER))
+       return res
+
+    def as_list_of_ranges(self):
+       """Return the set of IP addresses as a list of ranges.
+
+       Each range is a list of two long numbers.  Sample return
+       value: [[1L, 3L], [0x7f000001L, 0x7f000001L]], meaning
+       the set 0.0.0.1, 0.0.0.2, 0.0.0.3, 127.0.0.1.
+       """
+
+       # This method is currently very cheap, since this is the
+       # current internal representation.
+
+       return self.__set
+
+    def as_str_range(self):
+       """Return the set as a string, such as "1.2.3.4-1.2.3.8".
+
+       The returned value always has the form a.b.c.d-e.f.g.h.
+       Raises SetNotRepresentable if the set cannot be represented as a
+       single interval, or if it is the empty set.
+       """
+       if len(self.__set) != 1:
+           raise SetNotRepresentable()
+       return "%s-%s" % (intrep_to_dotted_decimal(self.__set[0][0]),
+                         intrep_to_dotted_decimal(self.__set[0][1]))
+
+    def contains(self, ip):
+       """Return true if IP is contained in the set.
+
+       IP should be an ipaddr object.  The empty ipaddr is never contained.
+       """
+
+       if ip.intrep == None:
+           return 0
+
+       for [lo, hi] in self.__set:
+           if lo <= ip.intrep <= hi:
+               return 1
+       return 0
+
+    def overlaps(self, nwip):
+       """Return true if NWIP overlaps the set of IP addresses.
+
+       NWIP may be an ipaddr, network or ip_set object.
+       """
+
+       if isinstance(nwip, ipaddr):
+           return self.contains(nwip)
+       elif isinstance(nwip, ip_set):
+           # This could be optimized -- we don't really need
+           # to compute the intersection.
+           return not self.intersection(nwip).is_empty()
+       elif isinstance(nwip, network):
+           wanted_low = nwip.network_intrep
+           wanted_high = nwip.broadcast_intrep
+           if wanted_low == None or wanted_high == None:
+               return 0
+           for [lo, hi] in self.__set:
+               if lo > wanted_high:
+                   # We are past the interresting interval.
+                   return 0
+               if lo >= wanted_low or hi >= wanted_low:
+                   return 1
+           return 0
+       else:
+           raise BadType('Expected an ipaddr, ip_set or network instance')
+
+    def is_empty(self):
+       """Return true if this ip_set is empty.
+       """
+
+       return len(self.__set) == 0
+
+    def any_ip(self):
+       """Return one of the IP addresses contained in ip_set.
+
+       This method may only be called if the set is non-empty.  You
+       can use the is_empty method to test for emptiness.
+
+       This picks an IP address from the set and returns it as an
+       ipaddr object.  Given the same set of IP addresses, this
+       method will always return the same IP address, but which IP
+       address it chooses is explicitly undocumented and may change
+       if the underlying implementation of ip_set ever changes.
+       """
+
+       assert not self.is_empty()
+       return ipaddr(self.__set[0][0])
+
+    def __str__(self):
+       res = []
+       for rng in self.__set:
+           if rng[0] == rng[1]:
+               res.append(intrep_to_dotted_decimal(rng[0]))
+           else:
+               res.append('%s-%s' % (intrep_to_dotted_decimal(rng[0]),
+                                     intrep_to_dotted_decimal(rng[1])))
+       return '<ipaddr.ip_set(%s)>' % string.join(res, ', ')
+
+complete_network = network(0L, 0, DEMAND_FILTER)
+complete_set = ip_set(complete_network)
+broadcast_network = network('255.255.255.255', 32, DEMAND_FILTER)
+broadcast_set = ip_set(broadcast_network)
+
+def compute_neighbor(intrep, bits):
+    xor_mask = intrep_with_bit_set(bits)
+    and_mask = bits_to_intrep(bits)
+    return (intrep ^ xor_mask) & and_mask
+
+
+if __name__ == '__main__':
+    # Test/demo code.  With no arguments, this will print a page
+    # of data that can be useful when trying to interpret an
+    # ipnumber/netmask pair.  With two arguments, it will print some
+    # information about the IP number and netmask that was entered.
+
+    import sys
+    if len(sys.argv) == 1:
+       print "Netmasks\n========"
+       for i in range(0, 17):
+           if i != 16:
+               print '%2d' % i,
+               print '%-13s' % netmask(i, DEMAND_NONE).netmask_str(),
+           else:
+               print ' ' * 16,
+           print i + 16, '%-16s' % netmask(i + 16, DEMAND_NONE).netmask_str()
+       print _("\n\nIP intervals\n============")
+       for i in range(9):
+           for j in range(0, 4):
+               print '%2d' % (8*j + i),
+           print '%3d' % (netmask(i, DEMAND_NONE).intrep >> 24),
+           x = 0
+           need_break = 0
+           if i < 8:
+               for j in range(0, 256, pow(2, 8-i)):
+                   if need_break:
+                       print
+                       print ' ' * 15,
+                       need_break = 0
+                   print '%3d-%-3d' % (j, j + pow(2, 8-i)-1),
+                   x = x + 1
+                   if x % 8 == 0:
+                       need_break = 1
+           else:
+               print '0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13...',
+           print
+       sys.exit(0)
+
+    if len(sys.argv) != 3:
+       sys.stderr.write(_("Usage: python ipaddr.py IP_ADDRESS NETMASK\n"))
+       sys.exit(1)
+    nw = network(sys.argv[1], sys.argv[2], DEMAND_NONE)
+    print nw
+    print "IP address:       ", nw.ip.ip_str()
+    print "Netmask:          ", nw.mask.netmask_str(),
+    print " (/" + nw.mask.netmask_bits_str + ")"
+    print "Network address:  ", nw.network_str()
+    print "Broadcast address:", nw.broadcast_str()