Routing table analysis, or why a Python network engineer

Hello Habr! This is my first article on Habré, and she was born from a question in one of the professional forums. The question looked, somewhat paraphrasing, as follows:



The task seemed to me interesting and echoed one of my own network utilities planned for the future. Therefore, on a free evening, having thought about its solution, I wrote the Proof-of-Concept implementation in Python 2.7 for Cisco IOS, IOS-XE and ASA, which responds basic requirements.


The article will try to reproduce the train of thought and comment on the main points.
The material is intended for people already familiar with the basics of networking and Python.
All interested welcome under the cat!


Disclaimer


The author of the article, being a network engineer, is not a professional developer. and may not yet know the difference of the interface from the abstract class but always open to constructive criticism and feedback. Comments on the code, selected approaches and algorithms from more experienced colleagues are welcome.


All code in this article is distributed under the MIT license, incl. is “as is” and does not give any kind of guarantee.


Clarification of the conditions and the choice of solution


Taking into account the input, the task can be divided into two main parts: parsing of source files with routing tables and direct path search using initialized data.
This separation will also allow, if necessary, import routes from devices to search for a path in an arbitrary way (for example, via SNMP or REST API).


To improve performance, it makes sense to initialize files once at script startup.
The file parser should recognize the format of the routing tables from different operating systems. Next will be considered an option for Cisco IOS, IOS-XE and ASA for IPv4. Support for other formats and IPv6 may be added later.


As is known, the choice of a route according to the routing table is done according to the principle of coincidence with the longest prefix match (longest prefix match).
As one of the solutions to quickly find such a match, one can construct a prefix tree from the source data. Since we have no restrictions on external dependencies, we ’ll dwell on the ready-made SubnetTree module.


There are potentially routing loops on the analyzed network segment, they should be detected and should not affect the script operation. In addition, on any of the nodes may not be a route to the desired subnet. This should also be considered.


If there is a VRF on the device, the routing tables from each instance should be stored in separate files, since from the topology point of view they are separate routers.


Limitations on the performance of iron for running the script, the number and size of the analyzed routing tables in the condition are not stated, but it is worth keeping them in mind.
The specificity of the application area suggests an average limit of 1,000,000 entries in the routing table for one modern device. On a router with BGP Full View as of June 2018 there can actually be 724.000+ routes.
According to a rough estimate and the results of tests for in-memory storage and processing of each 1,000,000 prefixes, you will need about 500MB of RAM. Thus, an average productivity of a workstation with 8GB RAM (2018 is still in our mind) will allow us to analyze the topology with an aggregate capacity of up to 14–16,000,000 routes. That is a segment of about 18-20 routers with full view on each.
For most cases, this is quite enough, but for large (no matter how stressful) networks you need to either break the analysis into segments, or transfer the logic to an out-of-memory database.
640KB is enough for everyone. Let's stop on in-memory option.


Parsing source files and selecting data structures


Files with routing tables will be placed in a separate sub-directory and make it variable:


RT_DIRECTORY = "./routing_tables" 

For Cisco IOS and IOS-XE, the routing table might look something like this:


show ip route
 S* 0.0.0.0/0 [1/0] via 10.220.88.1 10.0.0.0/8 is variably subnetted, 2 subnets, 2 masks C 10.220.88.0/24 is directly connected, FastEthernet4 L 10.220.88.20/32 is directly connected, FastEthernet4 1.0.0.0/32 is subnetted, 1 subnets S 1.1.1.1 [1/0] via 212.0.0.1 [1/0] via 192.168.0.1 D EX 10.1.198.0/24 [170/1683712] via 172.16.209.47, 1w2d, Vlan910 [170/1683712] via 172.16.60.33, 1w2d, Vlan60 [170/1683712] via 10.25.20.132, 1w2d, Vlan220 [170/1683712] via 10.25.20.9, 1w2d, Vlan20 4.0.0.0/16 is subnetted, 1 subnets O E2 4.4.0.0 [110/20] via 194.0.0.2, 00:02:00, FastEthernet0/0 5.0.0.0/24 is subnetted, 1 subnets D EX 5.5.5.0 [170/2297856] via 10.0.1.2, 00:12:01, Serial0/0 6.0.0.0/16 is subnetted, 1 subnets B 6.6.0.0 [200/0] via 195.0.0.1, 00:00:04 172.16.0.0/26 is subnetted, 1 subnets i L2 172.16.1.0 [115/10] via 10.0.1.2, Serial0/0 172.20.0.0/32 is subnetted, 3 subnets O 172.20.1.1 [110/11] via 194.0.0.2, 00:05:45, FastEthernet0/0 O 172.20.3.1 [110/11] via 194.0.0.2, 00:05:45, FastEthernet0/0 O 172.20.2.1 [110/11] via 194.0.0.2, 00:05:45, FastEthernet0/0 10.0.0.0/8 is variably subnetted, 5 subnets, 3 masks C 10.0.1.0/24 is directly connected, Serial0/0 D 10.0.5.0/26 [90/2297856] via 10.0.1.2, 00:12:03, Serial0/0 D 10.0.5.64/26 [90/2297856] via 10.0.1.2, 00:12:03, Serial0/0 D 10.0.5.128/26 [90/2297856] via 10.0.1.2, 00:12:03, Serial0/0 D 10.0.5.192/27 [90/2297856] via 10.0.1.2, 00:12:03, Serial0/0 192.168.0.0/32 is subnetted, 1 subnets D 192.168.0.1 [90/2297856] via 10.0.1.2, 00:12:03, Serial0/0 O IA 195.0.0.0/24 [110/11] via 194.0.0.2, 00:05:45, FastEthernet0/0 O E2 212.0.0.0/8 [110/20] via 194.0.0.2, 00:05:35, FastEthernet0/0 C 194.0.0.0/16 is directly connected, FastEthernet0/0 

