# ipv4.py - helpers to deal with ipv4 addresses, networks and other elements # (C) 2003 Christos TZOTZIOY Georgiou # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Author's email: tzot@sil-tec.gr # # TBD: # {tests} # networks_including(from_host, to_host, max_bits=32) # # This module was written because I have a lot of trouble finding # spamming networks and making sure my postfix installation # does block them... then I decided to make it public. # See at end for example usages (for the moment). # Enjoy :-) """ipv4: utilities for IPv4 networks, addresses and other elements""" import socket, re, struct assert len(struct.pack('!i', 0)) == 4, "this module works only on architectures that sizeof(int)==4" class CIDR(object): """Store the CIDR of a host or a network Syntax: CIDR(address [,bits=32], [,packed=False] address is a 4-byte integer unless packed is a true value, in which case it's a 4-byte packed string""" __slots__ = 'numeric_address', 'packed_address', 'bits', 'network', # masks: a list of the masks indexed on the /network-number masks = [0] + [int(-(2**(31-x))) for x in range(32)] del x # reMatchString: a re that matches string CIDR's reMatchString = re.compile( r'(\d+)' # first byte must always be given r'(?:' # start optional parts r'\.(\d+)' # second byte r'(?:'# optionally third byte r'\.(\d+)' r'(?:' # optionally fourth byte r'\.(\d+)' r')?' r')?' # fourth byte is optional r')?' # third byte is optional too r'(?:/(\d+))?$') # and bits possibly def __init__(self, address, bits=32, packed=False): "Create a CIDR from numerical address and bits or from a single string CIDR" if packed: self.packed_address = address self.numeric_address = struct.unpack('!i', address)[0] self.bits = bits elif isinstance(address, basestring): # eg '12.20.192/18' match = self.reMatchString.match(address) if not match: raise ValueError, "cannot infer address/bits from %.20r" % address else: numbers = [x and int(x) or 0 for x in match.groups()] # by packing we throw errors if any byte > 255 self.packed_address = struct.pack('4B', *numbers[:4]) # first 4 are in network order # the old way: numbers[0] << 24 | numbers[1] << 16 | numbers[2] << 8 | numbers[3] # shift is faster than unpacking but it will break in 2.4 self.numeric_address = struct.unpack('!i', self.packed_address)[0] self.bits = numbers[4] or numbers[3] and 32 or numbers[2] and 24 or numbers[1] and 16 or 8 else: # assume numerical self.numeric_address = address self.packed_address = struct.pack('!i', address) self.bits = bits if (self.numeric_address & self.mask) != self.numeric_address: # it's a host, not a network self.network = CIDR(self.numeric_address & self.mask, self.bits) else: # it's a network self.network = self mask = property( lambda self: self.masks[self.bits], doc="Get the subnet mask of the ip address") dotted_address = property( lambda self: socket.inet_ntoa(self.packed_address), doc="Get the address as a dotted ip address") broadcast = property( lambda self: CIDR((self.numeric_address&self.mask)|(~self.mask), self.network.bits), doc="Get the broadcast address of the CIDR") bytes = property( lambda self: struct.unpack('4B', self.packed_address), doc="Access the bytes of the address as a list") def truncate(self, bits): "Return a CIDR with a different subnet (resetting bits if less than self.bits)" return self.__class__(self.numeric_address, bits).network def __nontrue__(self): return self.numeric_address and True or False def __and__(self, other): "Return the common subnet of the two arguments if any, otherwise '0.0.0.0/0'" bits = min(self.bits, other.bits) while bits > 0: if (self.numeric_address&self.masks[bits]) == (other.numeric_address&self.masks[bits]): break bits -= 1 else: return CIDR(0, 0) return CIDR(self.numeric_address & self.masks[bits], bits) def shortform(self): "Return string rep of self skipping zero bytes at the end of address" result = str(self) while 1: new_result = result.replace(".0/", "/") if new_result==result: break result = new_result return result def __add__(self, hosts): "Add to self.numeric_address and return another CIDR" return self.__class__(self.numeric_address+hosts, self.bits) def __sub__(self, hosts): "Subtract from self.numeric_address and return another CIDR" return self.__class__(self.numeric_address-hosts, self.bits) def __str__(self): return "%s/%d" % (socket.inet_ntoa(self.packed_address), self.bits) def __contains__(self, other): """Check if a host/net is inside a network""" return (other.numeric_address & self.mask) == self.numeric_address def __repr__(self): return "%s('%s/%d')" % ( self.__class__.__name__, socket.inet_ntoa(self.packed_address), self.bits,) def __le__(self, other): return (self.packed_address, self.bits) <= (other.packed_address, other.bits) def __lt__(self, other): return (self.packed_address, self.bits) < (other.packed_address, other.bits) def __eq__(self, other): return self.numeric_address==other.numeric_address and self.bits==other.bits def __cmp__(self, other): return cmp((self.numeric_address, self.bits), (other.numeric_address, other.bits)) def __hash__(self): return self.numeric_address ^ self.bits def __len__(self): "The count of addresses in this net (if host/32, len==1)" return -self.mask def __iter__(self): "Iterate over all addresses in net" for numeric_address in xrange(self.numeric_address - (self.numeric_address % -self.mask), self.broadcast.numeric_address+1): yield CIDR(numeric_address, self.bits) def included_nets(min_addr, max_addr, max_bits=32): "Return a list of networks that define the range from_host - till_addr.broadcast" result_list = [] ## min_bits = None while True: ## print "called with", min_addr, max_addr, max_bits base_net = min_addr & max_addr if base_net.numeric_address==min_addr.numeric_address \ and base_net.broadcast.numeric_address==max_addr.numeric_address: # max addr is a broadcast result_list.append(base_net) break # trim a subnet at the beginning of the list bits = base_net.bits while bits < max_bits: subnet = CIDR(min_addr.numeric_address, bits) if subnet.network.numeric_address==min_addr.numeric_address \ and subnet.broadcast.packed_address <= max_addr.packed_address: result_list.append(subnet) break bits += 1 else: break min_addr = CIDR(subnet.broadcast.numeric_address+1, subnet.bits) if min_addr > max_addr: break return result_list if __name__=="__main__": try: assert False except AssertionError: pass else: raise RuntimeError, "don't test with optimisation on" # numeric, bits equals string assert CIDR(-1, 32) == CIDR('255.255.255.255') # get the network assert CIDR('10.1.12.42/24').network == CIDR('10.1.12.0/24') # get the broadcast address assert CIDR('10.1.12.42/24').broadcast == CIDR('10.1.12.255/24') # verify initialisation assert CIDR('10.1.12').bits == 24 assert CIDR('10.1').bits == 16 # verify arithmetic assert CIDR('10.1.12.42')+1 == CIDR('10.1.12.43') assert CIDR('10.1.12.42')-256 == CIDR('10.1.11.42') # verify truncation assert CIDR('10.1.12.42').truncate(24) == CIDR('10.1.12') assert CIDR('10.1.12.42').truncate(20) == CIDR('10.1.12.42/20').network # check if a host is in a network assert CIDR('10.1.12.42') in CIDR('10.1.12/24') # check the reverse assert CIDR('10.1.12.42') not in CIDR('10.1.10/23') # find net large enough to include both addresses assert CIDR('24.72.2.0') & CIDR('24.72.49.255') == CIDR('24.72.0.0/18') # find subnets between 212.195.64.0 and 212.195.255.255 assert included_nets(CIDR('212.195.64.0'), CIDR('212.195.255.255')) == \ [CIDR('212.195.64/18'), CIDR('212.195.128/17')] # same but tougher assert included_nets(CIDR('24.72.2.0'), CIDR('24.72.49.255')) == \ [CIDR('24.72.2.0/23'), CIDR('24.72.4.0/22'), CIDR('24.72.8.0/21'), CIDR('24.72.16.0/20'), CIDR('24.72.32.0/20'), CIDR('24.72.48.0/23')] # count addresses in a network assert len(CIDR('10.1.12.42/24')) == 256 assert len(CIDR('10.1.12.42')) == 1 # iterating through all hosts in a network assert list(CIDR('10.1.12.42/29')) == \ [CIDR('10.1.12.42/29'), CIDR('10.1.12.43/29'), CIDR('10.1.12.44/29'), CIDR('10.1.12.45/29'), CIDR('10.1.12.46/29'), CIDR('10.1.12.47/29')] # the same, but now get the whole network of the host assert list(CIDR('10.1.12.42/29').network) == \ [CIDR('10.1.12.40/29'), CIDR('10.1.12.41/29'), CIDR('10.1.12.42/29'), CIDR('10.1.12.43/29'), CIDR('10.1.12.44/29'), CIDR('10.1.12.45/29'), CIDR('10.1.12.46/29'), CIDR('10.1.12.47/29')] print "All tests passed." print "Example: 'ping' all hosts in network 10.1.12.0/26 (10.1.12.0-10.1.12.63)" print "Pinging", for host in CIDR('10.1.12.0/26'): print host.bytes[3], print