# EdgeVPNio # Copyright 2020, University of Florida # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import os import threading from collections import namedtuple import time from framework.ControllerModule import ControllerModule LinkEvent = ["LnkEvCreating", "LnkEvConnected", "LnkEvDisconnected", "LnkEvRemoved", "LnkEvAuthorized", "LnkEvDeauthorized"] class Link(): def __init__(self, lnkid, state): self.lnkid = lnkid self._creation_state = state self.status_retry = 0 self.stats = {} def __repr__(self): state = "Link" % \ (self.lnkid[:7], self._creation_state, self.status_retry, self.stats) return state def __str__(self): state = "Link" % \ (self.lnkid[:7], self._creation_state, self.status_retry) return state @property def creation_state(self): return self._creation_state @creation_state.setter def creation_state(self, new_state): "todo: implement transition checks" self._creation_state = new_state TUNNEL_STATES = ["TNL_AUTHORIZED", "TNL_CREATING", "TNL_QUERYING", "TNL_ONLINE", "TNL_OFFLINE"] TunnelStates = namedtuple("TunnelStates", TUNNEL_STATES) class Tunnel(): STATES = TunnelStates("TNL_AUTHORIZED", "TNL_CREATING", "TNL_QUERYING", "TNL_ONLINE", "TNL_OFFLINE") def __init__(self, tnlid, overlay_id, peer_id, tnl_state="TNL_AUTHORIZED", state_timeout=45): self.tnlid = tnlid self.overlay_id = overlay_id self.peer_id = peer_id self.tap_name = None self.mac = None self.fpr = None self.link = None self.peer_mac = None self._tunnel_state = tnl_state self.creation_start_time = time.time() self.timeout = time.time() + state_timeout # timeout for current phase def __repr__(self): state = "Tunnel" % \ (self.tnlid[:7], self.overlay_id[:7], self.peer_id[:7], self.tap_name, self.mac, self.link, self.peer_mac, self.tunnel_state, self.creation_start_time) return state def __str__(self): state = "Tunnel" % \ (self.tnlid[:7], self.overlay_id[:7], self.peer_id[:7], self.tap_name, self.mac, self.link, self.peer_mac, self.tunnel_state, self.creation_start_time) return state @property def tunnel_state(self): return self._tunnel_state @tunnel_state.setter def tunnel_state(self, new_state): "todo: implement transition checks" self._tunnel_state = new_state class LinkManager(ControllerModule): def __init__(self, cfx_handle, module_config, module_name): super(LinkManager, self).__init__(cfx_handle, module_config, module_name) self._tunnels = {} # maps tunnel id to its descriptor self._peers = {} # maps overlay id to peers map, which maps peer id to tunnel id self._links = {} # maps link id to tunnel id self._lock = threading.Lock() # serializes access to _overlays, _links self._link_updates_publisher = None self._ignored_net_interfaces = dict() def __repr__(self): state = "LinkManager<_peers=%s, _tunnels=%s>" % (self._peers, str(self._tunnels)) return state def initialize(self): self._link_updates_publisher = \ self._cfx_handle.publish_subscription("LNK_TUNNEL_EVENTS") self._cfx_handle.start_subscription("TincanInterface", "TCI_TINCAN_MSG_NOTIFY") try: # Subscribe for data request notifications from OverlayVisualizer self._cfx_handle.start_subscription("OverlayVisualizer", "VIS_DATA_REQ") except NameError as err: if "OverlayVisualizer" in str(err): self.register_cbt("Logger", "LOG_WARNING", "OverlayVisualizer module not loaded." " Visualization data will not be sent.") for olid in self.config["Overlays"]: self._peers[olid] = dict() self._ignored_net_interfaces[olid] = set() ol_cfg = self.config["Overlays"][olid] if "IgnoredNetInterfaces" in ol_cfg: for ign_inf in ol_cfg["IgnoredNetInterfaces"]: self._ignored_net_interfaces[olid].add(ign_inf) self.register_cbt("Logger", "LOG_INFO", "Module Loaded") def _get_ignored_tap_names(self, overlay_id, new_inf_name=None): ign_tap_names = set() if new_inf_name: ign_tap_names.add(new_inf_name) if not self.config["Overlays"][overlay_id].get("AllowRecursiveTunneling", False): # Ignore ALL the evio tap devices (regardless of their overlay id/link id) for tnlid in self._tunnels: if self._tunnels[tnlid].tap_name: ign_tap_names.add( self._tunnels[tnlid].tap_name) for tap_name in self._ignored_net_interfaces.values(): ign_tap_names |= tap_name else: ign_tap_names |= self._ignored_net_interfaces[overlay_id] return ign_tap_names def is_complete_link(self, tnlid): is_complete = (self._tunnels.get(tnlid, False) and self._tunnels[tnlid].link and self._tunnels[tnlid].link.creation_state == 0xC0) return is_complete def req_handler_add_ign_inf(self, cbt): ign_inf_details = cbt.request.params for olid in ign_inf_details: self._ignored_net_interfaces[olid] |= ign_inf_details[olid] cbt.set_response(None, True) self.complete_cbt(cbt) def req_handler_remove_tnl(self, cbt): """Remove the tunnel and link given either the overlay id and peer id, or the tunnel id""" olid = cbt.request.params.get("OverlayId", None) peer_id = cbt.request.params.get("PeerId", None) tnlid = cbt.request.params.get("TunnelId", None) if olid is not None and peer_id is not None: tnlid = self._peers[olid][peer_id] elif tnlid is not None: olid = self._tunnels[tnlid].overlay_id else: cbt.set_response("Insufficient parameters", False) self.complete_cbt(cbt) return if self._tunnels[tnlid].tunnel_state == Tunnel.STATES.TNL_ONLINE or \ self._tunnels[tnlid].tunnel_state == Tunnel.STATES.TNL_OFFLINE: tn = self._tunnels[tnlid].tap_name params = {"OverlayId": olid, "TunnelId": tnlid, "PeerId": peer_id, "TapName": tn} rtnl_cbt = self.create_linked_cbt(cbt) rtnl_cbt.set_request(self.module_name, "TincanInterface", "TCI_REMOVE_TUNNEL", params) self.submit_cbt(rtnl_cbt) else: cbt.set_response("Tunnel busy, retry operation", False) self.complete_cbt(cbt) def req_handler_remove_link(self, cbt): """Remove the link given either the overlay id and peer id, or the link id or tunnel id""" # not currently being used olid = cbt.request.params.get("OverlayId", None) peer_id = cbt.request.params.get("PeerId", None) lnkid = cbt.request.params.get("LinkId", None) tnlid = cbt.request.params.get("TunnelId", None) if olid is not None and peer_id is not None: tnlid = self._peers[olid][peer_id] lnkid = self.link_id(tnlid) elif tnlid is not None: olid = self._tunnels[tnlid].overlay_id lnkid = self.link_id(tnlid) elif lnkid is not None: tnlid = self.tunnel_id(lnkid) olid = self._tunnels[tnlid].overlay_id else: cbt.set_response("Insufficient parameters", False) self.complete_cbt(cbt) return if self._tunnels[tnlid].tunnel_state == Tunnel.STATES.TNL_ONLINE or \ self._tunnels[tnlid].tunnel_state == Tunnel.STATES.TNL_OFFLINE: params = {"OverlayId": olid, "TunnelId": tnlid, "LinkId": lnkid, "PeerId": peer_id} self.register_cbt("TincanInterface", "TCI_REMOVE_LINK", params) else: cbt.set_response("Tunnel busy, retry operation", False) self.complete_cbt(cbt) def _update_tunnel_descriptor(self, tnl_desc, tnlid): """ Update the tunnel desc with with lock owned """ self._tunnels[tnlid].mac = tnl_desc["MAC"] self._tunnels[tnlid].tap_name = tnl_desc["TapName"] self._tunnels[tnlid].fpr = tnl_desc["FPR"] self.register_cbt("Logger", "LOG_DEBUG", "Updated tunnels:{}".format(self._tunnels[tnlid])) def _query_link_stats(self): """Query the status of links that have completed creation process""" params = [] for lnkid in self._tunnels: link = self._tunnels[lnkid].link if link and link.creation_state == 0xC0: params.append(lnkid) if params: self.register_cbt("TincanInterface", "TCI_QUERY_LINK_STATS", params) def resp_handler_query_link_stats(self, cbt): if not cbt.response.status: self.register_cbt("Logger", "LOG_WARNING", "Link stats update error: {0}" .format(cbt.response.data)) self.free_cbt(cbt) return if not cbt.response.data: self.free_cbt(cbt) return data = cbt.response.data #self.register_cbt("Logger", "LOG_DEBUG", "Tunnel stats: {0}".format(data)) # Handle any connection failures and update tracking data for tnlid in data: for lnkid in data[tnlid]: if data[tnlid][lnkid]["Status"] == "UNKNOWN": self._cleanup_removed_tunnel(tnlid) elif tnlid in self._tunnels: tnl = self._tunnels[tnlid] if data[tnlid][lnkid]["Status"] == "OFFLINE": # tincan indicates offline so recheck the link status retry = tnl.link.status_retry if retry >= 2 and tnl.tunnel_state == Tunnel.STATES.TNL_CREATING: # link is stuck creating so destroy it olid = tnl.overlay_id peer_id = tnl.peer_id params = {"OverlayId": olid, "TunnelId": tnlid, "LinkId": lnkid, "PeerId": peer_id, "TapName": tnl.tap_name} self.register_cbt("TincanInterface", "TCI_REMOVE_TUNNEL", params) #elif retry >= 1 and tnl.tunnel_state == Tunnel.STATES.TNL_QUERYING: elif retry >= 0 and tnl.tunnel_state == Tunnel.STATES.TNL_QUERYING: # link went offline so notify top tnl.tunnel_state = Tunnel.STATES.TNL_OFFLINE olid = tnl.overlay_id peer_id = tnl.peer_id param = { "UpdateType": "LnkEvDisconnected", "OverlayId": olid, "PeerId": peer_id, "TunnelId": tnlid, "LinkId": lnkid, "TapName": tnl.tap_name} self._link_updates_publisher.post_update(param) else: tnl.link.status_retry = retry + 1 elif data[tnlid][lnkid]["Status"] == "ONLINE": tnl.tunnel_state = Tunnel.STATES.TNL_ONLINE #tnl.link.ice_role = data[tnlid][lnkid]["IceRole"] tnl.link.stats = data[tnlid][lnkid]["Stats"] tnl.link.status_retry = 0 else: self.register_cbt("Logger", "LOG_WARNING", "Unrecognized tunnel state " "{0}:{1}".format(lnkid, data[tnlid][lnkid]["Status"])) self.free_cbt(cbt) def _cleanup_tunnel(self, tnl): """ Remove the tunnel data """ del self._peers[tnl.overlay_id][tnl.peer_id] del self._tunnels[tnl.tnlid] def _cleanup_removed_tunnel(self, tnlid): """ Remove the tunnel data """ tnl = self._tunnels.pop(tnlid, None) if tnl: peer_id = tnl.peer_id olid = tnl.overlay_id del self._peers[olid][peer_id] def _remove_link_from_tunnel(self, tnlid): tnl = self._tunnels.get(tnlid) if tnl: if tnl.link and tnl.link.lnkid: self._links.pop(tnl.link.lnkid) tnl.link = None tnl.tunnel_state = Tunnel.STATES.TNL_OFFLINE def link_id(self, tnlid): tnl = self._tunnels.get(tnlid, None) if tnl and tnl.link: return tnl.link.lnkid return None def tunnel_id(self, lnkid): return self._links.get(lnkid) def _assign_link_to_tunnel(self, tnlid, lnkid, state): if tnlid in self._tunnels: self._tunnels[tnlid].link = Link(lnkid, state) self._links[lnkid] = tnlid def resp_handler_remove_tunnel(self, rmv_tnl_cbt): """ Clean up the tunnel meta data. Even of the CBT fails it is safe to discard as this is because Tincan has no record of it. """ parent_cbt = rmv_tnl_cbt.parent tnlid = rmv_tnl_cbt.request.params["TunnelId"] lnkid = self.link_id(tnlid) peer_id = rmv_tnl_cbt.request.params["PeerId"] olid = rmv_tnl_cbt.request.params["OverlayId"] # Notify subscribers of tunnel removal param = { "UpdateType": "LnkEvRemoved", "OverlayId": olid, "TunnelId": tnlid, "LinkId": lnkid, "PeerId": peer_id} if tnlid not in self._tunnels: return # this would be an invalid condition, possible double remove request if self._tunnels[tnlid].tap_name: param["TapName"] = self._tunnels[tnlid].tap_name self._link_updates_publisher.post_update(param) self._cleanup_tunnel(self._tunnels[tnlid]) self.free_cbt(rmv_tnl_cbt) if parent_cbt: parent_cbt.set_response("Tunnel removed", True) self.complete_cbt(parent_cbt) self.register_cbt("Logger", "LOG_INFO", "Tunnel {0} removed: {1}:{2}<->{3}" .format(tnlid[:7], olid[:7], self.node_id[:7], peer_id[:7])) #self.register_cbt("Logger", "LOG_DEBUG", "State:\n" + str(self)) def resp_handler_remove_link(self, rmv_tnl_cbt): parent_cbt = rmv_tnl_cbt.parent tnlid = rmv_tnl_cbt.request.params["TunnelId"] lnkid = self.link_id(tnlid) peer_id = rmv_tnl_cbt.request.params["PeerId"] olid = rmv_tnl_cbt.request.params["OverlayId"] # Notify subscribers of link removal param = { "UpdateType": "LnkEvRemoved", "OverlayId": olid, "TunnelId": tnlid, "LinkId": lnkid, "PeerId": peer_id} if self._tunnels[tnlid].tap_name: param["TapName"] = self._tunnels[tnlid].tap_name self._link_updates_publisher.post_update(param) self._remove_link_from_tunnel(tnlid) self.free_cbt(rmv_tnl_cbt) if parent_cbt: parent_cbt.set_response("Link removed", True) self.complete_cbt(parent_cbt) self.register_cbt("Logger", "LOG_INFO", "Link {0} from Tunnel {1} removed: {2}:{3}<->{4}" .format(lnkid[:7], tnlid[:7], olid[:7], self.node_id[:7], peer_id[:7])) def req_handler_query_tunnels_info(self, cbt): results = {} for tnlid in self._tunnels: if self._tunnels[tnlid].tunnel_state == Tunnel.STATES.TNL_ONLINE: results[tnlid] = {"OverlayId": self._tunnels[tnlid].overlay_id, "TunnelId": tnlid, "PeerId": self._tunnels[tnlid].peer_id, "Stats": self._tunnels[tnlid].link.stats, "TapName": self._tunnels[tnlid].tap_name, "MAC": self._tunnels[tnlid].mac, "PeerMac": self._tunnels[tnlid].peer_mac} cbt.set_response(results, status=True) self.complete_cbt(cbt) def _create_tunnel(self, params, parent_cbt=None): overlay_id = params["OverlayId"] ol_type = self.config["Overlays"][overlay_id]["Type"] tnlid = params["TunnelId"] lnkid = params["LinkId"] peer_id = params["PeerId"] tap_name = self.config["Overlays"][overlay_id]["TapName"][:8] + str(peer_id[:7]) if os.name == "nt": tap_name = self.config["Overlays"][overlay_id]["TapName"] self.log("LOG_DEBUG", "IgnoredNetInterfaces: %s", self._get_ignored_tap_names(overlay_id, tap_name)) create_tnl_params = { "OverlayId": overlay_id, "NodeId": self.node_id, "TunnelId": tnlid, "LinkId": lnkid, "StunServers": self.config.get("Stun", []), "Type": ol_type, "TapName": tap_name, "IP4": self.config["Overlays"][overlay_id].get("IP4"), "MTU4": self.config["Overlays"][overlay_id].get("MTU4"), "IP4PrefixLen": self.config["Overlays"][overlay_id].get("IP4PrefixLen"), "IgnoredNetInterfaces": list( self._get_ignored_tap_names(overlay_id, tap_name)) } if self.config.get("Turn"): create_tnl_params["TurnServers"] = self.config["Turn"] if parent_cbt: tnl_cbt = self.create_linked_cbt(parent_cbt) tnl_cbt.set_request(self.module_name, "TincanInterface", "TCI_CREATE_TUNNEL", create_tnl_params) else: tnl_cbt = self.create_cbt(self.module_name, "TincanInterface", "TCI_CREATE_TUNNEL", create_tnl_params) self.submit_cbt(tnl_cbt) def _request_peer_endpoint(self, params, parent_cbt): overlay_id = params["OverlayId"] tnlid = params["TunnelId"] endp_param = { "NodeData": { "FPR": self._tunnels[tnlid].fpr, "MAC": self._tunnels[tnlid].mac, "UID": self.node_id}} endp_param.update(params) remote_act = dict(OverlayId=overlay_id, RecipientId=parent_cbt.request.params["PeerId"], RecipientCM="LinkManager", Action="LNK_REQ_LINK_ENDPT", Params=endp_param) if parent_cbt: endp_cbt = self.create_linked_cbt(parent_cbt) endp_cbt.set_request(self.module_name, "Signal", "SIG_REMOTE_ACTION", remote_act) else: endp_cbt = self.create_cbt(self.module_name, "Signal", "SIG_REMOTE_ACTION", remote_act) # Send the message via SIG server to peer self.submit_cbt(endp_cbt) def _rollback_link_creation_changes(self, tnlid): """ Removes links that failed the setup handshake. Does not currently complete pending CBTs. This needs to be handled or these CBTs will remain in the pending queue. """ if tnlid not in self._tunnels: return creation_state = self._tunnels[tnlid].link.creation_state if creation_state < 0xC0: olid = self._tunnels[tnlid].overlay_id peer_id = self._tunnels[tnlid].peer_id lnkid = self.link_id(tnlid) params = {"OverlayId": olid, "PeerId": peer_id, "TunnelId": tnlid, "LinkId": lnkid} self.register_cbt("TincanInterface", "TCI_REMOVE_TUNNEL", params) self.register_cbt("Logger", "LOG_INFO", "Initiated removal of incomplete link: " "PeerId:{2}, LinkId:{0}, CreateState:{1}" .format(tnlid[:7], format(creation_state, "02X"), peer_id[:7])) def req_handler_auth_tunnel(self, cbt): """Node B""" olid = cbt.request.params["OverlayId"] peer_id = cbt.request.params["PeerId"] tnlid = cbt.request.params["TunnelId"] if peer_id in self._peers[olid] or tnlid in self._tunnels: cbt.set_response("Tunnel auth failed, resource already exist for peer:tunnel {0}:{1}" .format(peer_id, tnlid[:7]), False) else: self._peers[olid][peer_id] = tnlid self._tunnels[tnlid] = Tunnel(tnlid, olid, peer_id) self.register_cbt("Logger", "LOG_DEBUG", "TunnelId:{0} auth for Peer:{1} completed" .format(tnlid[:7], peer_id[:7])) cbt.set_response("Auth completed, TunnelId:{0}".format(tnlid[:7]), True) lnkupd_param = { "UpdateType": "LnkEvAuthorized", "OverlayId": olid, "PeerId": peer_id, "TunnelId": tnlid} self._link_updates_publisher.post_update(lnkupd_param) self.complete_cbt(cbt) def req_handler_create_tunnel(self, cbt): """ Handle the request for capability LNK_CREATE_TUNNEL. The caller provides the overlay id and the peer id which the link connects. The link id is generated here but it is returned to the caller after the local endpoint creation is completed asynchronously. The link is not necessarily ready for read/write at this time. The link status can be queried to determine when it is writeable. The link id is communicated in the request and will be the same at both nodes. """ # Create Link: Phase 1 Node A olid = cbt.request.params["OverlayId"] peer_id = cbt.request.params["PeerId"] if peer_id in self._peers[olid]: # Tunnel already exists tnlid = self._peers[olid][peer_id] tnl = self._tunnels[tnlid] if not tnl.link: # we need to create the link lnkid = tnlid self.register_cbt("Logger", "LOG_DEBUG", "Create Link:{} Tunnel exists. " "Skipping phase 1/5 Node A - Peer: {}" .format(lnkid[:7], peer_id[:7])) lnkupd_param = { "UpdateType": "LnkEvCreating", "OverlayId": olid, "PeerId": peer_id, "TunnelId": tnlid, "LinkId": lnkid} self._link_updates_publisher.post_update(lnkupd_param) self.register_cbt("Logger", "LOG_DEBUG", "Create Link:{} Phase 2/5 Node A - Peer: {}" .format(lnkid[:7], peer_id[:7])) self._assign_link_to_tunnel(tnlid, lnkid, 0xA2) tnl.tunnel_state = Tunnel.STATES.TNL_CREATING #tnl.creation_start_time = time.time() # create and send remote action to request endpoint from peer params = { "OverlayId": olid, "TunnelId": tnlid, "LinkId": lnkid, "NodeData": { "FPR": tnl.fpr, "MAC": tnl.mac, "UID": self.node_id } } remote_act = dict(OverlayId=olid, RecipientId=peer_id, RecipientCM="LinkManager", Action="LNK_REQ_LINK_ENDPT", Params=params) endp_cbt = self.create_linked_cbt(cbt) endp_cbt.set_request(self.module_name, "Signal", "SIG_REMOTE_ACTION", remote_act) # Send the message via SIG server to peer self.submit_cbt(endp_cbt) else: # Link already exists, TM should clean up first cbt.set_response("A link already exist or is being created for " "overlay id: {0} peer id: {1}" .format(olid, peer_id), False) self.complete_cbt(cbt) return # No tunnel exists, going to create it. tnlid = cbt.request.params["TunnelId"] lnkid = tnlid # index for quick peer->link lookup self._peers[olid][peer_id] = tnlid self._tunnels[tnlid] = Tunnel(tnlid, olid, peer_id, Tunnel.STATES.TNL_CREATING, self.config["LinkSetupTimeout"]) self._assign_link_to_tunnel(tnlid, lnkid, 0xA1) self.register_cbt("Logger", "LOG_DEBUG", "Create Link:{} Phase 1/5 Node A - Peer: {}" .format(lnkid[:7], peer_id[:7])) lnkupd_param = {"UpdateType": "LnkEvCreating", "OverlayId": olid, "PeerId": peer_id, "TunnelId": tnlid, "LinkId": lnkid} self._link_updates_publisher.post_update(lnkupd_param) params = {"OverlayId": olid, "TunnelId": tnlid, "LinkId": lnkid, "Type": self.config["Overlays"][olid]["Type"], "PeerId": peer_id} self._create_tunnel(params, parent_cbt=cbt) def resp_handler_create_tunnel(self, cbt): # Create Link: Phase 2 Node A parent_cbt = cbt.parent lnkid = cbt.request.params["LinkId"] # config overlay id tnlid = cbt.request.params["TunnelId"] resp_data = cbt.response.data if not cbt.response.status: self._cleanup_removed_tunnel(lnkid) self.free_cbt(cbt) parent_cbt.set_response(resp_data, False) self.complete_cbt(parent_cbt) self.register_cbt("Logger", "LOG_WARNING", "The create tunnel operation failed:{}" .format(parent_cbt.response.data)) return # transistion connection connection state self._tunnels[tnlid].link.creation_state = 0xA2 # store the overlay data overlay_id = cbt.request.params["OverlayId"] # config overlay id self.register_cbt("Logger", "LOG_DEBUG", "Create Link:{} Phase 2/5 Node A" .format(lnkid[:7])) self._update_tunnel_descriptor(resp_data, tnlid) # create and send remote action to request endpoint from peer params = {"OverlayId": overlay_id, "TunnelId": tnlid, "LinkId": lnkid} self._request_peer_endpoint(params, parent_cbt) self.free_cbt(cbt) def req_handler_req_link_endpt(self, lnk_endpt_cbt): params = lnk_endpt_cbt.request.params olid = params["OverlayId"] tnlid = params["TunnelId"] node_data = params["NodeData"] peer_id = node_data["UID"] if olid not in self.config["Overlays"]: self.register_cbt("Logger", "LOG_WARNING", "The requested overlay is not specified in " "local config, it will not be created") lnk_endpt_cbt.set_response("Unknown overlay id specified in request", False) self.complete_cbt(lnk_endpt_cbt) return if peer_id not in self._peers[olid] or tnlid not in self._tunnels: msg = str("The requested lnk endpt was not authorized it will not be created. " "TunnelId={0}, PeerId={1}".format(tnlid, peer_id)) self.register_cbt("Logger", "LOG_WARNING", msg) lnk_endpt_cbt.set_response(msg, False) self.complete_cbt(lnk_endpt_cbt) return if self._tunnels[tnlid].link: msg = str("A link already exist for this tunnel, it will not be created. " "TunnelId={0}, PeerId={1}".format(tnlid, peer_id)) self.register_cbt("Logger", "LOG_WARNING", msg) lnk_endpt_cbt.set_response(msg, False) self.complete_cbt(lnk_endpt_cbt) return lnkid = tnlid self._tunnels[tnlid].tunnel_state = Tunnel.STATES.TNL_CREATING self._tunnels[tnlid].timeout = time.time() + self.config["LinkSetupTimeout"] self._assign_link_to_tunnel(tnlid, lnkid, 0xB1) # publish notification of link creation initiated Node B lnkupd_param = { "UpdateType": "LnkEvCreating", "OverlayId": olid, "PeerId": peer_id, "TunnelId": tnlid, "LinkId": lnkid} self._link_updates_publisher.post_update(lnkupd_param) # Send request to Tincan ol_type = self.config["Overlays"][olid]["Type"] tap_name = self.config["Overlays"][olid]["TapName"][:8] + str(peer_id[:7]) self.log("LOG_DEBUG", "IgnoredNetInterfaces: %s", self._get_ignored_tap_names(olid, tap_name)) create_link_params = { "OverlayId": olid, # overlay params "TunnelId": tnlid, "NodeId": self.node_id, "StunServers": self.config.get("Stun", []), "Type": ol_type, "TapName": tap_name, "IP4": self.config["Overlays"][olid].get("IP4"), "MTU4": self.config["Overlays"][olid].get("MTU4"), "IP4PrefixLen": self.config["Overlays"][olid].get("IP4PrefixLen"), "IgnoredNetInterfaces": list( self._get_ignored_tap_names(olid, tap_name)), # link params "LinkId": lnkid, "NodeData": { "FPR": node_data["FPR"], "MAC": node_data["MAC"], "UID": node_data["UID"]}} if self.config.get("Turn"): create_link_params["TurnServers"] = self.config["Turn"] lcbt = self.create_linked_cbt(lnk_endpt_cbt) lcbt.set_request(self.module_name, "TincanInterface", "TCI_CREATE_LINK", create_link_params) self.submit_cbt(lcbt) def _complete_link_endpt_request(self, cbt): # Create Link: Phase 4 Node B parent_cbt = cbt.parent resp_data = cbt.response.data lnkid = cbt.request.params["LinkId"] tnlid = self.tunnel_id(lnkid) peer_id = cbt.request.params["NodeData"]["UID"] if not cbt.response.status: self.free_cbt(cbt) parent_cbt.set_response(resp_data, False) if parent_cbt.child_count == 1: self.complete_cbt(parent_cbt) self.register_cbt("Logger", "LOG_WARNING", "Create link endpoint failed :{}" .format(cbt.response.data)) self._rollback_link_creation_changes(tnlid) return self.register_cbt("Logger", "LOG_DEBUG", "Create Link:{} Phase 2/4 Node B - Peer: {}" .format(lnkid[:7], peer_id[:7])) # store the overlay data self._update_tunnel_descriptor(resp_data, tnlid) # add the peer MAC to the tunnel descr node_data = cbt.request.params["NodeData"] self._tunnels[tnlid].peer_mac = node_data["MAC"] self._tunnels[tnlid].link.creation_state = 0xB2 # respond with this nodes connection parameters node_data = { "MAC": resp_data["MAC"], "FPR": resp_data["FPR"], "UID": self.node_id, "CAS": resp_data["CAS"] } data = { "OverlayId": cbt.request.params["OverlayId"], "TunnelId": tnlid, "LinkId": lnkid, "NodeData": node_data } self.free_cbt(cbt) parent_cbt.set_response(data, True) self.complete_cbt(parent_cbt) def _complete_link_creation(self, cbt, parent_cbt): rem_act = parent_cbt.request.params lnkid = rem_act["LinkId"] tnlid = self.tunnel_id(lnkid) peer_id = rem_act["NodeData"]["UID"] self._tunnels[tnlid].link.creation_state = 0xC0 self.register_cbt("Logger", "LOG_DEBUG", "Create Link:{} Phase 4/4 Node B - Peer: {}" .format(lnkid[:7], peer_id[:7])) peer_id = rem_act["NodeData"]["UID"] olid = rem_act["OverlayId"] resp_data = cbt.response.data node_data = { "MAC": resp_data["MAC"], "FPR": resp_data["FPR"], "UID": self.node_id, "CAS": resp_data["CAS"] } data = { "OverlayId": cbt.request.params["OverlayId"], "TunnelId": tnlid, "LinkId": lnkid, "NodeData": node_data } parent_cbt.set_response(data=data, status=True) self.free_cbt(cbt) self.complete_cbt(parent_cbt) self.register_cbt("Logger", "LOG_INFO", "Tunnel {0} Link {1} accepted: {2}:{3}<-{4}" .format(tnlid[:7], lnkid[:7], olid[:7], self.node_id[:7], peer_id[:7])) def _create_link_endpoint(self, rem_act, parent_cbt): """ Send the Createlink control to local Tincan """ # Create Link: Phase 5 Node A lnkid = rem_act["Data"]["LinkId"] tnlid = self.tunnel_id(lnkid) peer_id = rem_act["RecipientId"] if tnlid not in self._tunnels: # abort the handshake as the process timed out parent_cbt.set_response("Tunnel creation timeout failure", False) self.complete_cbt(parent_cbt) return self._tunnels[tnlid].link.creation_state = 0xA3 self.register_cbt("Logger", "LOG_DEBUG", "Create Link:{} Phase 3/5 Node A - Peer: {}" .format(lnkid[:7], peer_id[:7])) node_data = rem_act["Data"]["NodeData"] olid = rem_act["OverlayId"] # add the peer MAC to the tunnel descr self._tunnels[tnlid].peer_mac = node_data["MAC"] cbt_params = {"OverlayId": olid, "TunnelId": tnlid, "LinkId": lnkid, "Type": "TUNNEL", "NodeData": { "UID": node_data["UID"], "MAC": node_data["MAC"], "CAS": node_data["CAS"], "FPR": node_data["FPR"]}} lcbt = self.create_linked_cbt(parent_cbt) lcbt.set_request(self.module_name, "TincanInterface", "TCI_CREATE_LINK", cbt_params) self.submit_cbt(lcbt) def _send_local_cas_to_peer(self, cbt): # Create Link: Phase 6 Node A lnkid = cbt.request.params["LinkId"] tnlid = self.tunnel_id(lnkid) peer_id = cbt.request.params["NodeData"]["UID"] self._tunnels[tnlid].link.creation_state = 0xA4 self.register_cbt("Logger", "LOG_DEBUG", "Create Link:{} Phase 4/5 Node A - Peer: {}" .format(lnkid[:7], peer_id[:7])) local_cas = cbt.response.data["CAS"] parent_cbt = cbt.parent olid = cbt.request.params["OverlayId"] peerid = parent_cbt.request.params["PeerId"] params = { "OverlayId": olid, "TunnelId": tnlid, "LinkId": lnkid, "NodeData": { "UID": self.node_id, "MAC": cbt.response.data["MAC"], "CAS": local_cas, "FPR": cbt.response.data["FPR"]}} remote_act = dict(OverlayId=olid, RecipientId=peerid, RecipientCM="LinkManager", Action="LNK_ADD_PEER_CAS", Params=params) lcbt = self.create_linked_cbt(parent_cbt) lcbt.set_request(self.module_name, "Signal", "SIG_REMOTE_ACTION", remote_act) self.submit_cbt(lcbt) self.free_cbt(cbt) def req_handler_add_peer_cas(self, cbt): # Create Link: Phase 7 Node B params = cbt.request.params olid = params["OverlayId"] lnkid = params["LinkId"] tnlid = self.tunnel_id(lnkid) peer_id = params["NodeData"]["UID"] if peer_id not in self._peers[olid] or tnlid not in self._tunnels \ or self._tunnels[tnlid].link is None: self._cleanup_removed_tunnel(tnlid) self.register_cbt("Logger", "LOG_DEBUG", "A response to an aborted add peer CAS operation was discarded: {0}". format(str(cbt))) return self._tunnels[tnlid].link.creation_state = 0xB3 self.register_cbt("Logger", "LOG_DEBUG", "Create Link:{} Phase 3/4 Node B - Peer: {}" .format(lnkid[:7], peer_id[:7])) lcbt = self.create_linked_cbt(cbt) params["Type"] = self.config["Overlays"][olid]["Type"] lcbt.set_request(self.module_name, "TincanInterface", "TCI_CREATE_LINK", params) self.submit_cbt(lcbt) def resp_handler_create_link_endpt(self, cbt): parent_cbt = cbt.parent resp_data = cbt.response.data if not cbt.response.status: self.register_cbt("Logger", "LOG_WARNING", "Create link endpoint failed :{}" .format(cbt)) lnkid = cbt.request.params["LinkId"] self._rollback_link_creation_changes(lnkid) self.free_cbt(cbt) parent_cbt.set_response(resp_data, False) self.complete_cbt(parent_cbt) return if parent_cbt.request.action == "LNK_REQ_LINK_ENDPT": """ To complete this request the responding node has to supply its own NodeData and CAS. The NodeData was previously queried and is stored on the parent cbt. Add the cas and send to peer. """ self._complete_link_endpt_request(cbt) elif parent_cbt.request.action == "LNK_CREATE_TUNNEL": """ Both endpoints are created now but the peer doesn't have our cas. It already has the node data so no need to send that again. """ self._send_local_cas_to_peer(cbt) elif parent_cbt.request.action == "LNK_ADD_PEER_CAS": """ The link creation handshake is complete on Node B, complete the outstanding request and publish notifications via subscription. """ self._complete_link_creation(cbt, parent_cbt) def _complete_create_link_request(self, parent_cbt): # Create Link: Phase 9 Node A # Complete the cbt that started this all olid = parent_cbt.request.params["OverlayId"] peer_id = parent_cbt.request.params["PeerId"] if peer_id not in self._peers[olid]: self.register_cbt("Logger", "LOG_DEBUG", "A response to an aborted create link operation was discarded: {0}". format(parent_cbt)) return tnlid = self._peers[olid][peer_id] lnkid = self.link_id(tnlid) self._tunnels[tnlid].link.creation_state = 0xC0 self.register_cbt("Logger", "LOG_DEBUG", "Create Link:{} Phase 5/5 Node A - Peer: {}" .format(tnlid[:7], peer_id[:7])) parent_cbt.set_response(data={"LinkId": lnkid}, status=True) self.complete_cbt(parent_cbt) self.register_cbt("Logger", "LOG_INFO", "Tunnel {0} created: {1}:{2}->{3}" .format(lnkid[:7], olid[:7], self.node_id[:7], peer_id[:7])) def resp_handler_remote_action(self, cbt): parent_cbt = cbt.parent resp_data = cbt.response.data if not cbt.response.status: lnkid = cbt.request.params["Params"]["LinkId"] tnlid = self.tunnel_id(lnkid) self._rollback_link_creation_changes(tnlid) self.free_cbt(cbt) parent_cbt.set_response(resp_data, False) self.complete_cbt(parent_cbt) else: rem_act = cbt.response.data self.free_cbt(cbt) if rem_act["Action"] == "LNK_REQ_LINK_ENDPT": self._create_link_endpoint(rem_act, parent_cbt) elif rem_act["Action"] == "LNK_ADD_PEER_CAS": self._complete_create_link_request(parent_cbt) def req_handler_tincan_msg(self, cbt): lts = time.time() if cbt.request.params["Command"] == "LinkStateChange": if cbt.request.params["Data"] == "LINK_STATE_DOWN": # issue a link state check lnkid = cbt.request.params["LinkId"] tnlid = cbt.request.params["TunnelId"] self.log("LOG_DEBUG", "LINK STATE DOWN cbt=%s", cbt) self._tunnels[tnlid].tunnel_state = Tunnel.STATES.TNL_QUERYING self.register_cbt("TincanInterface", "TCI_QUERY_LINK_STATS", [tnlid]) elif cbt.request.params["Data"] == "LINK_STATE_UP": lnkid = cbt.request.params["LinkId"] tnlid = self.tunnel_id(lnkid) olid = self._tunnels[tnlid].overlay_id peer_id = self._tunnels[tnlid].peer_id lnk_status = self._tunnels[tnlid].tunnel_state self._tunnels[tnlid].tunnel_state = Tunnel.STATES.TNL_ONLINE if lnk_status != Tunnel.STATES.TNL_QUERYING: param = { "UpdateType": "LnkEvConnected", "OverlayId": olid, "PeerId": peer_id, "TunnelId": tnlid, "LinkId": lnkid, "ConnectedTimestamp": lts, "TapName": self._tunnels[tnlid].tap_name, "MAC": self._tunnels[tnlid].mac, "PeerMac": self._tunnels[tnlid].peer_mac} self._link_updates_publisher.post_update(param) elif lnk_status == Tunnel.STATES.TNL_QUERYING: # Do not post a notification if the the connection state was being queried self._tunnels[tnlid].link.status_retry = 0 # if the lnk_status is TNL_OFFLINE the recconect event came in too late and the # tear down has already been issued. This scenario is unlikely as the recheck time # is long enough such that the webrtc reconnect attempts will have been abandoned. cbt.set_response(data=None, status=True) else: cbt.set_response(data=None, status=True) self.complete_cbt(cbt) def process_cbt(self, cbt): with self._lock: if cbt.op_type == "Request": if cbt.request.action == "LNK_CREATE_TUNNEL": # Create Link: Phase 1 Node A # TOP wants a new link, first SIGnal peer to create endpt self.req_handler_create_tunnel(cbt) elif cbt.request.action == "LNK_REQ_LINK_ENDPT": # Create Link: Phase 3 Node B # Rcvd peer req to create endpt, send to TCI self.req_handler_req_link_endpt(cbt) elif cbt.request.action == "LNK_ADD_PEER_CAS": # Create Link: Phase 7 Node B # CAS rcvd from peer, sends to TCI to update link's peer CAS info self.req_handler_add_peer_cas(cbt) elif cbt.request.action == "LNK_REMOVE_TUNNEL": self.req_handler_remove_tnl(cbt) elif cbt.request.action == "LNK_REMOVE_LINK": self.req_handler_remove_link(cbt) elif cbt.request.action == "LNK_QUERY_TUNNEL_INFO": self.req_handler_query_tunnels_info(cbt) elif cbt.request.action == "VIS_DATA_REQ": self.req_handler_query_viz_data(cbt) elif cbt.request.action == "TCI_TINCAN_MSG_NOTIFY": self.req_handler_tincan_msg(cbt) elif cbt.request.action == "LNK_ADD_IGN_INF": self.req_handler_add_ign_inf(cbt) elif cbt.request.action == "LNK_AUTH_TUNNEL": self.req_handler_auth_tunnel(cbt) else: self.req_handler_default(cbt) elif cbt.op_type == "Response": if cbt.request.action == "SIG_REMOTE_ACTION": # Create Link: Phase 5 Node A # Attempt to create our end of link # Create Link: Phase 9 Node A # Link created, notify others self.resp_handler_remote_action(cbt) elif cbt.request.action == "TCI_CREATE_LINK": # Create Link: Phase 4 Node B # Create Link: Phase 6 Node A # SIGnal to peer to update CAS # Create Link: Phase 8 Node B # Complete setup self.resp_handler_create_link_endpt(cbt) elif cbt.request.action == "TCI_CREATE_TUNNEL": # Create Link: Phase 2 Node A # Retrieved our node data for response self.resp_handler_create_tunnel(cbt) elif cbt.request.action == "TCI_QUERY_LINK_STATS": self.resp_handler_query_link_stats(cbt) elif cbt.request.action == "TCI_REMOVE_LINK": self.resp_handler_remove_link(cbt) elif cbt.request.action == "TCI_REMOVE_TUNNEL": self.resp_handler_remove_tunnel(cbt) else: parent_cbt = cbt.parent cbt_data = cbt.response.data cbt_status = cbt.response.status self.free_cbt(cbt) if (parent_cbt is not None and parent_cbt.child_count == 1): parent_cbt.set_response(cbt_data, cbt_status) self.complete_cbt(parent_cbt) def _deauth_tnl(self, tnl): self.register_cbt("Logger", "LOG_INFO", "Tunnel {0} auth timed out".format(tnl.tnlid)) param = { "UpdateType": "LnkEvDeauthorized", "OverlayId": tnl.overlay_id, "PeerId": tnl.peer_id, "TunnelId": tnl.tnlid, "TapName": tnl.tap_name} self._link_updates_publisher.post_update(param) self._cleanup_tunnel(tnl) def _cleanup_expired_incomplete_links(self): deauth = [] rollbk = [] for tnlid, tnl in self._tunnels.items(): if tnl.tunnel_state == Tunnel.STATES.TNL_AUTHORIZED and time.time() > tnl.timeout: deauth.append(tnl) elif tnl.link is not None and tnl.link.creation_state != 0xC0 and \ time.time() > tnl.timeout: rollbk.append(tnlid) for tnl in deauth: self._deauth_tnl(tnl) for tnlid in rollbk: self._rollback_link_creation_changes(tnlid) def timer_method(self): with self._lock: self._cleanup_expired_incomplete_links() self._query_link_stats() self.trace_state() def terminate(self): pass def req_handler_query_viz_data(self, cbt): nid = self.node_id tnls = dict() for tnlid in self._tunnels: if self._tunnels[tnlid].link is None: continue tnl_data = { "NodeId": nid, "PeerId": self._tunnels[tnlid].peer_id, "TunnelState": self._tunnels[tnlid].tunnel_state } if self._tunnels[tnlid].tap_name: tnl_data["TapName"] = self._tunnels[tnlid].tap_name if self._tunnels[tnlid].mac: tnl_data["MAC"] = self._tunnels[tnlid].mac #if "IceRole" in self._tunnels[tnlid]["Link"]: # tnl_data["IceRole"] = self._tunnels[tnlid]["Link"]["IceRole"] if self._tunnels[tnlid].link.stats: tnl_data["Stats"] = self._tunnels[tnlid].link.stats overlay_id = self._tunnels[tnlid].overlay_id if overlay_id not in tnls: tnls[overlay_id] = dict() if nid not in tnls[overlay_id]: tnls[overlay_id][nid] = dict() tnls[overlay_id][nid][tnlid] = tnl_data cbt.set_response({"LinkManager": tnls}, bool(tnls)) self.complete_cbt(cbt)