mirror of https://github.com/EdgeVPNio/evio.git
315 lines
15 KiB
Python
315 lines
15 KiB
Python
# 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 time
|
|
from copy import deepcopy
|
|
from collections import namedtuple
|
|
from .NetworkGraph import ConnectionEdge
|
|
from .NetworkGraph import ConnEdgeAdjacenctList
|
|
import modules.NetworkGraph as ng
|
|
|
|
|
|
EdgeRequest = namedtuple("EdgeRequest",
|
|
["overlay_id", "edge_id", "edge_type", "initiator_id", "recipient_id"])
|
|
EdgeResponse = namedtuple("EdgeResponse", ["is_accepted", "data"])
|
|
EdgeNegotiate = namedtuple("EdgeNegotiate", EdgeRequest._fields + EdgeResponse._fields)
|
|
|
|
"""
|
|
CE enters the nego list with state CEStatePreAuth
|
|
Successful auth updates the state to CEStateAuthorized and removes it from nego list
|
|
"""
|
|
class NetworkBuilder():
|
|
_DEL_RETRY_INTERVAL = 10
|
|
"""description of class"""
|
|
def __init__(self, top_man, overlay_id, node_id):
|
|
self._current_adj_list = ConnEdgeAdjacenctList(overlay_id, node_id)
|
|
self._pending_adj_list = None
|
|
self._negotiated_edges = {}
|
|
self._top = top_man
|
|
self._ops = {}
|
|
|
|
def __repr__(self):
|
|
# state = "current_adj_list=%s, pending_adj_list=%s, negotiated_edges=%s, "\
|
|
# "refresh_in_progress=%s, _max_concurrent_wrkload=%s" % \
|
|
# (self._current_adj_list, self._pending_adj_list, self._negotiated_edges,
|
|
# self._refresh_in_progress, self._max_concurrent_wrkload)
|
|
state = "current_adj_list=%s, pending_adj_list=%s, negotiated_edges=%s, " % \
|
|
(self._current_adj_list, self._pending_adj_list, self._negotiated_edges)
|
|
return state
|
|
|
|
@property
|
|
def is_ready(self):
|
|
"""
|
|
Is the NetworkBuilder ready for a new NetGraph? This means all the entries in the
|
|
pending adj list has been cleared.
|
|
"""
|
|
return self._is_ready()
|
|
|
|
def _is_ready(self):
|
|
return not bool(self._pending_adj_list)
|
|
|
|
def get_adj_list(self):
|
|
return deepcopy(self._current_adj_list)
|
|
|
|
def refresh(self, net_graph=None):
|
|
"""
|
|
Transitions the overlay network overlay to the desired state specified by pending
|
|
adjacency list.
|
|
"""
|
|
self._top.log("LOG_DEBUG", "New net graph: %s", str(net_graph))
|
|
assert ((self._is_ready() and bool(net_graph)) or
|
|
(not self._is_ready() and not bool(net_graph))),\
|
|
"Netbuilder is not ready for a new net graph"
|
|
|
|
if net_graph and self._is_ready():
|
|
self._pending_adj_list = net_graph
|
|
self._current_adj_list.max_successors = net_graph.max_successors
|
|
self._current_adj_list.max_ldl = net_graph.max_ldl
|
|
self._current_adj_list.max_ondemand = net_graph.max_ondemand
|
|
self._current_adj_list.update_closest()
|
|
self._process_pending_adj_list()
|
|
self._create_new_edges()
|
|
self._remove_edges()
|
|
|
|
def update_edge_state(self, event):
|
|
"""
|
|
Updates the connection edge's current state based on the provided event. The number of CEs
|
|
not in the EdgeState CEStateConnected is used to limit the number of edges being
|
|
constructed concurrently.
|
|
"""
|
|
peer_id = event["PeerId"]
|
|
edge_id = event["TunnelId"]
|
|
overlay_id = event["OverlayId"]
|
|
if event["UpdateType"] == "LnkEvAuthorized":
|
|
self._add_incoming_auth_conn_edge(peer_id)
|
|
elif event["UpdateType"] == "LnkEvDeauthorized":
|
|
ce = self._current_adj_list[peer_id]
|
|
assert ce.edge_state == "CEStateAuthorized", "Deauth CE={0}".format(ce)
|
|
ce.edge_state = "CEStateDeleting"
|
|
del self._current_adj_list[peer_id]
|
|
del self._pending_adj_list[peer_id]
|
|
elif event["UpdateType"] == "LnkEvCreating":
|
|
conn_edge = self._current_adj_list.conn_edges.get(peer_id, None)
|
|
conn_edge.edge_state = "CEStateCreated"
|
|
elif event["UpdateType"] == "LnkEvConnected":
|
|
self._current_adj_list[peer_id].edge_state = "CEStateConnected"
|
|
self._current_adj_list[peer_id].connected_time = \
|
|
event["ConnectedTimestamp"]
|
|
del self._pending_adj_list[peer_id]
|
|
elif event["UpdateType"] == "LnkEvDisconnected":
|
|
# the local topology did not request removal of the connection
|
|
self._top.log("LOG_DEBUG", "CEStateDisconnected event recvd peer_id: %s, edge_id: %s",
|
|
peer_id, edge_id)
|
|
self._current_adj_list[peer_id].edge_state = "CEStateDisconnected"
|
|
self._top.top_remove_edge(overlay_id, peer_id)
|
|
elif event["UpdateType"] == "LnkEvRemoved":
|
|
self._current_adj_list[peer_id].edge_state = "CEStateDeleting"
|
|
del self._current_adj_list[peer_id]
|
|
del self._pending_adj_list[peer_id]
|
|
elif event["UpdateType"] == "RemoveEdgeFailed":
|
|
# leave the node in the adj list and marked for removal to be retried.
|
|
# the retry occurs too quickly and causes too many attempts before it succeeds
|
|
# self._refresh_in_progress -= 1
|
|
self._current_adj_list[peer_id].created_time = \
|
|
time.time() + NetworkBuilder._DEL_RETRY_INTERVAL
|
|
else:
|
|
self._top.log("LOG_WARNING", "Invalid UpdateType specified for event")
|
|
|
|
def _mark_edges_for_removal(self):
|
|
"""
|
|
Anything edge the set (Active - Pending) is marked for deletion but do not remove
|
|
negotiated edges.
|
|
"""
|
|
for peer_id in self._current_adj_list:
|
|
if self._current_adj_list[peer_id].edge_type in ng.EdgeTypesIn:
|
|
continue # do not remove incoming edges
|
|
if peer_id in self._pending_adj_list:
|
|
continue # the edge should be maintained
|
|
if self._current_adj_list[peer_id].edge_state != "CEStateConnected":
|
|
# don't delete an edge before it completes the create process. if it fails LNK will
|
|
# initiate the removal.
|
|
continue
|
|
if time.time() - self._current_adj_list[peer_id].connected_time < 30:
|
|
continue # events get supressed
|
|
self._current_adj_list[peer_id].marked_for_delete = True
|
|
self._top.log("LOG_DEBUG", "Marked connedge for delete: %s",
|
|
str(self._current_adj_list[peer_id]))
|
|
|
|
def _remove_edges(self):
|
|
"""
|
|
Removes a connected edge that was previousls marked for deletion. Minimize churn by
|
|
removing a single edge per invokation.
|
|
"""
|
|
overlay_id = self._current_adj_list.overlay_id
|
|
for peer_id in self._current_adj_list:
|
|
ce = self._current_adj_list[peer_id]
|
|
if (ce.marked_for_delete and ce.edge_state == "CEStateConnected"):
|
|
ce.edge_state = "CEStateDeleting"
|
|
self._top.top_remove_edge(overlay_id, peer_id)
|
|
return
|
|
|
|
def _create_new_edges(self):
|
|
for peer_id, ce in self._negotiated_edges.items():
|
|
if ce.edge_state == "CEStateInitialized":
|
|
# avoid repeat auth request by only acting on CEStateInitialized
|
|
ce.edge_state = "CEStatePreAuth"
|
|
self._negotiate_new_edge(ce.edge_id, ce.edge_type, peer_id)
|
|
|
|
def _process_pending_adj_list(self):
|
|
"""
|
|
Sync the network state by determining the difference between the active and pending net
|
|
graphs. Create new successors edges before removing existing ones.
|
|
"""
|
|
rmv_list = []
|
|
if self._current_adj_list.overlay_id != self._pending_adj_list.overlay_id:
|
|
raise ValueError("Overlay ID mismatch adj lists, active:{0}, pending:{1}".
|
|
format(self._current_adj_list.overlay_id,
|
|
self._pending_adj_list.overlay_id))
|
|
self._mark_edges_for_removal()
|
|
|
|
# Any edge in set (Pending - Active) is added for nego
|
|
for peer_id in self._pending_adj_list:
|
|
ce = self._pending_adj_list[peer_id]
|
|
#if ce.edge_type == "CETypeLongDistance" and \
|
|
# self._current_adj_list.num_ldl >= self._current_adj_list.max_ldl:
|
|
# continue
|
|
if peer_id not in self._negotiated_edges and peer_id not in self._current_adj_list:
|
|
self._current_adj_list[peer_id] = ce
|
|
self._negotiated_edges[peer_id] = ce
|
|
else:
|
|
rmv_list.append(peer_id)
|
|
|
|
for peer_id in rmv_list:
|
|
del self._pending_adj_list[peer_id]
|
|
|
|
def _negotiate_new_edge(self, edge_id, edge_type, peer_id):
|
|
""" Role A1 """
|
|
olid = self._current_adj_list.overlay_id
|
|
nid = self._current_adj_list.node_id
|
|
er = EdgeRequest(overlay_id=olid, edge_id=edge_id, edge_type=edge_type,
|
|
recipient_id=peer_id, initiator_id=nid)
|
|
self._top.top_send_negotiate_edge_req(er)
|
|
|
|
def _resolve_request_collision(self, edge_req):
|
|
nid = self._top.node_id
|
|
peer_id = edge_req.initiator_id
|
|
ce = self._current_adj_list[peer_id]
|
|
edge_state = ce.edge_state
|
|
edge_resp = None
|
|
if edge_state in ("CEStateAuthorized", "CEStateCreated", "CEStateConnected"):
|
|
msg = "E1 - A valid edge already exists. TunnelId={0}"\
|
|
.format(self._current_adj_list[peer_id].edge_id[:7])
|
|
edge_resp = EdgeResponse(is_accepted=False, data=msg)
|
|
self._top.log("LOG_DEBUG", msg)
|
|
elif edge_state == "CEStateInitialized":
|
|
edge_resp = EdgeResponse(is_accepted=True, data="Precollision edge permitted")
|
|
del self._current_adj_list[peer_id]
|
|
del self._pending_adj_list[peer_id]
|
|
del self._negotiated_edges[peer_id]
|
|
elif edge_state == "CEStatePreAuth" and nid < edge_req.initiator_id:
|
|
msg = "E2 - Node {0} superceeds edge request due to collision, "\
|
|
"edge={1}".format(nid, self._current_adj_list[peer_id].edge_id[:7])
|
|
edge_resp = EdgeResponse(is_accepted=False, data=msg)
|
|
self._top.log("LOG_DEBUG", msg)
|
|
elif edge_state == "CEStatePreAuth" and nid > edge_req.initiator_id:
|
|
ce.edge_type = ng.transpose_edge_type(edge_req.edge_type)
|
|
ce.edge_id = edge_req.edge_id
|
|
msg = "E0 - Node {2} accepts edge collision override. CE:{0} remapped -> edge:{1}"\
|
|
.format(ce, edge_req.edge_id[:7], nid)
|
|
edge_resp = EdgeResponse(is_accepted=True, data=msg)
|
|
self._top.log("LOG_DEBUG", msg)
|
|
else:
|
|
edge_resp = EdgeResponse(False, "E6 - Request colides with an edge being destroyed."\
|
|
"Try later")
|
|
assert bool(edge_resp), "NetBuilder={0}".format(self)
|
|
return edge_resp
|
|
|
|
def negotiate_incoming_edge(self, edge_req):
|
|
""" Role B1 """
|
|
self._top.log("LOG_DEBUG", "Rcvd EdgeRequest=%s", str(edge_req))
|
|
edge_resp = None
|
|
peer_id = edge_req.initiator_id
|
|
if peer_id in self._current_adj_list:
|
|
edge_resp = self._resolve_request_collision(edge_req)
|
|
elif edge_req.edge_type == "CETypeSuccessor":
|
|
edge_resp = EdgeResponse(is_accepted=True, data="Successor edge permitted")
|
|
elif edge_req.edge_type == "CETypeEnforced":
|
|
edge_resp = EdgeResponse(is_accepted=True, data="Enforced edge permitted")
|
|
elif edge_req.edge_type == "CETypeOnDemand":
|
|
edge_resp = EdgeResponse(is_accepted=True, data="On-demand edge permitted")
|
|
elif not self._current_adj_list.is_threshold_ildl():
|
|
edge_resp = EdgeResponse(is_accepted=True, data="Any edge permitted")
|
|
else:
|
|
edge_resp = EdgeResponse(is_accepted=False,
|
|
data="E5 - Too many existing edges.")
|
|
|
|
if edge_resp.is_accepted and edge_resp.data[:2] != "E0":
|
|
et = ng.transpose_edge_type(edge_req.edge_type)
|
|
ce = ConnectionEdge(peer_id=peer_id, edge_id=edge_req.edge_id, edge_type=et)
|
|
ce.edge_state = "CEStatePreAuth"
|
|
self._negotiated_edges[peer_id] = ce
|
|
self._top.log("LOG_DEBUG", "New CE=%s added to negotiated_edges=%s", str(ce),
|
|
str(self._negotiated_edges))
|
|
return edge_resp
|
|
|
|
def _add_incoming_auth_conn_edge(self, peer_id):
|
|
""" Role B2 """
|
|
ce = self._negotiated_edges.pop(peer_id)
|
|
ce.edge_state = "CEStateAuthorized"
|
|
self._current_adj_list.add_conn_edge(ce)
|
|
|
|
def complete_edge_negotiation(self, edge_nego):
|
|
""" Role A2 """
|
|
self._top.log("LOG_DEBUG", "EdgeNegotiate=%s", str(edge_nego))
|
|
if edge_nego.recipient_id not in self._current_adj_list and \
|
|
edge_nego.recipient_id not in self._negotiated_edges:
|
|
self._top.log("LOG_ERROR", "Peer Id from edge negotiation not in current adjacency " \
|
|
" list or _negotiated_edges. The transaction has been discarded.")
|
|
return
|
|
peer_id = edge_nego.recipient_id
|
|
edge_id = edge_nego.edge_id
|
|
|
|
ce = self._negotiated_edges.get(edge_nego.recipient_id, None) # do not pop here, E0 needed
|
|
if not ce:
|
|
return # OK - Collision override occurred, CE was popped in role B2 (above). Completion
|
|
# order can vary, in other case handled below.
|
|
if not edge_nego.is_accepted:
|
|
# if E2 (request superceeded) do nothing here. The corresponding CE instance will
|
|
# be converted in resolve_collision_request().
|
|
if edge_nego.data[:2] != "E2":
|
|
ce.edge_state = "CEStateDeleting"
|
|
self._negotiated_edges.pop(ce.peer_id)
|
|
del self._pending_adj_list[peer_id]
|
|
del self._current_adj_list[ce.peer_id]
|
|
else:
|
|
if ce.edge_id != edge_nego.edge_id:
|
|
self._top.log("LOG_ERROR", "EdgeNego parameters does not match current " \
|
|
"adjacency list, The transaction has been discarded.")
|
|
ce.edge_state = "CEStateDeleting"
|
|
self._negotiated_edges.pop(ce.peer_id)
|
|
del self._pending_adj_list[peer_id]
|
|
del self._current_adj_list[ce.peer_id]
|
|
else:
|
|
ce.edge_state = "CEStateAuthorized"
|
|
self._negotiated_edges.pop(ce.peer_id)
|
|
self._top.top_add_edge(self._current_adj_list.overlay_id, peer_id, edge_id)
|