In Cisco ASA, the format is similar, but, instead of prefix lengths, the subnet mask is displayed in decimal representation:


show route
 S 10.1.1.0 255.255.255.0 [3/0] via 10.86.194.1, outside C 10.86.194.0 255.255.254.0 is directly connected, outside S* 0.0.0.0 0.0.0.0 [1/0] via 10.86.194.1, outside 

As you can see from the example, the routes, despite the great diversity, have the same and predictable structure, and therefore can be processed with regular expressions.
It turns out two global groups: routes of types Local + Connected and everything else.
The possibility of having multi-line routes with a variable number of next-hops somewhat complicates the matter. Because of this, it is also problematic to read a file for processing line by line. One of the outputs is the processing of a set of matches by an iterator of a regular expression in a file loaded entirely into a text variable.


We write regular expressions with the listed requirements and limitations:


 # Local and Connected route strings matching. REGEXP_ROUTE_LOCAL_CONNECTED = re.compile( '^(?P<routeType>[L|C])\s+' + '((?P<ipaddress>\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)' + '\s?' + '(?P<maskOrPrefixLength>(\/\d\d?)?' + '|(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)?))' + '\ is\ directly\ connected\,\ ' + '(?P<interface>\S+)', re.MULTILINE ) # Static and dynamic route strings matching. REGEXP_ROUTE = re.compile( '^(\S\S?\*?\s?\S?\S?)' + '\s+' + '((?P<subnet>\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)' + '\s?' + '(?P<maskOrPrefixLength>(\/\d\d?)?' +'|(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)?))' + '\s*' + '(?P<viaPortion>(?:\n?\s+(\[\d\d?\d?\/\d+\])\s+' + 'via\s+(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)(.*)\n?)+)', re.MULTILINE ) 

Both expressions contain named groups for easy retrieval of data when searching for matches and maintaining code.
In particular, from each route you need to get a prefix ( subnet / interface and maskOrPrefixLength groups ) and information about where it leads ( viaPortion / interface groups).


Since expressions take into account several variants of the prefix representation, in the maskOrPrefixLength group there can be both a subnet mask and a prefix length. We will bring this to a single format during processing, we will focus on the length of the prefix:


 def convert_netmask_to_prefix_length(mask_or_pref): if not mask_or_pref: return "" if re.match("^\/\d\d?$", mask_or_pref): return mask_or_pref if re.match("^\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?$", mask_or_pref): return ( "/" + str(sum([bin(int(x)).count("1") for x in mask_or_pref.split(".")])) ) return "" 

Add also regular expressions for line-by-line parsing of the next-hop from the viaPortion group, checking the format of IPv4-addresses and user input:


 # Route string VIA portion matching. REGEXP_VIA_PORTION = re.compile( '.*via\s+(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?).*' ) # RegEx template string for IPv4 address matching. REGEXP_IPv4_STR = ( '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' + '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' + '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' + '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))' ) # IPv4 CIDR notation matching in user input. REGEXP_INPUT_IPv4 = re.compile("^" + REGEXP_IPv4_STR + "(\/\d\d?)?$") 

Now we will transfer the representation of our network to Python data structures.
The resulting parsing of the routing table prefixes will be used as keys in the prefix tree. The object of the prefix tree is inherited from the SubnetTree module.
The result of the search by prefix in the tree will return a list from the list of next-hops and the full text representation of the corresponding route from the original file.
Additionally create a list of local interfaces.
Each router is represented by a dictionary in which we put the above data.


 # Example data structures route_tree = SubnetTree.SubnetTree() route_tree['subnet'] = ((next_hop_1, next_hop_n), raw_route_string) interface_list = ((interface_1, ip_address_1), (interface_n, ip_address_n)) connected_networks = ((interface_1, subnet_1), (interface_n, subnet_n)) router = { 'routing_table': route_tree, 'interface_list': interface_list, 'connected_networks': connected_networks, } 

The request to the routing table is moved to a separate function:


 def route_lookup(destination, router): if destination in router['routing_table']: return router['routing_table'][destination] else: return (None, None) 

We assign a unique identifier to each router. You can assign it in a variety of ways; in the current example, it will be permissible and visually do it based on the name of the original file.
As a result, we will add the resulting router objects to the dictionary with a key in the form of this unique identifier.


 ROUTERS = { 'router_id_1': router_1, 'router_id_n': router_n, } 

We also need a mechanism to find the next router by the IP address of the next hop, obtained from the routing table during the path search. For this, we will create another global prefix tree with keys in the form of IP addresses of all known routers in the topology and return sheets for them with a router ID and interface type.


 # Example GLOBAL_INTERFACE_TREE = SubnetTree.SubnetTree() GLOBAL_INTERFACE_TREE['ip_address'] = (router_id, interface_type) # Returns RouterID by Interface IP address which it belongs to. def get_rid_by_interface_ip(interface_ip): if interface_ip in GLOBAL_INTERFACE_TREE: return GLOBAL_INTERFACE_TREE[interface_ip][0] 

