def __init__(self, iface, address, dodag, rank, dtsn): self.iface = iface self.address = address self.rank = rank self.dodag = dodag self.preferred = False self.dtsn = Lollipop(dtsn) assert Address(self.address).is_linklocal()
def __init__(self, instanceID, version, \ G, MOP, Prf, DTSN, \ dodagID, \ interfaces, advertised_prefixes=[], active=False, is_root=False): # TODO: # - remove timer (not sure what it actually does) # (expiry may trigger No-Path advertisements or immediately deallocate # the DAO entry if no DAO parents exists) self.__lock = RLock() self.instanceID = instanceID self.version = Lollipop(version) self.dodagID = dodagID self.G = G self.MOP = MOP self.Prf = Prf self.DTSN = Lollipop(DTSN) self.active = active self.advertised_prefixes = advertised_prefixes self.last_DAOSequence = Lollipop() # used during DAO - DAO_ACK exchanges self.last_PathSequence = Lollipop() self.DAO_ACK_source = None self.DAO_ACK_source_iface = None self.DAO_trans_retry = 0 self.downward_routes = set() # set of tuple in the form of (destination, prefix_len, prefix) self.preferred_parent = None # cleanup purposes self.no_path_routes = set() # store the routes for which we received a No-Path DAO self.no_path_routes_trans = 0 # how many times the No-Path DAO have been transmitted self.interfaces = interfaces if is_root: self.is_dodagRoot = True self.rank = ROOT_RANK if not gv.address_cache.is_assigned(dodagID): raise Exception("DODAG ID must be an address assigned to this node") if Address(dodagID).is_linklocal(): raise Exception("DODAG ID must be a routable address (not a link local address)") self.dodagID = str(Address(dodagID)) else: self.rank = INFINITE_RANK # import objective function related function self.compute_rank_increase = partial(of.compute_rank_increase, self) self.OCP = of.OCP self.last_dio = time.time() self.setDIOtimer() if self.is_dodagRoot: self.DIOtimer.hear_inconsistent()
class DODAG(object): """A DODAG ( part of a RPL instance)""" dodagID = r"\0" * 16 is_dodagRoot = False DODAGVersionNumber = 0 last_DAOSequence = 0 # used during DAO - DAO_ACK exchanges last_PathSequence = 0 lowest_rank_advertized = INFINITE_RANK rank = INFINITE_RANK # learned from the DODAG configuration option authenticated = 0 PCS = DEFAULT_PATH_CONTROL_SIZE DIOIntDoublings = DEFAULT_DIO_INTERVAL_DOUBLINGS DIOIntMin = DEFAULT_DIO_INTERVAL_MIN DIORedundancyConst = DEFAULT_DIO_REDUNDANCY_CONSTANT MaxRankIncrease = DEFAULT_MAX_RANK_INCREASE MinHopRankIncrease = DEFAULT_MIN_HOP_RANK_INCREASE OCP = 0 DftLft = 0xff # represents Infinity LftUnit = 0xffff def __init__(self, instanceID, version, \ G, MOP, Prf, DTSN, \ dodagID, \ interfaces, advertised_prefixes=[], active=False, is_root=False): # TODO: # - remove timer (not sure what it actually does) # (expiry may trigger No-Path advertisements or immediately deallocate # the DAO entry if no DAO parents exists) self.__lock = RLock() self.instanceID = instanceID self.version = Lollipop(version) self.dodagID = dodagID self.G = G self.MOP = MOP self.Prf = Prf self.DTSN = Lollipop(DTSN) self.active = active self.advertised_prefixes = advertised_prefixes self.last_DAOSequence = Lollipop() # used during DAO - DAO_ACK exchanges self.last_PathSequence = Lollipop() self.DAO_ACK_source = None self.DAO_ACK_source_iface = None self.DAO_trans_retry = 0 self.downward_routes = set() # set of tuple in the form of (destination, prefix_len, prefix) self.preferred_parent = None # cleanup purposes self.no_path_routes = set() # store the routes for which we received a No-Path DAO self.no_path_routes_trans = 0 # how many times the No-Path DAO have been transmitted self.interfaces = interfaces if is_root: self.is_dodagRoot = True self.rank = ROOT_RANK if not gv.address_cache.is_assigned(dodagID): raise Exception("DODAG ID must be an address assigned to this node") if Address(dodagID).is_linklocal(): raise Exception("DODAG ID must be a routable address (not a link local address)") self.dodagID = str(Address(dodagID)) else: self.rank = INFINITE_RANK # import objective function related function self.compute_rank_increase = partial(of.compute_rank_increase, self) self.OCP = of.OCP self.last_dio = time.time() self.setDIOtimer() if self.is_dodagRoot: self.DIOtimer.hear_inconsistent() def sendDIO(self, iface=None, destination=None, dodag_shutdown=False): """Send a DIO message: - iface: if specified, this is the interface where messages are going out, if not, message are broadcasted on all registered interfaces - destination: if this is a unicast DIO, destionation is the destination address. If destination is not specified, the message is sent to the RPL-all-routers multicast address - dodag_shutdown: this function also schedule a DAO message to be sent. During the shutdown procedure or when a DODAG version is phased out, this is not a desirable behavior, as the DAO should be sent right away. """ logger.info("sending DIO message for %s (version %d)" % (repr(Address(self.dodagID)), self.version.get_val())) if self.advertised_prefixes: extra_option = "".join([str(RPL_Option_Prefix_Information(prefix_len=64, L=0, A=1, R=0, prefix=prefix, valid_lifetime=0xFFFFFFFF, # infinite lifetime preferred_lifetime=0xFFFFFFFF)) for prefix in self.advertised_prefixes]) else: extra_option = "" DIO_message = str(DIO(instanceID=self.instanceID, version=self.version.get_val(), rank=self.rank, G=self.G, MOP=self.MOP, Prf=self.Prf, DTSN=self.DTSN.get_val(), flags=0, reserved=0, DODAGID=self.dodagID)) + \ str(RPL_Option_DODAG_Configuration(A=self.authenticated, PCS=self.PCS, DIOIntDoubl=self.DIOIntDoublings, DIOIntMin=self.DIOIntMin, DIORedun=self.DIORedundancyConst, MaxRankIncrease=self.MaxRankIncrease, MinHopRankIncrease=self.MinHopRankIncrease, OCP=self.OCP, DefLifetime=self.DftLft, LifetimeUnit=self.LftUnit)) +\ extra_option if iface and destination: self.interfaces[iface].send(destination, DIO_message) else: broadcast(self.interfaces, DIO_message) # DAO message are sent after a short interval when DIO messages are # sent if not self.is_dodagRoot and not dodag_shutdown: self.setDAOtimer() del DIO_message def sendDAO(self, iface=None, destination=None, retransmit=False, nopath=False): """Send a DAO message to its DAO parent (by default) Build the target list on the fly nopath parameters indicates that the node must announce all its downward routes as no-path""" assert self.active or nopath logger.info("sending DAO message for %s (version %d)" % (repr(Address(self.dodagID)), self.version.get_val())) if not retransmit: self.last_DAOSequence += 1 # if no destination is specified, find the DAO parent if not destination: # here, destination is None if no DAO parent exists destination = self.preferred_parent try: iface = destination.iface destination = destination.address except: destination = None iface = None assert destination != ALL_RPL_NODES # build the RPL Target Options for the addresses allocated on the node targets_opt = "".join([str(RPL_Option_RPL_Target(prefix_len=128, target_prefix=str(Address(address)))) for (address, pref_len, nh_iface) in gv.address_cache]) # the Parent Address field is not needed because the node is in Storing Mode transit_inf_opt = str(RPL_Option_Transit_Information(path_control=0, path_sequence=self.last_PathSequence.get_val(), path_lifetime=0x00 if nopath else self.DftLft, parent_address="")) no_path_targets_opt = "" no_path_transit_inf_opt = "" if destination and Address(destination).is_RPL_all_nodes(): logger.debug("sending DAO message to All-RPL-Nodes multicast address: %s" % destination) DAO_header = str(DAO(instanceID=self.instanceID, K=0, DAOsequence=self.last_DAOSequence.get_val(), \ DODAGID=self.dodagID)) elif destination and Address(destination).is_linklocal(): # because the K flag is set, we set a timer for receiving a DAO-ACK self.DAO_ACK_source = destination self.DAO_ACK_source_iface = iface self.setDAO_ACKtimer() logger.debug("sending DAO message to a Link-Local address: %s" % destination) DAO_header = str(DAO(instanceID=self.instanceID, K=1, DAOsequence=self.last_DAOSequence.get_val(), \ DODAGID=self.dodagID)) with self.__lock: # build the RPL Target Options from the list of downward routes targets = [tuple(route.target.split("/")) for route in self.downward_routes] targets_opt += "".join([str(RPL_Option_RPL_Target(prefix_len=int(preflen), target_prefix=str(Address(prefix)))) for (prefix, preflen) in targets]) if self.no_path_routes_trans < DEFAULT_DAO_NO_PATH_TRANS and self.no_path_routes: logger.debug("advertising additional routes that need to be removed") self.no_path_routes_trans += 1 # there is no need to propagate the No-Path information when an alternative path exists locally reachable_targets = set([route.target for route in self.downward_routes]) no_path_targets = [tuple(route.target.split("/")) for route in self.no_path_routes if route.target not in reachable_targets] if no_path_targets: no_path_targets_opt = "".join([str(RPL_Option_RPL_Target(prefix_len=int(preflen), target_prefix=str(Address(prefix)))) for (prefix, preflen) in no_path_targets]) no_path_transit_inf_opt = str(RPL_Option_Transit_Information(path_control=0, path_sequence=self.last_PathSequence.get_val(), path_lifetime=0x00, parent_address="")) else: self.no_path_routes = set() else: logger.debug("destination address %s is not a Link-Local address or a Multicast address, dropping command" % destination) return if nopath: DAO_message = DAO_header + targets_opt + no_path_targets_opt + transit_inf_opt else: DAO_message = DAO_header + targets_opt + transit_inf_opt + no_path_targets_opt + no_path_transit_inf_opt if iface and destination: self.interfaces[iface].send(destination, DAO_message) else: broadcast(self.interfaces, DAO_message) def sendTwoDAOs(self): """Send a multicast DAO, for the node's own destination and a unicast DAO to announce all downwards routes known to the node (only if the DODAG is currently active""" if self.active: self.sendDAO(destination=ALL_RPL_NODES, retransmit=True) self.sendDAO() def sendDAO_ACK(self, iface, destination, DAOsequence, dodagID = None): logger.info("sending DAO-ACK to %s" % repr(Address(destination))) if dodagID: DAO_ACK_message = str(DAO_ACK(instanceID=self.instanceID, DAOSequence = DAOsequence, Status = 0, # unqualified acceptance DODAGID = dodagID )) else: DAO_ACK_message = str(DAO_ACK(instanceID=self.instanceID, DAOSequence = DAOsequence, Status = 0 # unqualified acceptance )) self.interfaces[iface].send(destination, DAO_ACK_message) def setDIOtimer(self): """(Re)set the DIO timer""" try: self.DIOtimer.cancel() except: pass self.DIOtimer = trickleTimer(self.sendDIO, {}, \ Imin=0.001 * 2 ** self.DIOIntMin, \ Imax=self.DIOIntDoublings, \ k=self.DIORedundancyConst) self.DIOtimer.start() def setDAOtimer(self): """Set the DAO timer, ignore new calls when timer is armed. This function ensures that DAO messages is sent after the DEFAULT_DAO_DELAY seconds (so that more routes could be aggregated) """ try: if not self.DAOtimer.is_alive(): self.DAOtimer = Timer(DEFAULT_DAO_DELAY, self.sendTwoDAOs) else: return except AttributeError: self.DAOtimer = Timer(DEFAULT_DAO_DELAY, self.sendTwoDAOs) self.DAOtimer.start() def setDAO_ACKtimer(self): """Set the DAO ack timer, to retransmit DAO when it is not acknowledged in a timely fashion""" self.cancelDAO_ACKtimer() if self.DAO_trans_retry >= DEFAULT_DAO_MAX_TRANS_RETRY: self.DAO_trans_retry = 0 # the destination (self.DAO_ACK_source) seems unreachable # hence, it should be removed, and if it was the DIO parent, a new # DIO parent must be found gv.neigh_cache.remove_node_by_address(self, self.DAO_ACK_source) updated = gv.neigh_cache.update_DIO_parent() if updated: self.DIOtimer.hear_inconsistent() return self.DAO_trans_retry += 1 self.DAO_ACKtimer = Timer(DEFAULT_DAO_ACK_DELAY, self.sendDAO, kwargs={'iface':self.DAO_ACK_source_iface, 'destination':self.DAO_ACK_source, 'retransmit':True}) self.DAO_ACKtimer.start() def cancelDAO_ACKtimer(self): """cancel the DAO ACK timer (when the acknowledgment message is received)""" try: self.DAO_ACKtimer.cancel() except: pass def downward_route_add(self, route): with self.__lock: if not gv.address_cache.is_assigned(route.target.split("/")[0]): self.downward_routes.add(route) def downward_route_del(self, route): with self.__lock: if route in self.downward_routes: self.downward_routes.remove(route) self.no_path_routes_trans = 0 self.no_path_routes.add(route) def downward_routes_reset(self): logger.debug("Removing all downward routes for this DODAG (%s)" % str(Address(self.dodagID))) with self.__lock: gv.route_cache.remove_routes(self.downward_routes) self.downward_routes = set() def downward_routes_remove_by_nexthop(self, address): logger.debug("Removing all downward routes going through %s" % address) updated = False with self.__lock: for route in self.downward_routes.copy(): if address == route.nexthop: self.downward_route_del(route) if self.active: updated += gv.route_cache.remove_route(route) return updated def downward_routes_get(self): with self.__lock: return self.downward_routes.copy() def get_filtered_downward_routes(self): new_routes = {} removed_routes = [] with self.__lock: for route in self.downward_routes: if route.target not in new_routes: new_routes[route.target] = (route.nexthop, route.nexthop_iface, route.onehop) continue (current_nexthop, current_nexthop_iface, current_onehop) = new_routes[route.target] # only one route can be one hop to a destination # (unless a node uses two link local addresses) assert not (current_onehop and route.onehop) # one hop route takes precedence over multiphop routes if current_onehop: removed_routes.append(route) continue # one hop route takes precedence over multihop routes if route.onehop: removed_routes.append(Route(route.target, current_nexthop, current_nexthop_iface, current_onehop)) new_routes[route.target] = (route.nexthop, route.nexthop_iface, route.onehop) continue node = gv.neigh_cache.get_node(route.nexthop_iface, route.nexthop, self) current_node = gv.neigh_cache.get_node(current_nexthop_iface, current_nexthop, self) assert id(node) != id(current_node) if not node: removed_routes.append(route) continue if not current_node: removed_routes.append(Route(route.target, current_nexthop, current_nexthop_iface, current_onehop)) new_routes[route.target] = (route.nexthop, route.nexthop_iface, route.onehop) continue if self.DAGRank(node.rank) >= self.DAGRank(current_node.rank): removed_routes.append(route) else: new_routes[route.target] = (route.nexthop, route.nexthop_iface, route.onehop) removed_routes.append(Route(route.target, current_nexthop, current_nexthop_iface, current_onehop)) new_routes = [Route(target, nexthop, nexthop_iface, onehop) for (target, (nexthop, nexthop_iface, onehop)) in new_routes.items()] return (removed_routes, new_routes) def DAGRank(self, rank): return floor(float(rank)/ self.MinHopRankIncrease) def poison(self, shutdown=False): logger.debug("Poisoning DODAG %s (version %d)" % (repr(Address(self.dodagID)), self.version.get_val())) # send a No-PATH DAO message only when the RPL router stops and the # downward routes won't be accessible anymore if shutdown: self.sendDAO(nopath=True, destination=ALL_RPL_NODES) if self.preferred_parent: self.sendDAO(nopath=True) self.rank = INFINITE_RANK self.sendDIO(dodag_shutdown=True) def cleanup(self): # so this ugly, but it's very hard to make sure the DODAG object has # really been destroyed, instead, we make sure these methods can never # be called again self.sendDAO = undef self.sendDIO = undef self.sendDAO_ACK = undef self.setDAOtimer = undef self.setDIOtimer = undef self.setDAO_ACKtimer = undef # disable all the running timers try: self.DIOtimer.cancel() except: pass try: self.DAOtimer.cancel() except: pass try: self.DAO_ACKtimer.cancel() except: pass del self.DIOtimer try: del self.DAOtimer except: pass try: del self.DAO_ACKtimer except: pass gv.neigh_cache.remove_nodes_by_dodag(self) def __eq__(self, other): """ Compare DODAG Versions between two DODAGs """ return self.instanceID == other.instanceID and \ self.version == other.version and \ self.dodagID == other.dodagID def __str__(self): """Print a text presentation of the DODAG parameters""" textdag = "DODAG Identifier: {0}\n" \ "RPL Instance Identifier: {1}\n" \ "DODAG Root: {2}\n" \ "Grounded: {3}\n" \ "Mode of operation: {4}\n" \ "Administrative Preference: {5}\n" \ "DTSN: {6}\n" \ "DODAG Version Number: {7}\n" \ "Rank: {8}\n" \ "Lowest Rank ever recorded: {9}\n" \ "Authenticated: {10}\n" \ "Path Control Size: {11}\n" \ "DIO Interval Doublings: {12}\n" \ "DIO Interval Minimum: {13}\n" \ "DIO Redundancy Constant: {14}\n" \ "Max Rank Increase: {15}\n" \ "Min Hop Rank Increase: {16}\n" \ "Objective Code Point: {17}\n" \ "Default Lifetime: {18}\n" \ "Lifetime Unit: {19}\n" \ "This is the active DODAG: {20}\n" \ "Advertised prefixes: {21}\n" \ "Last DIO received at: {22}\n".format( repr(Address(self.dodagID)), self.instanceID, self.is_dodagRoot and "yes" or "no", self.G and "yes" or "no", self.MOP, self.Prf, self.DTSN.get_val(), self.version.get_val(), self.rank, self.lowest_rank_advertized, self.authenticated and "yes" or "no", self.PCS, self.DIOIntDoublings, self.DIOIntMin, self.DIORedundancyConst, self.MaxRankIncrease, self.MinHopRankIncrease, self.OCP, self.DftLft, self.LftUnit, self.active and "yes" or "no", " ".join([socket.inet_ntop(socket.AF_INET6, prefix[:8] + "\x00" * 8) for prefix in self.advertised_prefixes]), time.ctime(self.last_dio)) return textdag def __del__(self): # identify neighbors that need to be removed self.cleanup() super(DODAG, self).__del__()