def __init__(self, instanceID, version, G, MOP, Prf, DTSN, dodagID, rplnode,\ advertised_prefixes=[], of = of_zero(), active=False, is_root=False): """ Creates DODAG Object identified by instanceID, dodagID, version """ super(dodag, self).__init__() self.of = of #Instance of objective function object 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 # there are open flow ports, rpl_node ip and datapath self.rplnode = rplnode # self.ports = ports # self.ip = ip # self.datapath = datapath if is_root: self.is_dodagRoot = True self.rank = ROOT_RANK if IPAddress(dodagID).is_link_local(): raise Exception("DODAG ID must be a routable address (not a link local address)") self.dodagID = dodagID else: self.rank = INFINITE_RANK # import objective function related function self.compute_rank_increase = self.of.compute_rank_increase self.OCP = of.OCP self.last_dio = time() self.setDIOtimer() if self.is_dodagRoot: self.DIOtimer.hear_inconsistent()
class DODAG(object): """A base class for a DODAG object """ 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, rplnode,\ advertised_prefixes=[], of = of_zero(), active=False, is_root=False): """ Creates DODAG Object identified by instanceID, dodagID, version """ super(dodag, self).__init__() self.of = of #Instance of objective function object 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 # there are open flow ports, rpl_node ip and datapath self.rplnode = rplnode # self.ports = ports # self.ip = ip # self.datapath = datapath if is_root: self.is_dodagRoot = True self.rank = ROOT_RANK if IPAddress(dodagID).is_link_local(): raise Exception("DODAG ID must be a routable address (not a link local address)") self.dodagID = dodagID else: self.rank = INFINITE_RANK # import objective function related function self.compute_rank_increase = self.of.compute_rank_increase self.OCP = of.OCP self.last_dio = time() self.setDIOtimer() if self.is_dodagRoot: self.DIOtimer.hear_inconsistent() def _build_rpl_pkt(self, src_ip, dst_ip, src_mac, dst_mac, rpl): pkt = packet.Packet() e = ethernet.ethernet(ethertype=ether.ETH_TYPE_IPV6, dst = dst_mac, src = src_mac) #TODO: hop_limit=1 i6 = ipv6.ipv6(dst=dst_ip,src=src_ip, nxt=inet.IPPROTO_ICMPV6) pkt.add_protocol(e) pkt.add_protocol(i6) pkt.add_protocol(rpl) return pkt def _send_rpl_pkt(self, out_port=[], dst_ip=[], dst_mac =[], rpl): """ out_port: list of output port numbers dst_ip: list of destination ip addresses for each output port dst_mac: list of dst mac of each output port """ assert len(out_port) == len(dst_ip) assert len(dst_ip) == len(dst_mac) # unicast or multicast for i in range(out_port): port = out_port[i] dstip = dst_ip[i] dstmac = dst_mac[i] p = self.rplnode.ports[port] pkt = self._build_rpl_pkt(dodag,src_ip = self.rplnode.ip, dst_ip = dstip, src_mac = p['hw_addr'], dst_mac = dstmac, rpl= rpl) send_packet(self.rplnode.datapath, port, pkt) def _broadcast_rpl_pkt(self, rpl): # broadcast dstip = ALL_RPL_NODES dstmac = BROADCAST_MAC for port in self.rplnode.ports.keys(): p = self.rplnode.ports[port] pkt = self._build_rpl_pkt(dodag,src_ip = self.rplnode.ip, dst_ip = dstip, src_mac = p['hw_addr'], dst_mac = dstmac, rpl= rpl) send_packet(self.rplnode.datapath, port, pkt) def sendDIO(self, out_port=[], dst_ip=[], dst_mac =[], dodag_shutdown=False): """ Send a DIO message port: port number to send on dst_ip: destination ippv6 dst_mac: destination mac with the following Options: 1. Prefix Information 2. DODAG Configuration 3. DAO messages are sent after short interval if the sending node is not the root of this DODAG or it is not poisoned Called by: DIS message handler, if the DIS is unicast, posion, and setDIOtime """ # create RPL options rploption = [] # create Dodag Configuration Option dodag_conf = icmpv6_rpl.rpl_option_dodag_conf(length=0, # the rpl serialize method know what to do with len flags=0, a = self.authenticated, pcs = self.PCS, diointdouble = self.DIOIntDoublings, diointmin = self.DIOIntMin, dioredun = self.DIORedundancyConst, maxrankinc = self.MaxRankIncrease, minhoprankinc = self.MinHopRankIncrease, ocp = self.OCP, reserved=0, def_lifetime = self.DftLft, lifetime_unit = self.LftUnit) rploption.append(dodag_conf) # Create Prefix Information Option for every advertised prefix for prefix in self.advertised_prefixes: prefix_info = icmpv6_rpl.rpl_option_pi(length=0, pre_length=64, l=0, a=1,r=0, reserved1=0, valid_lifetime=RPL_PREFIX_TIME_INF, pref_lifetime=RPL_PREFIX_TIME_INF, reserved2=0, prefix=prefix) rploption.append(prefix_info) # Create RPL_DIO message diomsg = icmpv6_rpl.rpl_dio(rplinstanceid=self.instanceID, version=self.version.get_val(), rank=self.rank,g=self.G,o=0,mop=self.MOP, prf=self.Prf,dtsn=self.DTSN.get_val(), flags=0,reserved=0, dodagid=self.dodagID,option=rploption) # Create Dodag RPL Control message rplmsg = icmpv6_rpl.rpl_control(code=icmpv6_rpl.RPL_DIO, rplctrl=diomsg) # Create ICMPv6 protocol message rpl = icmpv6.icmpv6(type_= icmpv6_rpl.ICMPv6_RPL, code=icmpv6_rpl.RPL_DIO, csum=0, data= rplmsg) if len(out_port) and len(dst_ip) and len(dst_mac): self._send_rpl_pkt(out_port, dst_ip, dst_mac, rpl) else: self._broadcast_rpl_pkt(rpl) # 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, out_port=[], dst_ip=[], dst_mac =[], retransmit=False, nopath=False): """ Send a DAO message 1. If not a retrasmit increment the DAO seq 2. By default DAO is sent to the preferred parent 3. Contains the options: RPL Target (from downward routes), and Transit Information (the Parent Address field is not needed because the node is in Storing Mode) 4. If it's broadcast don't set the K flag (Dont ask for DAO_ACK) 5. If unicast set the K flag, and activate the DAO_ACK gaurd timer using setDAO_ACKtimer 6. The DODAG checks which target it has path for and which it doesn't and constructs RPL Target and Transit Information accordingly. If poison is needed, we advertise all targets as if we have no path for. Called By: SendTwoDAOs, if DAO_ACK gaurd timer expires (retrunsmission), poison """ assert self.active or nopath logger.info("sending DAO message for %s (version %d)" % (self.dodagID, self.version.get_val())) if not retransmit: self.last_DAOSequence += 1 # if no destination is specified, find the DAO parent if len(dst_ip) == 0: # here, destination is None if no DAO parent exists destination = self.preferred_parent try: outport = [destination.port] dstip = [destination.ip] except: dstip = None outport = None assert dstip != ALL_RPL_NODES # create RPL options rploption = [] with self.__lock: for address in self.rplnode.address_cache: rpltargetopt = icmpv6_rpl.rpl_option_rpltarget(length=0, flags=0, prefix_length=128, target_prefix = address) rploption.add(rpltargetopt) # the Parent Address field is not needed because the node is in Storing Mode rpltiopt = icmpv6_rpl.rpl_option_ti(length=0, e=0, flags=0,path_control=0, path_seq=self.last_PathSequence.get_val(), path_lifetime=0x00 if nopath else self.DftLft, parent_addr = None) no_path_targets_opt = [] no_path_transit_inf_opt = None # if dstip == ALL_RPL_NODES: logger.debug("sending DAO message to All-RPL-Nodes multicast address: %s" % ALL_RPL_NODES) kval = 0 elif dstip is None: logger.debug("DAO dest_ip is not set") return else: #destination is the prefererred parent # because the K flag is set, we set a timer for receiving a DAO-ACK kval =1 self.DAO_ACK_source = dstip self.DAO_ACK_source_port = outport self.setDAO_ACKtimer() logger.debug("sending DAO message to a Link-Local address (Not really): %s" % dstip) 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] for (prefix, preflen) in targets: rpltargetopt = icmpv6_rpl.rpl_option_rpltarget(length=0, flags=0, prefix_length=int(preflen), target_prefix = prefix) rploption.add(rpltargetopt) 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: for (prefix, preflen) in no_path_targets: rpltargetopt = icmpv6_rpl.rpl_option_rpltarget(length=0, flags=0, prefix_length=int(preflen), target_prefix = prefix) no_path_targets_opt.append(rpltargetopt) no_path_transit_inf_opt = icmpv6_rpl.rpl_option_ti(length=0, e=0, flags=0,path_control=0, path_seq=self.last_PathSequence.get_val(), path_lifetime=0x00, parent_addr = None) else: self.no_path_routes = set() if nopath: rploption += no_path_targets_opt + transit_inf_opt else: rploption += transit_inf_opt + no_path_targets_opt + no_path_transit_inf_opt rpl = icmpv6_rpl.rpl_dao(rplinstanceid=self.instanceID,k=kval, d=0 ,flags=0,reserved=0, daosequence = self.last_DAOSequence.get_val(), dodagid=self.dodagID,option = rpltopion) if len(out_port) and len(dst_ip) and len(dst_mac): self._send_rpl_pkt(out_port, dst_ip, dst_mac, rpl) else: self._broadcast_rpl_pkt(rpl) 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) Called by: setDAOtimer """ if self.active: self.sendDAO(dst_ip=[ALL_RPL_NODES], retransmit=True) self.sendDAO() def sendDAO_ACK(self, out_port, dst_ip, dst_mac, DAOsequence, dodagID = 0): """ send a DAO ACK message Applicable only for one dst as it is requested in unicast DAO ONLY Called by: DAO Handler if the K bit in the received DAO is set""" logger.info("sending DAO-ACK to %s" % dst_ip) rpl = icmpv6_rpl.rpl_dao_ack(rplinstanceid=self.instanceID,d=0, reserved=0,daosequence=DAOsequence,status=0, dodagid=dodagID) self._send_rpl_pkt([out_port], [dst_ip], [dst_mac], rpl) def setDIOtimer(self): """Broadcast DIO after Trickle timer Called by: creation of the DODAG object """ 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): """ Send delayed DAO Called by: SendDAO, DAO handler if downward routes are updated, DIO handler if a parent node changed DTSN, DIO handler if not consistent condition """ 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): """Gaurd timer for DAO ACK Called by: sendDAO after sending the DAO message (waiting for ACK) """ 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 self.rplnode.neigh_cache.remove_node_by_address(self, self.DAO_ACK_source) updated = self.rplnode.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 DAO ACK is received) Called by: setDAO_ACKtimer to reset the timer, and DAO_ACK handler if the DAO_ACK corresponging to a DAO is received """ try: self.DAO_ACKtimer.cancel() except: pass def downward_route_add(self,route): """ register a downward route CAlled by: DAO handler parsing RPL Target Option (depending on path_lifetime) """ with self.__lock: if not self.rplnode.address_cache.is_assigned(route.target.split("/")[0]): self.downward_routes.add(route) def downward_route_del(self,route): """ remove a downward route Called by: downward_routes_remove_by_nexthop and DAO handler if the path is no-path (path_lifetime is null 0) """ 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): """ remove all downward routes Called by: DIO handler if the sending node is a parent and increased its DTSN (request DAO message)""" logger.debug("Removing all downward routes for this DODAG (%s)" % self.dodagID) with self.__lock: self.rplnode.route_cache.remove_routes(self.downward_routes) self.downward_routes = set() def downward_routes_remove_by_nexthop(self,address): """ remove all routes through a partular next_hop node if the DAO_ACk maximum retransmission is reached or in DIO handler if the parent node advertised infinite RANK Called by: remove_node_by_address in the neighbor cache """ 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 += self.rplnode.route_cache.remove_route(route) return updated def downward_routes_get(self): """ retrieve a copy of a route Called by: neighborcache.set_preferred parnet # moving to a new DODAG version or a completely different # DODAG, hence some downward routes might need to be # removed, while some others might need to be added """ with self.__lock: return self.downward_routes.copy() def get_filtered_downward_routes(self): """ Filter downward redundant routess: for example # one hop route takes precedence over multiphop routes Called by: DAO handler """ 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): """ implements the GAGRank function in the standard Called by: get_filtered_downward_routes to prune worse routes , neighbor chase to compute_DIO_parents and set_prefered and objective function (of_zero for example) to compare two parents """ return floor(float(rank)/ self.MinHopRankIncrease) def poison(self, shutdown=False): """ poisons the DODAG 1. If the RPL_Node is shutting down (not avail) to indicate that downward routes are not available anymore: a. send multicast DAO with no-path b. send a unicast DAO with no-path 2. Set rank to INFINITE 3. Send DIO with new updated data (infinite rank) Called by: dodag cache to remove old DODAG with updated version , cleanning up the rpl_node """ logger.debug("Poisoning DODAG %s (version %d)" % (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(dst_ip=[ALL_RPL_NODES], nopath=True) if self.preferred_parent: self.sendDAO(nopath=True) self.rank = INFINITE_RANK self.sendDIO(dodag_shutdown=True) 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 __del__(self): # identify neighbors that need to be removed self.cleanup() super(dodag, self).__del__() def cleanup(self): """Distroy Objects.""" # 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 self.rplnode.neigh_cache.remove_nodes_by_dodag(self)