Let's put together a parser for the IOS / IOS-XE / ASA format together. At the input we give it the routing table in text form, at the output we get the router dictionary in the format specified above.


 def parse_show_ip_route_ios_like(raw_routing_table): router = {} route_tree = SubnetTree.SubnetTree() interface_list = [] # Parse Local and Connected route strings in text. for raw_route_string in REGEXP_ROUTE_LOCAL_CONNECTED.finditer(raw_routing_table): subnet = ( raw_route_string.group('ipaddress') + convert_netmask_to_prefix_length( raw_route_string.group('maskOrPrefixLength') ) ) interface = raw_route_string.group('interface') route_tree[subnet] = ((interface,), raw_route_string.group(0)) if raw_route_string.group('routeType') == 'L': interface_list.append((interface, subnet,)) if not interface_list: print('Failed to find routing table entries in given output') return None # parse static and dynamic route strings in text for raw_route_string in REGEXP_ROUTE.finditer(raw_routing_table): subnet = ( raw_route_string.group('subnet') + convert_netmask_to_prefix_length( raw_route_string.group('maskOrPrefixLength') ) ) via_portion = raw_route_string.group('viaPortion') next_hops= [] if via_portion.count('via') > 1: for line in via_portion.splitlines(): if line: next_hops.append(REGEXP_VIA_PORTION.match(line).group(1)) else: next_hops.append(REGEXP_VIA_PORTION.match(via_portion).group(1)) route_tree[subnet] = (next_hops, raw_route_string.group(0)) router = { 'routing_table': route_tree, 'interface_list': interface_list, } return router 

Let's wrap the parser into another function for the possibility of the subsequent addition of parsers of other formats (for example, NX-OS):


 def parse_text_routing_table(raw_routing_table): """ Parser functions wrapper. Add additional parsers for alternative routing table syntaxes here. """ router = parse_show_ip_route_ios_like(raw_routing_table) if router: return router 

So, it remains to go through the text files in the directory:


 def do_parse_directory(rt_directory): new_routers = {} if not os.path.isdir(rt_directory): print("{} directory does not exist.".format(rt_directory) + "Check rt_directory variable value." ) return None start_time = time() print("Initializing files...") for FILENAME in os.listdir(rt_directory): if FILENAME.endswith('.txt'): file_init_start_time = time() with open(os.path.join(rt_directory, FILENAME), 'r') as f: print ('Opening {}'.format(FILENAME)) raw_table = f.read() new_router = parse_text_routing_table(raw_table) router_id = FILENAME.replace('.txt', '') if new_router: new_routers[router_id] = new_router if new_router['interface_list']: for iface, addr in new_router['interface_list']: GLOBAL_INTERFACE_TREE[addr]= (router_id, iface,) else: print ('Failed to parse ' + FILENAME) print (FILENAME + " parsing has been completed in %s sec".format( "{:.3f}".format(time() - file_init_start_time)) ) else: if not new_routers: print ("Could not find any valid .txt files with routing tables" + " in {} directory".format(rt_directory) ) else: print ("\nAll files have been initialized" + " in {} sec".format("{:.3f}".format(time() - start_time)) ) return new_routers 

And, expanding all the files into ordered data structures, you can take up the second part of the problem.


Path search through processed routing tables


In general, at this stage the task is reduced to the analysis of the network graph. Routers are vertices of the graph, L3 links between them are edges.
The ROUTERS dictionary stores the identifiers of the routers in the keys, and, in their corresponding values, references to the IP addresses of the next hop. That is, in conjunction with GLOBAL_INTERFACE_TREE , which returns router IDs by IP address, it determines the graph adjacency table for each desired subnet.


If you draw parallels with real routers, to find the path you need to reproduce the high-level logic of their work (abstracting from RIB / FIB / ASIC and other optimizations) when processing a packet from a loop into the routing table to an ARP request (or router_id in our case) and routing or package drop, depending on the result.


We implement recursive path searching through routers. Each leg of the path will be represented by a sheet from router_id and raw_route_string — the original route string on it. The current path will be written to the path tuple. When reaching a destination, no route in the routing table or next-hop in the studied topology, the current path will be added to the resulting paths tuple, which the function will return.


 def trace_route(source_router_id, target_ip, path=[]): if not source_router_id: return [path + [(None, None)]] current_router = ROUTERS[source_router_id] next_hop, raw_route_string = route_lookup(target_ip, current_router) path = path + [(source_router_id, raw_route_string)] paths = [] if next_hop: if nexthop_is_local(next_hop[0]): return [path] for nh in next_hop: next_hop_rid = get_rid_by_interface_ip(nh) if not next_hop_rid in [r[0] for r in path]: inner_path = trace_route(next_hop_rid, target_ip, path) for p in inner_path: paths.append(p) else: path = path + [(next_hop_rid+"<<LOOP DETECTED", None)] return [path] else: return [path] return paths def nexthop_is_local(next_hop): interface_types = ('Eth', 'Fast', 'Gig', 'Ten', 'Port', 'Serial', 'Vlan', 'Tunn', 'Loop', 'Null' ) for type in interface_types: if next_hop.startswith(type): return True 

Add a function to start a search online after initializing text files:


 def do_user_interactive_search(): while True: print ('\n') target_subnet = raw_input('Enter Target Subnet or Host: ') if not target_subnet: continue if not REGEXP_INPUT_IPv4.match(target_subnet.replace(' ', '')): print ("incorrect input") continue lookup_start_time = time() for rtr in ROUTERS.keys(): subsearch_start_time = time() result = trace_route(rtr, target_subnet) if result: print ("\n") print ("PATHS TO {} FROM {}".format(target_subnet, rtr)) n = 1 print ('Detailed info:') for r in result: print ("Path {}:".format(n)) print ([h[0] for h in r]) for hop in r: print ("ROUTER: {}".format(hop[0])) print ("Matched route string: \n{}".format(hop[1])) else: print ('\n') n+=1 else: print ("Path search on {} has been completed in {} sec".format( rtr, "{:.3f}".format(time() - subsearch_start_time)) ) else: print ("\nFull search has been completed in {} sec".format( "{:.3f}".format(time() - lookup_start_time),) ) 

The final touch to combine the two parts:


 def main(): global ROUTERS ROUTERS = do_parse_directory(RT_DIRECTORY) if ROUTERS: do_user_interactive_search() if __name__ == "__main__": main() 

