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:
- There is a set of text files containing the output of the routing tables from various network devices;
- Each file contains information from one device;
- Devices may have a different output format for the routing table;
- It is necessary to derive the path to an arbitrary subnet or IP address from each device on the basis of the available data on request;
- The output should include in each section of the path information about the record from the routing table, which will be routed packet.
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 under the Cisco IOS, IOS-XE and ASA format that 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 classbut 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 the solution algorithm
Given the introductory, the task can be divided into two main parts: parsing the source files with the routing tables and the direct search for the path using the 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 prefix of the greatest length (longest prefix match).
As one of the solutions to quickly find such a match, you can construct a prefix tree from the source data. Since we have no restrictions on external dependencies, we ’ll focus 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 hardware 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, approximately 500MB RAM is required. 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 split 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:
S* 0.0.0.0/0 [1/0] via 10.220.88.110.0.0.0/8is variably subnetted, 2 subnets, 2 masks
C 10.220.88.0/24is directly connected, FastEthernet4
L 10.220.88.20/32is directly connected, FastEthernet4
1.0.0.0/32is 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/16is subnetted, 1 subnets
O E2 4.4.0.0 [110/20] via 194.0.0.2, 00:02:00, FastEthernet0/05.0.0.0/24is subnetted, 1 subnets
D EX 5.5.5.0 [170/2297856] via 10.0.1.2, 00:12:01, Serial0/06.0.0.0/16is subnetted, 1 subnets
B 6.6.0.0 [200/0] via 195.0.0.1, 00:00:04172.16.0.0/26is subnetted, 1 subnets
i L2 172.16.1.0 [115/10] via 10.0.1.2, Serial0/0172.20.0.0/32is 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/010.0.0.0/8is variably subnetted, 5 subnets, 3 masks
C 10.0.1.0/24is 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/0192.168.0.0/32is 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/16is directly connected, FastEthernet0/0
In Cisco ASA, the format is similar, but instead of the prefix lengths, the subnet mask is displayed in decimal representation:
S 10.1.1.0 255.255.255.0[3/0]via 10.86.194.1, outsideC 10.86.194.0 255.255.254.0isdirectlyconnected, outsideS* 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.
There are 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 complicates the matter somewhat. 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 restrictions:
# 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, a prefix ( subnet / interface and maskOrPrefixLength groups ) and information about where it leads ( viaPortion / interface groups ) must be obtained from each route .
Since the expressions take into account several variants of the prefix representation, the maskOrPrefixLength group can contain both a subnet mask and a prefix length. We will bring this to a single format when processing, dwell on the length of the prefix:
defconvert_netmask_to_prefix_length(mask_or_pref):ifnot 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""
Let's also add 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 will be moved to a separate function:
defroute_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 search for 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 returned sheets with a router identifier 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.defget_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 dictionary dictionary router in the format specified above.
defparse_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,))
ifnot interface_list:
print('Failed to find routing table entries in given output')
returnNone# parse static and dynamic route strings in textfor 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):
defparse_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:
defdo_parse_directory(rt_directory):
new_routers = {}
ifnot os.path.isdir(rt_directory):
print("{} directory does not exist.".format(rt_directory)
+ "Check rt_directory variable value."
)
returnNone
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:
ifnot 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 a 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 of 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.
deftrace_route(source_router_id, target_ip, path=[]):ifnot 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)
ifnot 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
defnexthop_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):
returnTrue
Add a function to start a search online after initializing text files:
defdo_user_interactive_search():whileTrue:
print ('\n')
target_subnet = raw_input('Enter Target Subnet or Host: ')
ifnot target_subnet:
continueifnot 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 = 1print ('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+=1else:
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:
defmain():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.
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()
defparse_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,))
ifnot interface_list:
print('Failed to find routing table entries in given output')
returnNone# parse static and dynamic route strings in textfor 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
defparse_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
defconvert_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.
"""ifnot 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""defroute_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)
defget_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]
defnexthop_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):
returnTruedeftrace_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.
"""ifnot 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)
ifnot 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
defdo_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 = {}
ifnot os.path.isdir(rt_directory):
print("{} directory does not exist.".format(rt_directory)
+ "Check rt_directory variable value."
)
returnNone
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:
ifnot 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
defdo_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.
"""whileTrue:
print ('\n')
target_subnet = raw_input('Enter Target Subnet or Host: ')
ifnot target_subnet:
continueifnot 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 = 1print ('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+=1else:
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),)
)
defmain():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 via GigabitEthernet 2 and 3 interfaces. An EIGRP adjacency 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.
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 externaltype1, N2 - OSPF NSSA externaltype2
E1 - OSPF externaltype1, E2 - OSPF externaltype2
i - IS-IS, su - IS-ISsummary, L1 - IS-ISlevel-1, L2 - IS-ISlevel-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 isnotset
S 10.0.0.0/8 [1/0] via 192.168.142.2
[1/0] via 192.168.141.2172.16.0.0/16is variably subnetted, 2 subnets, 2 masks
C 172.16.114.0/24is directly connected, GigabitEthernet2
L 172.16.114.5/32is directly connected, GigabitEthernet2
192.168.2.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.2.0/24is directly connected, GigabitEthernet1
L 192.168.2.201/32is directly connected, GigabitEthernet1
192.168.12.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.12.0/24is directly connected, GigabitEthernet2
L 192.168.12.201/32is directly connected, GigabitEthernet2
192.168.13.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.13.0/24is directly connected, GigabitEthernet3
L 192.168.13.201/32is 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/24is variably subnetted, 2 subnets, 2 masks
C 192.168.141.0/30is directly connected, Tunnel141
L 192.168.141.1/32is directly connected, Tunnel141
192.168.142.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.142.0/30is directly connected, Tunnel142
L 192.168.142.1/32is directly connected, Tunnel142
192.168.201.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.201.0/24is directly connected, Loopback201
L 192.168.201.201/32is 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
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 externaltype1, N2 - OSPF NSSA externaltype2
E1 - OSPF externaltype1, E2 - OSPF externaltype2
i - IS-IS, su - IS-ISsummary, L1 - IS-ISlevel-1, L2 - IS-ISlevel-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 isnotset192.168.2.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.2.0/24is directly connected, GigabitEthernet1
L 192.168.2.202/32is directly connected, GigabitEthernet1
192.168.12.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.12.0/24is directly connected, GigabitEthernet2
L 192.168.12.202/32is directly connected, GigabitEthernet2
D 192.168.13.0/24 [90/3072] via 192.168.12.201, 00:46:17, GigabitEthernet2
192.168.24.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.24.0/24is directly connected, GigabitEthernet3
L 192.168.24.202/32is 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/24is variably subnetted, 2 subnets, 2 masks
C 192.168.202.0/24is directly connected, Loopback202
L 192.168.202.202/32is 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
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 externaltype1, N2 - OSPF NSSA externaltype2
E1 - OSPF externaltype1, E2 - OSPF externaltype2
i - IS-IS, su - IS-ISsummary, L1 - IS-ISlevel-1, L2 - IS-ISlevel-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 isnotset192.168.2.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.2.0/24is directly connected, GigabitEthernet1
L 192.168.2.203/32is directly connected, GigabitEthernet1
D 192.168.12.0/24 [90/3072] via 192.168.13.201, 00:46:12, GigabitEthernet3
192.168.13.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.13.0/24is directly connected, GigabitEthernet3
L 192.168.13.203/32is directly connected, GigabitEthernet3
D 192.168.24.0/24 [90/3072] via 192.168.34.204, 00:46:12, GigabitEthernet2
192.168.34.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.34.0/24is directly connected, GigabitEthernet2
L 192.168.34.203/32is 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/24is variably subnetted, 2 subnets, 2 masks
C 192.168.203.0/24is directly connected, Loopback203
L 192.168.203.203/32is directly connected, Loopback203
D 192.168.204.0/24
[90/130816] via 192.168.34.204, 00:37:22, GigabitEthernet2
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 externaltype1, N2 - OSPF NSSA externaltype2
E1 - OSPF externaltype1, E2 - OSPF externaltype2
i - IS-IS, su - IS-ISsummary, L1 - IS-ISlevel-1, L2 - IS-ISlevel-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 isnotset
S 10.0.0.0/8 [1/0] via 192.168.142.1
[1/0] via 192.168.141.1192.168.2.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.2.0/24is directly connected, GigabitEthernet1
L 192.168.2.204/32is 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/24is variably subnetted, 2 subnets, 2 masks
C 192.168.24.0/24is directly connected, GigabitEthernet3
L 192.168.24.204/32is directly connected, GigabitEthernet3
192.168.34.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.34.0/24is directly connected, GigabitEthernet2
L 192.168.34.204/32is directly connected, GigabitEthernet2
192.168.141.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.141.0/30is directly connected, Tunnel141
L 192.168.141.2/32is directly connected, Tunnel141
192.168.142.0/24is variably subnetted, 2 subnets, 2 masks
C 192.168.142.0/30is directly connected, Tunnel142
L 192.168.142.2/32is 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/24is variably subnetted, 2 subnets, 2 masks
C 192.168.204.0/24is directly connected, Loopback204
L 192.168.204.204/32is directly connected, Loopback204
Save these show ip route pins into separate files in the ./routing_tables/ sub-directory and run the script:
$ python2.7 traceRouteByShowIPRoute.py
Initializing files...
Opening csr1000v-01.txt
csr1000v-01.txt parsing has been completed in0.001 sec
Opening csr1000v-02.txt
csr1000v-02.txt parsing has been completed in0.001 sec
Opening csr1000v-03.txt
csr1000v-03.txt parsing has been completed in0.001 sec
Opening csr1000v-04.txt
csr1000v-04.txt parsing has been completed in0.001 sec
All files have been initialized in0.003 sec
Enter Target Subnet orHost:
All files are initialized, the script, as expected, requests an IP address or subnet to find the path to it from each of the routers.
Let us introduce in turn several options and verify the result obtained with information from the routers themselves.
Paths to this IP address must be from all available routers.
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
All paths are found, routes are derived. Check with routers:
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 updatefrom192.168.13.203on GigabitEthernet3, 00:02:15 ago
Routing Descriptor Blocks:
192.168.13.203, from192.168.13.203, 00:02:15 ago, via GigabitEthernet3
Route metric is131072, traffic sharecountis1
Total delay is5020 microseconds, minimum bandwidth is1000000 Kbit
Reliability 255/255, minimum MTU 1500bytes
Loading 1/255, Hops 2
* 192.168.12.202, from192.168.12.202, 00:02:15 ago, via GigabitEthernet2
Route metric is131072, traffic sharecountis1
Total delay is5020 microseconds, minimum bandwidth is1000000 Kbit
Reliability 255/255, minimum MTU 1500bytes
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
Routing entry for 192.168.204.0/24
Known via "eigrp 200", distance 90, metric 130816, type internal
Redistributing via eigrp 200
Last updatefrom192.168.24.204on GigabitEthernet3, 00:08:48 ago
Routing Descriptor Blocks:
* 192.168.24.204, from192.168.24.204, 00:08:48 ago, via GigabitEthernet3
Route metric is130816, traffic sharecountis1
Total delay is5010 microseconds, minimum bandwidth is1000000 Kbit
Reliability 255/255, minimum MTU 1500bytes
Loading 1/255, Hops 1
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 updatefrom192.168.34.204on GigabitEthernet2, 00:08:45 ago
Routing Descriptor Blocks:
* 192.168.34.204, from192.168.34.204, 00:08:45 ago, via GigabitEthernet2
Route metric is130816, traffic sharecountis1
Total delay is5010 microseconds, minimum bandwidth is1000000 Kbit
Reliability 255/255, minimum MTU 1500bytes
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.204Routing entry for192.168.204.204/32Known via "connected", distance0, metric 0 (connected)
RoutingDescriptorBlocks:
* directly connected, via Loopback204Route metric is0, traffic share countis1
На csr1000v-4 это Connnected сеть на интерфейсе Loopback204.
Скрипт показал Local маршрут и путь на самого себя: ['csr1000v-04'].
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.0255.255.255.0Routing entry for10.0.0.0/8Known via "static", distance1, metric 0RoutingDescriptorBlocks:
* 192.168.142.2Route metric is0, traffic share countis1192.168.141.2
Also popular now:
-
VR - Issue # 42
-
Updating the working draft on the server.
-
Unclesoсky podcast - Issue # 27
-
Vinyl CDs and CDs - Floppy Disks
-
Invites to dcdnet.ru
-
Darwinism and web programming
-
Ruby on Rails Web File Download Indicator
-
Question: Linux and Java-based IDE
-
Google Geo - Big data update in Google Earth, underwater models in Sketh Up
-
Isn't it time to stop?