# David.Clark (a) CIBER-research.eu 2012-12-20 # CIBER Research Ltd. # version 10 hybrid for both clarity and optimisation, + bug fix. # this code is online at # http://code.activestate.com/recipes/576479/ """ Create a digital trie of ipranges(CIDR); for a given ip determine membership of smallest CIDR (if any) by digital search. 'Instead of basing a search method on comparisons between keys, we can make use of their representation as a sequence of digits or alphabetic characters.' Knuth. Art of Computer Programming vol 3, 2e 1998 (6.3 p492) """ def bytes2bits(): """Create a table of bit values for int(0) to int(256).""" # derived from Python Cookbook 2e 2005. bytes = [None] * 256 for n in xrange(256): bits = [] q = n # q is modified by bitshift for b in xrange(0,8): if (q & 128): bits.append(1) else: bits.append(0) q <<= 1 bytes[n] = tuple(bits) # make immutable return tuple(bytes) # One-time call to build table; rebind the funtion name to its result. bytes2bits = bytes2bits() class ProgramError(ValueError): """This is a bug""" class CIDRnode(object): def __init__(self, cidr=None): self.cidr = cidr self.left = None self.right = None def get_net(self): if self.cidr: return self.cidr.split('/')[0] else: return None network = property(get_net, doc='IPv4 dotted quad') def get_bits(self): if self.cidr: return int(self.cidr.split('/')[1]) else: return None netbits = property(get_bits, doc='IPv4 netmask bits') class CIDRtrie(object): """ take a shortcut by using a list for the first octet as any CIDR will be no greater than class A. Thereafter work down a binary tree of ip network addresses. If a node has a CIDR record stack the result, but continue down the tree while there are links. (Not all Nodes have CIDR records.). At leif node pop (the best) result. """ # we need some definitions before we can __init__() @staticmethod def byte2bit(ip): for q in ip: q = int(q) for b in xrange(0,8): yield 1 if (q & 128) else 0 q <<= 1 def add_cidr(self, cidr): """Build the trie. For clarity this is the non-optimised version.""" c = cidr.split('/') network = c[0].split('.') nm_bit = int(c[1]) classA = int(network[0]) subtree = self.root[classA] if not subtree: subtree = CIDRnode() self.root[classA] = subtree nm_bit -= 7 # leave a bit for test at top of loop network_bits = self.byte2bit(network[1:]) for nextbit in network_bits: if nm_bit == 1: overwrite = subtree.cidr subtree.cidr = cidr return overwrite # expect to return None nm_bit -= 1 if nextbit is 0: if not subtree.left: subtree.left = CIDRnode() subtree = subtree.left continue elif nextbit is 1: if not subtree.right: subtree.right = CIDRnode() subtree = subtree.right continue else: raise ProgramError( 'Tried to bud %s, bitten by bug, fell out of Tree'% cidr) subtree.cidr = cidr def __init__(self, ipranges=None): self.root = [None] * 256 # A forest of (x.0.0.0/8) 'class A' addresses if ipranges: for cidr in ipranges: self.add_cidr(cidr) def get_cidr(self, ip): """This is very similar to add_cidr but inline code improves performance by around 10% and the cost of building the lookup table is amortised over serveral million lookups gaining another 10%.""" ip = ip.split('.') subtree = self.root[int(ip[0])] # subtree = CIDRnode if subtree is None: return None results = [None] for quad in ip[1:]: quad_bits = bytes2bits[int(quad)] for nextbit in quad_bits: if subtree.cidr: results.append(subtree.cidr) if subtree.left and nextbit is 0: subtree = subtree.left continue elif subtree.right and nextbit is 1: subtree = subtree.right continue else: return results.pop() return subtree.cidr #### Command Line Processing #### def test(): index = {} networks = ( ('10.40.47.0/27', 'network 1'), ('10.40.0.0/16' , 'network 2'), ('10.44.0.0/16' , 'network 3'), ('192.168.47.0/27','network 4'), ('10.10.1.1/32' , 'single_ip'),) ipaddr = ('10.40.47.26', '10.40.47.34', '10.44.47.26', '192.168.47.0', '192.168.47.31', '192.168.47.32', '192.168.0.47', '192.168.47.1', '10.10.1.1') trie = CIDRtrie() for cidr, name in networks: index[cidr] = name overwrite = trie.add_cidr(cidr) if overwrite: print('WARNING overwriting %s with %s'% (overwrite, cidr)) for ip in ipaddr: cidr = trie.get_cidr(ip) if cidr: name = index[cidr] print('%s is in network %s (%s)'% (ip, name, cidr)) else: print('network not known for %s'% ip) if __name__ == '__main__': #only when run from cmd line test()