And we have ready-to-work code.


Code
 import os import re import SubnetTree from time import time # Path to directory with routing table files. # Each routing table MUST be in separate .txt file. RT_DIRECTORY = "./routing_tables" # RegEx template string for IPv4 address matching. REGEXP_IPv4_STR = ( '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' + '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' + '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' + '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))' ) # IPv4 CIDR notation matching in user input. REGEXP_INPUT_IPv4 = re.compile("^" + REGEXP_IPv4_STR + "(\/\d\d?)?$") # Local and Connected route strings matching. REGEXP_ROUTE_LOCAL_CONNECTED = re.compile( '^(?P<routeType>[L|C])\s+' + '((?P<ipaddress>\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)' + '\s?' + '(?P<maskOrPrefixLength>(\/\d\d?)?' + '|(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)?))' + '\ is\ directly\ connected\,\ ' + '(?P<interface>\S+)', re.MULTILINE ) # Static and dynamic route strings matching. REGEXP_ROUTE = re.compile( '^(\S\S?\*?\s?\S?\S?)' + '\s+' + '((?P<subnet>\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)' + '\s?' + '(?P<maskOrPrefixLength>(\/\d\d?)?' +'|(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)?))' + '\s*' + '(?P<viaPortion>(?:\n?\s+(\[\d\d?\d?\/\d+\])\s+' + 'via\s+(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)(.*)\n?)+)', re.MULTILINE ) # Route string VIA portion matching. REGEXP_VIA_PORTION = re.compile( '.*via\s+(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?).*' ) # Store for 'router' objects generated from input routing table files. # Each file is represented by single 'router' object. # Router is referenced by Router ID (RID). # RID is filename by default. # Format: # # ROUTERS = { # 'RID1': {'routing_table': {}, 'interface_list': ()}, # 'RID_N': {'routing_table': {}, 'interface_list': ()}, # } # ROUTERS = {} # Global search tree for Interface IP address to Router ID (RID) resolving. # Stores Interface IP addresses as keys. # Returns (RID, interfaceID) list. # Interface IP addresses SHOULD be globally unique across inspected topology. GLOBAL_INTERFACE_TREE = SubnetTree.SubnetTree() def parse_show_ip_route_ios_like(raw_routing_table): """ Parser for routing table text output. Compatible with both Cisco IOS(IOS-XE) 'show ip route' and Cisco ASA 'show route' output format. Processes input text file and writes into Python data structures. Builds internal SubnetTree search tree in 'route_tree'. Generates local interface list for router in 'interface_list' Returns 'router' dictionary object with parsed data. """ router = {} route_tree = SubnetTree.SubnetTree() interface_list = [] # Parse Local and Connected route strings in text. for raw_route_string in REGEXP_ROUTE_LOCAL_CONNECTED.finditer(raw_routing_table): subnet = ( raw_route_string.group('ipaddress') + convert_netmask_to_prefix_length( raw_route_string.group('maskOrPrefixLength') ) ) interface = raw_route_string.group('interface') route_tree[subnet] = ((interface,), raw_route_string.group(0)) if raw_route_string.group('routeType') == 'L': interface_list.append((interface, subnet,)) if not interface_list: print('Failed to find routing table entries in given output') return None # parse static and dynamic route strings in text for raw_route_string in REGEXP_ROUTE.finditer(raw_routing_table): subnet = ( raw_route_string.group('subnet') + convert_netmask_to_prefix_length( raw_route_string.group('maskOrPrefixLength') ) ) via_portion = raw_route_string.group('viaPortion') next_hops= [] if via_portion.count('via') > 1: for line in via_portion.splitlines(): if line: next_hops.append(REGEXP_VIA_PORTION.match(line).group(1)) else: next_hops.append(REGEXP_VIA_PORTION.match(via_portion).group(1)) route_tree[subnet] = (next_hops, raw_route_string.group(0)) router = { 'routing_table': route_tree, 'interface_list': interface_list, } return router def parse_text_routing_table(raw_routing_table): """ Parser functions wrapper. Add additional parsers for alternative routing table syntaxes here. """ router = parse_show_ip_route_ios_like(raw_routing_table) if router: return router def convert_netmask_to_prefix_length(mask_or_pref): """ Gets subnet_mask (XXX.XXX.XXX.XXX) of /prefix_length (/XX). For subnet_mask, converts it to /prefix_length and returns result. For /prefix_length, returns as is. For empty input, returns "" string. """ if not mask_or_pref: return "" if re.match("^\/\d\d?$", mask_or_pref): return mask_or_pref if re.match("^\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?$", mask_or_pref): return ( "/" + str(sum([bin(int(x)).count("1") for x in mask_or_pref.split(".")])) ) return "" def route_lookup(destination, router): """ Performs route_tree lookup in passed router object for passed destination subnet. Returns list of next_hops with original route strings or (None,None) depending on lookup result. """ if destination in router['routing_table']: return router['routing_table'][destination] else: return (None, None) def get_rid_by_interface_ip(interface_ip): """Returns RouterID by Interface IP address which it belongs to.""" if interface_ip in GLOBAL_INTERFACE_TREE: return GLOBAL_INTERFACE_TREE[interface_ip][0] def nexthop_is_local(next_hop): """ Check if nexthop points to local interface. Will be True for Connected and Local route strings on Cisco devices. """ interface_types = ('Eth', 'Fast', 'Gig', 'Ten', 'Port', 'Serial', 'Vlan', 'Tunn', 'Loop', 'Null' ) for type in interface_types: if next_hop.startswith(type): return True def trace_route(source_router_id, target_ip, path=[]): """ Performs recursive path search from source Router ID (RID) to target subnet. Returns tuple of path tuples. Each path tuple contains a sequence of Router IDs with matched route strings. Multiple paths are supported. """ if not source_router_id: return [path + [(None, None)]] current_router = ROUTERS[source_router_id] next_hop, raw_route_string = route_lookup(target_ip, current_router) path = path + [(source_router_id, raw_route_string)] paths = [] if next_hop: if nexthop_is_local(next_hop[0]): return [path] for nh in next_hop: next_hop_rid = get_rid_by_interface_ip(nh) if not next_hop_rid in [r[0] for r in path]: inner_path = trace_route(next_hop_rid, target_ip, path) for p in inner_path: paths.append(p) else: path = path + [(next_hop_rid+"<<LOOP DETECTED", None)] return [path] else: return [path] return paths def do_parse_directory(rt_directory): """ Go through specified directory and parse all .txt files. Generate router objects based on parse result if any. Populate new_routers with those router objects. Default key for each router object is FILENAME. Return new_routers. """ new_routers = {} if not os.path.isdir(rt_directory): print("{} directory does not exist.".format(rt_directory) + "Check rt_directory variable value." ) return None start_time = time() print("Initializing files...") for FILENAME in os.listdir(rt_directory): if FILENAME.endswith('.txt'): file_init_start_time = time() with open(os.path.join(rt_directory, FILENAME), 'r') as f: print ('Opening {}'.format(FILENAME)) raw_table = f.read() new_router = parse_text_routing_table(raw_table) router_id = FILENAME.replace('.txt', '') if new_router: new_routers[router_id] = new_router if new_router['interface_list']: for iface, addr in new_router['interface_list']: GLOBAL_INTERFACE_TREE[addr]= (router_id, iface,) else: print ('Failed to parse ' + FILENAME) print (FILENAME + " parsing has been completed in {} sec".format( "{:.3f}".format(time() - file_init_start_time)) ) else: if not new_routers: print ("Could not find any valid .txt files with routing tables" + " in {} directory".format(rt_directory) ) else: print ("\nAll files have been initialized" + " in {} sec".format("{:.3f}".format(time() - start_time)) ) return new_routers def do_user_interactive_search(): """ Provides interactive search dialog for user. Asks user for target subnet or host in CIDR notation. Validates input. Prints error and goes back to start for invalid input. Executes path search to given target from each router in global ROUTERS. Prints formatted path search result. Goes back to start. """ while True: print ('\n') target_subnet = raw_input('Enter Target Subnet or Host: ') if not target_subnet: continue if not REGEXP_INPUT_IPv4.match(target_subnet.replace(' ', '')): print ("incorrect input") continue lookup_start_time = time() for rtr in ROUTERS.keys(): subsearch_start_time = time() result = trace_route(rtr, target_subnet) if result: print ("\n") print ("PATHS TO {} FROM {}".format(target_subnet, rtr)) n = 1 print ('Detailed info:') for r in result: print ("Path {}:".format(n)) print ([h[0] for h in r]) for hop in r: print ("ROUTER: {}".format(hop[0])) print ("Matched route string: \n{}".format(hop[1])) else: print ('\n') n+=1 else: print ("Path search on {} has been completed in {} sec".format( rtr, "{:.3f}".format(time() - subsearch_start_time)) ) else: print ("\nFull search has been completed in {} sec".format( "{:.3f}".format(time() - lookup_start_time),) ) def main(): global ROUTERS ROUTERS = do_parse_directory(RT_DIRECTORY) if ROUTERS: do_user_interactive_search() if __name__ == "__main__": main() 

