def runstuff(): NUM_SWITCHES = 20 # total number of switches in the complete graph of switch-port connectivity NUM_CLIENTS = 2 # number of clients on every switch. large numbers will decrease the size of sccs, because # forwarding to clients is always safe; not part of a loop. RULES_PER_ROUND = 350 # number of rule installs to do between gathering time statistics. MAX_TOTAL_RULES_INSTALLED = 450 # number of total rules to maintain in the network model, so # that as more than these come in, we evict the oldest ones. TOTAL_RULE_INSTALLATIONS_BEFORE_HALT = 350 # halt after this many total rule installations. SHOW_TRACE = False # should we show the trace when loops are detected? This can get hectic. MIN_IN = 1 # minininum number of in_ports to be used in random rules. MAX_IN = 3 # etc. MIN_OUT = 1 MAX_OUT = 2 # the network that we'kll use to generate random valid rules net = TestNetwork1(NUM_SWITCHES, NUM_CLIENTS, minswitch=5555, minport=100001) # our model n = NetworkFlowruleModel() # track the flow install rate in this timestats = StatsBuddy() total_installed = 0 # track the rules in place in our NetworkFlowruleModel so we can evict them q = Queue.Queue() while True: #start a timer t1 = time.time() # do some rule installations for r in xrange(RULES_PER_ROUND): if q.qsize() > MAX_TOTAL_RULES_INSTALLED: droprule = q.get() # get the number of the rule that's over the limit n.drop_flow_rule(droprule) # kill the rule rule = net.get_random_rule(MIN_IN, MAX_IN, MIN_OUT, MAX_OUT) (rnum, trace) = n.install_flow_rule(rule) if SHOW_TRACE and trace: NetworkFlowruleModel.explain(trace) q.put(rnum) total_installed += 1 t2 = time.time() # NOTE: linux might report this in ms instead of seconds, I think, # so this might give weird results timestats.add(RULES_PER_ROUND/(t2-t1)) # how we eventually halt if total_installed >= TOTAL_RULE_INSTALLATIONS_BEFORE_HALT: break print timestats, 'are stats for the number of rule installs and evictions per second.' print n.sccsizestats, 'are stats for the average scc size of installed rules.' return
def __init__(self): self.in_port_rules = defaultdict(lambda: []) self.out_port_rules = defaultdict(lambda: []) self.rulenum_to_rule = {} self.rulenum_to_inspace = {} self.rulenum_to_outspace = {} self.rulenum_to_in_ports = {} self.rulenum_to_out_ports = {} self.dscc = DynamicSCC() # for tracking the number of components in sccs for newly added rules self._collect_stats = False self.scc_size_stats = StatsBuddy() self.scc_buckets = None self.edges_stats = StatsBuddy() self.loops_detected = 0 self.loop_detection_calls = 0 # first installed flow rule will be number 0 # iterated whenever a flow is installed, so no overlap is possible self._r = 0
class NetworkFlowruleModel(object): ''' Build an object that can tell us whether the ruleset can permit looping in the network. Flow rules are to be provided in the form of the result of a call to TF.create_standard_rule(in_ports, match, out_ports, mask, rewrite,file_name,lines) where all referenced port numbers refer model distinct unidirectional physical connections between switches. This class does not necessarily check that the rules are sane. That is, you can (unsafely) add flow rules that are not physically possible, for instance redirecting packets that arrive on more than one switch or that do not have the in ports and out ports on the same physical router. ''' def __init__(self): self.in_port_rules = defaultdict(lambda: []) self.out_port_rules = defaultdict(lambda: []) self.rulenum_to_rule = {} self.rulenum_to_inspace = {} self.rulenum_to_outspace = {} self.rulenum_to_in_ports = {} self.rulenum_to_out_ports = {} self.dscc = DynamicSCC() # for tracking the number of components in sccs for newly added rules self._collect_stats = False self.scc_size_stats = StatsBuddy() self.scc_buckets = None self.edges_stats = StatsBuddy() self.loops_detected = 0 self.loop_detection_calls = 0 # first installed flow rule will be number 0 # iterated whenever a flow is installed, so no overlap is possible self._r = 0 def install_flow_rule(self, rule): # we use the flow rule number (assigned from 0 as rules are entered) as the vertex newrnum = self._r self._r += 1 self.rulenum_to_rule[newrnum] = rule # update the in/out_port_rules dictionaries ,so we can easily detect # who we have to test for intersection with this rule later # HACK. This uses a poorly understood backdoor into the TF class # by asking for in/outport_to_rule.keys(), when there may be situations # (I'm not sure, OK?) when it doesn't provide the right answers inports = [int(p) for p in rule.inport_to_rule.keys()] self.rulenum_to_in_ports[newrnum] = inports outports = [int(p) for p in rule.outport_to_rule.keys()] self.rulenum_to_out_ports[newrnum] = outports for port in inports: self.in_port_rules[port].append(newrnum) for port in outports: self.out_port_rules[port].append(newrnum) # these are lists of pairs (headerspace, portlist) outspace = [item for port in inports for item in rule.T(HEADERSPACE_ALL, port)] inspace = [item for port in outports for item in rule.T_inv(HEADERSPACE_ALL, port)] self.rulenum_to_inspace[newrnum] = inspace self.rulenum_to_outspace[newrnum] = outspace # add this rule into the dynamic graph structure self.dscc.insert_vertices([newrnum]) # cases where there might be an edge (newrnum, ?) and below, (?, newrnum), since they in/out on the same port possible_to_rules = [r for outport in outports for r in self.in_port_rules[outport]] possible_from_rules = [r for inport in inports for r in self.out_port_rules[inport]] # cases where there is a headerspace intersection in the inputs and outputs real_to_rules = filter(lambda r: NetworkFlowruleModel.rule_spaces_intersect(outspace, self.rulenum_to_inspace[r]), possible_to_rules) #if len(real_to_rules) > 0: print real_to_rules real_from_rules = filter(lambda r: NetworkFlowruleModel.rule_spaces_intersect(inspace, self.rulenum_to_outspace[r]), possible_from_rules) #if len(real_from_rules) > 0: print real_from_rules # the real edges to insert newedges = set([(newrnum, to) for to in real_to_rules]) newedges.update(set([(frum, newrnum) for frum in real_from_rules])) #print "New edges are:", newedges # add the edges to our SCC detecting graph self.dscc.insert_edges(newedges) # how big is the scc we're in? newscc = self.dscc.getSCC(newrnum) sccsize = len(newscc) # process SCC statistics if self._collect_stats: self.edges_stats.add(len(newedges)) self.scc_size_stats.add(sccsize) self.scc_buckets.add(sccsize) if sccsize == 1: return (newrnum, False) ''' print newscc, 'is a set of rules that could loop' for rnum in newscc: print self.rulenum_to_rule[rnum] print self.rulenum_to_inspace[rnum] print self.rulenum_to_outspace[rnum] raise Exception("Quick Break") ''' # return the number of the inserted rule and some info info = self.find_loop_in_scc(newscc, newrnum) return (newrnum, info) ## end install_flow_rule def collect_stats(self, bool): ''' Turn collecting of SCC size statistics on or off ''' self._collect_stats = bool # collect scc buckets on the assumption that the current number of rules is about the most we'll see, unless we have none yet bmax = len(self.rulenum_to_rule) if len(self.rulenum_to_rule) > 20 else 200 self.scc_buckets = Buckets(0, bmax) def find_loop_in_scc(self, scc, rnum): q = Queue() # collect stats, it we should be doing so if self._collect_stats: self.loop_detection_calls += 1 # this was set when the rule was added initial_spaces = self.rulenum_to_outspace[rnum] # queue items look like ([(hs, [ports])], [visited_rules]) # we start with the full header space on the ports that the rule of interest outputs q.put( (initial_spaces, [rnum]) ) while not q.empty(): current_places, visited_rules = q.get() # this is to help reduce the depth of the recursion here if TRY_EFFICIENCY_TWEAK: current_places = rewrite_space(current_places) for space, ports in current_places: # sometimes empty spaces can be here. we can ignore them if space.is_empty(): continue for port in ports: for nextrule in scc: # if the current space routes into another rule in the scc if nextrule in self.in_port_rules[port]: currpath = visited_rules + [nextrule] # process space, port through nextrule out = self.rulenum_to_rule[nextrule].T(space, port) if len(out) == 0: continue # check if we have a loop here if nextrule in visited_rules: # we've visited this rule before, if we have a nonempty header left, we've found a loop for outspace, outports in out: if not outspace.is_empty(): # collect stats, it we should be doing so if self._collect_stats: self.loops_detected += 1 # backtrace this, ignoring the other possible loops original_space = self.backtrace([(outspace, outports)], currpath) ret = {'rule_path': currpath, 'headers_in': original_space, 'headers_out': [(outspace, outports)]} return ret continue # register another node to check q.put( (out, currpath) ) return False ## end find_loop_in_scc @staticmethod def explain(info): path = info['rule_path'] print '---Backtrace shows that packets can loop in your network:' for hs, ports in info['headers_in']: print '-- Port(s)', ports, 'with headers', hs print '-- They take the path of rules', path, 'in a loop.' print '-- And are emitted by rule', path[-1], 'at the end of the above loop as:' for hs, ports in info['headers_out']: print '-- Port(s)', ports, 'with headers', hs print '-- Other packets may also loop.' def backtrace(self, hspaces, rulelist): ''' Trace the behaviour of hspace travelling through rulelist _in reverse_, returning the original set of (hspace,ports) that could have caused the hspaces arg to have been emitted. ''' for rnum in reversed(rulelist): # trace hspace backwards through rule rnum rule = self.rulenum_to_rule[rnum] nextgen = [] for (hspace, ports) in hspaces: for port in ports: nextgen.extend(rule.T_inv(hspace, port)) hspaces = nextgen # hspaces is now the original ingress packets that had eventually # called the original hspaces to be ejected from the last rule in rulelist return hspaces ## end backtrace @staticmethod def rule_spaces_intersect(a, b): ''' Detemine whether there is a nonempty intersection between 2 (headerspace, portlist) lists ''' for (hs1, ports1) in a: sports1 = set(ports1) for (hs2, ports2) in b: sports2 = set(ports2) if len(sports1.intersection(sports2)) == 0: continue if not hs1.copy_intersect(hs2).is_empty(): return True return False def drop_flow_rule(self, rnum): '''Update all of the stored data to a state as if rule rnum never existed''' rule = self.rulenum_to_rule[rnum] del self.rulenum_to_rule[rnum] for port in self.rulenum_to_in_ports[rnum]: self.in_port_rules[port].remove(rnum) for port in self.rulenum_to_out_ports[rnum]: self.out_port_rules[port].remove(rnum) del self.rulenum_to_in_ports[rnum] del self.rulenum_to_out_ports[rnum] del self.rulenum_to_inspace[rnum] del self.rulenum_to_outspace[rnum] self.dscc.delete_vertex(rnum)