Script operation check


Armed with a small abstract topology of the four Cisco CSR-1000v :



They are connected in pairs through the GigabitEthernet 2 and 3 interfaces. An EIGRP neighborhood is established between them, through which all Connected networks are advertised, including the networks on the Loopback interfaces behind each router.
Between csr1000v-01 and csr1000v-04, two GRE-tunnels are additionally raised, through the remote IP addresses of which static routes to the 10.0.0.0/8 network are assigned for testing the routing loop.


csr1000v-01 # show ip route
 Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2 E1 - OSPF external type 1, E2 - OSPF external type 2 i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2 ia - IS-IS inter area, * - candidate default, U - per-user static route o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP a - application route + - replicated route, % - next hop override, p - overrides from PfR Gateway of last resort is not set S 10.0.0.0/8 [1/0] via 192.168.142.2 [1/0] via 192.168.141.2 172.16.0.0/16 is variably subnetted, 2 subnets, 2 masks C 172.16.114.0/24 is directly connected, GigabitEthernet2 L 172.16.114.5/32 is directly connected, GigabitEthernet2 192.168.2.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.2.0/24 is directly connected, GigabitEthernet1 L 192.168.2.201/32 is directly connected, GigabitEthernet1 192.168.12.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.12.0/24 is directly connected, GigabitEthernet2 L 192.168.12.201/32 is directly connected, GigabitEthernet2 192.168.13.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.13.0/24 is directly connected, GigabitEthernet3 L 192.168.13.201/32 is directly connected, GigabitEthernet3 D 192.168.24.0/24 [90/3072] via 192.168.12.202, 00:06:56, GigabitEthernet2 D 192.168.34.0/24 [90/3072] via 192.168.13.203, 00:06:56, GigabitEthernet3 192.168.141.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.141.0/30 is directly connected, Tunnel141 L 192.168.141.1/32 is directly connected, Tunnel141 192.168.142.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.142.0/30 is directly connected, Tunnel142 L 192.168.142.1/32 is directly connected, Tunnel142 192.168.201.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.201.0/24 is directly connected, Loopback201 L 192.168.201.201/32 is directly connected, Loopback201 D 192.168.202.0/24 [90/130816] via 192.168.12.202, 00:05:44, GigabitEthernet2 D 192.168.203.0/24 [90/130816] via 192.168.13.203, 00:06:22, GigabitEthernet3 D 192.168.204.0/24 [90/131072] via 192.168.13.203, 00:06:56, GigabitEthernet3 [90/131072] via 192.168.12.202, 00:06:56, GigabitEthernet2 

csr1000v-02 # show ip route
 Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2 E1 - OSPF external type 1, E2 - OSPF external type 2 i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2 ia - IS-IS inter area, * - candidate default, U - per-user static route o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP a - application route + - replicated route, % - next hop override, p - overrides from PfR Gateway of last resort is not set 192.168.2.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.2.0/24 is directly connected, GigabitEthernet1 L 192.168.2.202/32 is directly connected, GigabitEthernet1 192.168.12.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.12.0/24 is directly connected, GigabitEthernet2 L 192.168.12.202/32 is directly connected, GigabitEthernet2 D 192.168.13.0/24 [90/3072] via 192.168.12.201, 00:46:17, GigabitEthernet2 192.168.24.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.24.0/24 is directly connected, GigabitEthernet3 L 192.168.24.202/32 is directly connected, GigabitEthernet3 D 192.168.34.0/24 [90/3072] via 192.168.24.204, 00:46:15, GigabitEthernet3 D 192.168.201.0/24 [90/130816] via 192.168.12.201, 00:36:59, GigabitEthernet2 192.168.202.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.202.0/24 is directly connected, Loopback202 L 192.168.202.202/32 is directly connected, Loopback202 D 192.168.203.0/24 [90/131072] via 192.168.24.204, 00:06:31, GigabitEthernet3 [90/131072] via 192.168.12.201, 00:06:31, GigabitEthernet2 D 192.168.204.0/24 [90/130816] via 192.168.24.204, 00:37:26, GigabitEthernet3 

csr1000v-03 # show ip route
 Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2 E1 - OSPF external type 1, E2 - OSPF external type 2 i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2 ia - IS-IS inter area, * - candidate default, U - per-user static route o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP a - application route + - replicated route, % - next hop override, p - overrides from PfR Gateway of last resort is not set 192.168.2.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.2.0/24 is directly connected, GigabitEthernet1 L 192.168.2.203/32 is directly connected, GigabitEthernet1 D 192.168.12.0/24 [90/3072] via 192.168.13.201, 00:46:12, GigabitEthernet3 192.168.13.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.13.0/24 is directly connected, GigabitEthernet3 L 192.168.13.203/32 is directly connected, GigabitEthernet3 D 192.168.24.0/24 [90/3072] via 192.168.34.204, 00:46:12, GigabitEthernet2 192.168.34.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.34.0/24 is directly connected, GigabitEthernet2 L 192.168.34.203/32 is directly connected, GigabitEthernet2 D 192.168.201.0/24 [90/130816] via 192.168.13.201, 00:36:56, GigabitEthernet3 D 192.168.202.0/24 [90/131072] via 192.168.34.204, 00:05:51, GigabitEthernet2 [90/131072] via 192.168.13.201, 00:05:51, GigabitEthernet3 192.168.203.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.203.0/24 is directly connected, Loopback203 L 192.168.203.203/32 is directly connected, Loopback203 D 192.168.204.0/24 [90/130816] via 192.168.34.204, 00:37:22, GigabitEthernet2 

csr1000v-04#show ip route
 Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2 E1 - OSPF external type 1, E2 - OSPF external type 2 i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2 ia - IS-IS inter area, * - candidate default, U - per-user static route o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP a - application route + - replicated route, % - next hop override, p - overrides from PfR Gateway of last resort is not set S 10.0.0.0/8 [1/0] via 192.168.142.1 [1/0] via 192.168.141.1 192.168.2.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.2.0/24 is directly connected, GigabitEthernet1 L 192.168.2.204/32 is directly connected, GigabitEthernet1 D 192.168.12.0/24 [90/3072] via 192.168.24.202, 00:46:17, GigabitEthernet3 D 192.168.13.0/24 [90/3072] via 192.168.34.203, 00:46:19, GigabitEthernet2 192.168.24.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.24.0/24 is directly connected, GigabitEthernet3 L 192.168.24.204/32 is directly connected, GigabitEthernet3 192.168.34.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.34.0/24 is directly connected, GigabitEthernet2 L 192.168.34.204/32 is directly connected, GigabitEthernet2 192.168.141.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.141.0/30 is directly connected, Tunnel141 L 192.168.141.2/32 is directly connected, Tunnel141 192.168.142.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.142.0/30 is directly connected, Tunnel142 L 192.168.142.2/32 is directly connected, Tunnel142 D 192.168.201.0/24 [90/131072] via 192.168.34.203, 00:37:02, GigabitEthernet2 [90/131072] via 192.168.24.202, 00:37:02, GigabitEthernet3 D 192.168.202.0/24 [90/130816] via 192.168.24.202, 00:05:57, GigabitEthernet3 D 192.168.203.0/24 [90/130816] via 192.168.34.203, 00:06:34, GigabitEthernet2 192.168.204.0/24 is variably subnetted, 2 subnets, 2 masks C 192.168.204.0/24 is directly connected, Loopback204 L 192.168.204.204/32 is directly connected, Loopback204 

show ip route - ./routing_tables/ :


$ python2.7 traceRouteByShowIPRoute.py
 $ python2.7 traceRouteByShowIPRoute.py Initializing files... Opening csr1000v-01.txt csr1000v-01.txt parsing has been completed in 0.001 sec Opening csr1000v-02.txt csr1000v-02.txt parsing has been completed in 0.001 sec Opening csr1000v-03.txt csr1000v-03.txt parsing has been completed in 0.001 sec Opening csr1000v-04.txt csr1000v-04.txt parsing has been completed in 0.001 sec All files have been initialized in 0.003 sec Enter Target Subnet or Host: 

, , , IP- .
.


Loopback204 192.168.204.204 csr1000v-04

IP- .


Enter Target Subnet or Host: 192.168.204.204
 Enter Target Subnet or Host: 192.168.204.204 PATHS TO 192.168.204.204 FROM csr1000v-04 Detailed info: Path 1: ['csr1000v-04'] ROUTER: csr1000v-04 Matched route string: L 192.168.204.204/32 is directly connected, Loopback204 Path search on csr1000v-04 has been completed in 0.000 sec PATHS TO 192.168.204.204 FROM csr1000v-03 Detailed info: Path 1: ['csr1000v-03', 'csr1000v-04'] ROUTER: csr1000v-03 Matched route string: D 192.168.204.0/24 [90/130816] via 192.168.34.204, 00:37:22, GigabitEthernet2 ROUTER: csr1000v-04 Matched route string: L 192.168.204.204/32 is directly connected, Loopback204 Path search on csr1000v-03 has been completed in 0.000 sec PATHS TO 192.168.204.204 FROM csr1000v-02 Detailed info: Path 1: ['csr1000v-02', 'csr1000v-04'] ROUTER: csr1000v-02 Matched route string: D 192.168.204.0/24 [90/130816] via 192.168.24.204, 00:37:26, GigabitEthernet3 ROUTER: csr1000v-04 Matched route string: L 192.168.204.204/32 is directly connected, Loopback204 Path search on csr1000v-02 has been completed in 0.000 sec PATHS TO 192.168.204.204 FROM csr1000v-01 Detailed info: Path 1: ['csr1000v-01', 'csr1000v-03', 'csr1000v-04'] ROUTER: csr1000v-01 Matched route string: D 192.168.204.0/24 [90/131072] via 192.168.13.203, 00:06:56, GigabitEthernet3 [90/131072] via 192.168.12.202, 00:06:56, GigabitEthernet2 ROUTER: csr1000v-03 Matched route string: D 192.168.204.0/24 [90/130816] via 192.168.34.204, 00:37:22, GigabitEthernet2 ROUTER: csr1000v-04 Matched route string: L 192.168.204.204/32 is directly connected, Loopback204 Path 2: ['csr1000v-01', 'csr1000v-02', 'csr1000v-04'] ROUTER: csr1000v-01 Matched route string: D 192.168.204.0/24 [90/131072] via 192.168.13.203, 00:06:56, GigabitEthernet3 [90/131072] via 192.168.12.202, 00:06:56, GigabitEthernet2 ROUTER: csr1000v-02 Matched route string: D 192.168.204.0/24 [90/130816] via 192.168.24.204, 00:37:26, GigabitEthernet3 ROUTER: csr1000v-04 Matched route string: L 192.168.204.204/32 is directly connected, Loopback204 Path search on csr1000v-01 has been completed in 0.000 sec Full search has been completed in 0.001 sec 

, . :


csr1000v-01#show ip route 192.168.204.204
 csr1000v-01#show ip route 192.168.204.204 Routing entry for 192.168.204.0/24 Known via "eigrp 200", distance 90, metric 131072, type internal Redistributing via eigrp 200 Last update from 192.168.13.203 on GigabitEthernet3, 00:02:15 ago Routing Descriptor Blocks: 192.168.13.203, from 192.168.13.203, 00:02:15 ago, via GigabitEthernet3 Route metric is 131072, traffic share count is 1 Total delay is 5020 microseconds, minimum bandwidth is 1000000 Kbit Reliability 255/255, minimum MTU 1500 bytes Loading 1/255, Hops 2 * 192.168.12.202, from 192.168.12.202, 00:02:15 ago, via GigabitEthernet2 Route metric is 131072, traffic share count is 1 Total delay is 5020 microseconds, minimum bandwidth is 1000000 Kbit Reliability 255/255, minimum MTU 1500 bytes Loading 1/255, Hops 2 

csr1000v-01 equal-cost EIGRP csr1000v-02 csr1000v-03.
, : ['csr1000v-01', 'csr1000v-03', 'csr1000v-04'] ['csr1000v-01', 'csr1000v-02', 'csr1000v-04'].


csr1000v-02#show ip route 192.168.204.204
 csr1000v-02#show ip route 192.168.204.204 Routing entry for 192.168.204.0/24 Known via "eigrp 200", distance 90, metric 130816, type internal Redistributing via eigrp 200 Last update from 192.168.24.204 on GigabitEthernet3, 00:08:48 ago Routing Descriptor Blocks: * 192.168.24.204, from 192.168.24.204, 00:08:48 ago, via GigabitEthernet3 Route metric is 130816, traffic share count is 1 Total delay is 5010 microseconds, minimum bandwidth is 1000000 Kbit Reliability 255/255, minimum MTU 1500 bytes Loading 1/255, Hops 1 

csr1000v-03#show ip route 192.168.204.204
 csr1000v-3#show ip route 192.168.204.204 Routing entry for 192.168.204.0/24 Known via "eigrp 200", distance 90, metric 130816, type internal Redistributing via eigrp 200 Last update from 192.168.34.204 on GigabitEthernet2, 00:08:45 ago Routing Descriptor Blocks: * 192.168.34.204, from 192.168.34.204, 00:08:45 ago, via GigabitEthernet2 Route metric is 130816, traffic share count is 1 Total delay is 5010 microseconds, minimum bandwidth is 1000000 Kbit Reliability 255/255, minimum MTU 1500 bytes Loading 1/255, Hops 1 

csr1000v-2 csr1000v-3 EIGRP csr1000v-4.
, : ['csr1000v-02', 'csr1000v-04'] ['csr1000v-03', 'csr1000v-04'] .


csr1000v-04#show ip route 192.168.204.204
 csr1000v-04#show ip route 192.168.204.204 Routing entry for 192.168.204.204/32 Known via "connected", distance 0, metric 0 (connected) Routing Descriptor Blocks: * directly connected, via Loopback204 Route metric is 0, traffic share count is 1 

csr1000v-4 Connnected Loopback204.
Local : ['csr1000v-04'].


10.10.10.0/24 ( )
Enter Target Subnet or Host: 10.10.10.0/24
 Enter Target Subnet or Host: 10.10.10.0/24 PATHS TO 10.10.10.0/24 FROM csr1000v-04 Detailed info: Path 1: ['csr1000v-04', 'csr1000v-01', 'csr1000v-04<<LOOP DETECTED'] ROUTER: csr1000v-04 Matched route string: S 10.0.0.0/8 [1/0] via 192.168.142.1 [1/0] via 192.168.141.1 ROUTER: csr1000v-01 Matched route string: S 10.0.0.0/8 [1/0] via 192.168.142.2 [1/0] via 192.168.141.2 ROUTER: csr1000v-04<<LOOP DETECTED Matched route string: None Path 2: ['csr1000v-04', 'csr1000v-01', 'csr1000v-04<<LOOP DETECTED'] ROUTER: csr1000v-04 Matched route string: S 10.0.0.0/8 [1/0] via 192.168.142.1 [1/0] via 192.168.141.1 ROUTER: csr1000v-01 Matched route string: S 10.0.0.0/8 [1/0] via 192.168.142.2 [1/0] via 192.168.141.2 ROUTER: csr1000v-04<<LOOP DETECTED Matched route string: None Path search on csr1000v-04 has been completed in 0.000 sec PATHS TO 10.10.10.0/24 FROM csr1000v-03 Detailed info: Path 1: ['csr1000v-03'] ROUTER: csr1000v-03 Matched route string: None Path search on csr1000v-03 has been completed in 0.000 sec PATHS TO 10.10.10.0/24 FROM csr1000v-02 Detailed info: Path 1: ['csr1000v-02'] ROUTER: csr1000v-02 Matched route string: None Path search on csr1000v-02 has been completed in 0.000 sec PATHS TO 10.10.10.0/24 FROM csr1000v-01 Detailed info: Path 1: ['csr1000v-01', 'csr1000v-04', 'csr1000v-01<<LOOP DETECTED'] ROUTER: csr1000v-01 Matched route string: S 10.0.0.0/8 [1/0] via 192.168.142.2 [1/0] via 192.168.141.2 ROUTER: csr1000v-04 Matched route string: S 10.0.0.0/8 [1/0] via 192.168.142.1 [1/0] via 192.168.141.1 ROUTER: csr1000v-01<<LOOP DETECTED Matched route string: None Path 2: ['csr1000v-01', 'csr1000v-04', 'csr1000v-01<<LOOP DETECTED'] ROUTER: csr1000v-01 Matched route string: S 10.0.0.0/8 [1/0] via 192.168.142.2 [1/0] via 192.168.141.2 ROUTER: csr1000v-04 Matched route string: S 10.0.0.0/8 [1/0] via 192.168.142.1 [1/0] via 192.168.141.1 ROUTER: csr1000v-01<<LOOP DETECTED Matched route string: None Path search on csr1000v-01 has been completed in 0.003 sec Full search has been completed in 0.004 sec 

.
:


csr1000v-01#show ip route 10.10.10.0 255.255.255.0
 csr1000v-01#show ip route 10.10.10.0 255.255.255.0 Routing entry for 10.0.0.0/8 Known via "static", distance 1, metric 0 Routing Descriptor Blocks: * 192.168.142.2 Route metric is 0, traffic share count is 1 192.168.141.2 Route metric is 0, traffic share count is 1 

csr1000v-04#show ip route 10.10.10.0 255.255.255.0
 csr1000v-04#show ip route 10.10.10.0 255.255.255.0 Routing entry for 10.0.0.0/8 Known via "static", distance 1, metric 0 Routing Descriptor Blocks: 192.168.142.1 Route metric is 0, traffic share count is 1 * 192.168.141.1 Route metric is 0, traffic share count is 1 

csr1000v-01 csr1000v-04 GRE- 10.0.0.0/8. .
:


 PATHS TO 10.10.10.0/24 FROM csr1000v-01 Path 1: ['csr1000v-01', 'csr1000v-04', 'csr1000v-01<<LOOP DETECTED'] Path 2: ['csr1000v-01', 'csr1000v-04', 'csr1000v-01<<LOOP DETECTED'] PATHS TO 10.10.10.0/24 FROM csr1000v-04 Path 1: ['csr1000v-04', 'csr1000v-01', 'csr1000v-04<<LOOP DETECTED'] Path 2: ['csr1000v-04', 'csr1000v-01', 'csr1000v-04<<LOOP DETECTED'] 

csr1000v-02#show ip route 10.10.10.0 255.255.255.0
 csr1000v-02#show ip route 10.10.10.0 255.255.255.0 % Network not in table 

csr1000v-3#show ip route 10.10.10.0 255.255.255.0
 csr1000v-3#show ip route 10.10.10.0 255.255.255.0 % Network not in table 

csr1000v-02 csr1000v-03 . .


, , , .


Conclusion


, . MacBook Pro Intel Core i5 8GB RAM 700.000+ 6.85 ( 100 ). 320-350.
( ).


. IPv6 SubnetTree .
Python3, , raw_input input .


, , — , " " , " ", ..


Policy Based Routing ( PBR ) . , .


, - .
Thanks to everyone who read to the end.

Source: https://habr.com/ru/post/414043/


All Articles