# 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. try: import simplejson as json except ImportError: import json import threading import os import socketserver from abc import ABCMeta, abstractmethod from distutils import spawn from framework.ControllerModule import ControllerModule import framework.Modlib as Modlib class BridgeABC(): __metaclass__ = ABCMeta bridge_type = NotImplemented iptool = spawn.find_executable("ip") def __init__(self, name, ip_addr, prefix_len, mtu, cm): self.name = name self.ip_addr = ip_addr self.prefix_len = prefix_len self.mtu = mtu self.ports = set() self.cm = cm @abstractmethod def add_port(self, port_name): pass @abstractmethod def del_port(self, port_name): pass @abstractmethod def del_br(self): pass @property @abstractmethod def brctl(self,): pass def __repr__(self): """ Return a representaion of a bridge object. """ return "%s %s" % (self.bridge_type, self.name) def __str__(self): """ Return a string of the bridge name. """ return self.__repr__() ################################################################################################### class OvsBridge(BridgeABC): brctl = spawn.find_executable("ovs-vsctl") bridge_type = "OVS" def __init__(self, name, ip_addr, prefix_len, mtu, cm, stp_enable, sdn_ctrl_cfg=None): """ Initialize an OpenvSwitch bridge object. """ super(OvsBridge, self).__init__(name, ip_addr, prefix_len, mtu, cm) if OvsBridge.brctl is None or OvsBridge.iptool is None: raise RuntimeError("openvswitch-switch was not found" if not OvsBridge.brctl else "iproute2 was not found") self._patch_port = self.name[:3]+"-pp0" Modlib.runshell([OvsBridge.brctl, "--may-exist", "add-br", self.name]) if ip_addr and prefix_len: net = "{0}/{1}".format(ip_addr, prefix_len) Modlib.runshell([OvsBridge.iptool, "addr", "flush", "dev", self.name]) Modlib.runshell([OvsBridge.iptool, "addr", "add", net, "dev", self.name]) else: Modlib.runshell(["sysctl", "net.ipv6.conf.{}.disable_ipv6=1".format(self.name)]) Modlib.runshell([OvsBridge.iptool, "addr", "flush", self.name]) try: Modlib.runshell([OvsBridge.brctl, "set", "int", self.name, "mtu_request=" + str(self.mtu)]) except RuntimeError as e: self.cm.register_cbt("Logger", "LOG_WARN", "The following error occurred while setting" " MTU for OVS bridge: {0}".format(e)) self.stp(stp_enable) Modlib.runshell([OvsBridge.iptool, "link", "set", "dev", self.name, "up"]) if sdn_ctrl_cfg: self.add_sdn_ctrl(sdn_ctrl_cfg) def add_sdn_ctrl(self, sdn_ctrl_cfg): if sdn_ctrl_cfg["ConnectionType"] == "tcp": ctrl_conn_str = ":".join([sdn_ctrl_cfg["ConnectionType"], sdn_ctrl_cfg["HostName"], sdn_ctrl_cfg["Port"]]) Modlib.runshell([OvsBridge.brctl, "set-controller", self.name, ctrl_conn_str]) def del_sdn_ctrl(self): Modlib.runshell([OvsBridge.brctl, "del-controller", self.name]) def del_br(self): self.del_sdn_ctrl() Modlib.runshell([OvsBridge.brctl, "--if-exists", "del-br", self.name]) def add_port(self, port_name): Modlib.runshell([OvsBridge.iptool, "link", "set", "dev", port_name, "mtu", str(self.mtu)]) Modlib.runshell(["sysctl", "net.ipv6.conf.{}.disable_ipv6=1".format(port_name)]) Modlib.runshell([OvsBridge.iptool, "addr", "flush", port_name]) Modlib.runshell([OvsBridge.brctl, "--may-exist", "add-port", self.name, port_name]) self.ports.add(port_name) def del_port(self, port_name): Modlib.runshell([OvsBridge.brctl, "--if-exists", "del-port", self.name, port_name]) if port_name in self.ports: self.ports.remove(port_name) def stp(self, enable): Modlib.runshell([OvsBridge.brctl, "set", "bridge", self.name, "stp_enable={0}" .format("true" if enable else "false")]) def add_patch_port(self, peer_patch_port): iface_opt = "options:peer={0}".format(peer_patch_port) Modlib.runshell([OvsBridge.brctl, "--may-exist", "add-port", self.name, self._patch_port, "--", "set", "interface", self._patch_port, "type=patch", iface_opt]) def get_patch_port_name(self): return self._patch_port ################################################################################################### class LinuxBridge(BridgeABC): brctl = spawn.find_executable("brctl") bridge_type = "LXBR" def __init__(self, name, ip_addr, prefix_len, mtu, cm, stp_enable): """ Initialize a Linux bridge object. """ super(LinuxBridge, self).__init__(name, ip_addr, prefix_len, mtu, cm) if LinuxBridge.brctl is None or LinuxBridge.iptool is None: raise RuntimeError("bridge-utils was not found" if not LinuxBridge.brctl else "iproute2 was not found") p = Modlib.runshell([LinuxBridge.brctl, "show"]) wlist = map(str.split, p.stdout.decode("utf-8").splitlines()[1:]) brwlist = filter(lambda x: len(x) != 1, wlist) brlist = map(lambda x: x[0], brwlist) for br in brlist: if br == name: return p = Modlib.runshell([LinuxBridge.brctl, "addbr", self.name]) net = "{0}/{1}".format(ip_addr, prefix_len) if ip_addr and prefix_len: Modlib.runshell([LinuxBridge.iptool, "addr", "add", net, "dev", name]) self.stp(stp_enable) Modlib.runshell([LinuxBridge.iptool, "link", "set", "dev", name, "up"]) def del_br(self): # Set the device down and delete the bridge Modlib.runshell([LinuxBridge.iptool, "link", "set", "dev", self.name, "down"]) Modlib.runshell([LinuxBridge.brctl, "delbr", self.name]) def add_port(self, port_name): Modlib.runshell([LinuxBridge.iptool, "link", "set", port_name, "mtu", str(self.mtu)]) Modlib.runshell([LinuxBridge.brctl, "addif", self.name, port_name]) self.ports.add(port_name) def del_port(self, port_name): p = Modlib.runshell([LinuxBridge.brctl, "show", self.name]) wlist = map(str.split, p.stdout.decode("utf-8").splitlines()[1:]) port_lines = filter(lambda x: len(x) == 4, wlist) ports = map(lambda x: x[-1], port_lines) for port in ports: if port == port_name: Modlib.runshell([LinuxBridge.brctl, "delif", self.name, port_name]) if port_name in self.ports: self.ports.remove(port_name) def stp(self, val=True): """ Turn STP protocol on/off. """ if val: state = "on" else: state = "off" Modlib.runshell([LinuxBridge.brctl, "stp", self.name, state]) def set_bridge_prio(self, prio): """ Set bridge priority value. """ Modlib.runshell([LinuxBridge.brctl, "setbridgeprio", self.name, str(prio)]) def set_path_cost(self, port, cost): """ Set port path cost value for STP protocol. """ Modlib.runshell([LinuxBridge.brctl, "setpathcost", self.name, port, str(cost)]) def set_port_prio(self, port, prio): """ Set port priority value. """ Modlib.runshell([LinuxBridge.brctl, "setportprio", self.name, port, str(prio)]) ################################################################################################### class BoundedFloodProxy(socketserver.ThreadingMixIn, socketserver.TCPServer): """ Starts the TCP proxy listener Starts the Ryu engine and module Supports interactions between BF Ryu module and evio controller """ RyuManager = spawn.find_executable("ryu-manager") if RyuManager is None: raise RuntimeError("RyuManager was not found, is it installed?") def __init__(self, host_port_tuple, streamhandler, netman): super().__init__(host_port_tuple, streamhandler) self.netman = netman self._bf_proc = None def start_bf_client_module(self): config = self.netman.config["BoundedFlood"] cmd = [ BoundedFloodProxy.RyuManager, "--user-flags", "modules/BFFlags.py", "--nouse-stderr", "--bf-config-string", json.dumps(config), "modules/BoundedFlood.py"] self._bf_proc = Modlib.create_process(cmd) def server_close(self): if self._bf_proc: self._bf_proc.kill() self._bf_proc.wait() socketserver.TCPServer.server_close(self) class BFRequestHandler(socketserver.BaseRequestHandler): def handle(self): data = self.request.recv(65536) if not data: return task = json.loads(data.decode("utf-8")) task = self.process_task(task) self.request.sendall(bytes(json.dumps(task) + "\n", "utf-8")) def process_task(self, task): # task structure # dict(Request=dict(Action=None, Params=None), # Response=dict(Status=False, Data=None)) if task["Request"]["Action"] == "GetTunnels": task = self._handle_get_topo(task) elif task["Request"]["Action"] == "GetNodeId": task["Response"] = dict(Status=True, Data=dict(NodeId=str(self.server.netman.node_id))) elif task["Request"]["Action"] == "TunnelRquest": task["Response"] = dict(Status=True, Data=dict(StatusMsg="Request shall be considered")) self.server.netman.log("LOG_INFO", "On-demand tunnel request recvd %s", task["Request"]) self.server.netman.tunnel_request(task["Request"]["Params"]) # op is ADD/REMOVE else: self.server.netman.log("LOG_WARNING", "An unrecognized SDNI task was discarded %s", task) task["Response"] = dict(Status=False, Data=dict(ErrorMsg="Unsupported request")) return task def _handle_get_topo(self, task): olid = task["Request"]["Params"]["OverlayId"] topo = self.server.netman.get_ovl_topo(olid) task["Response"] = dict(Status=True, Data=topo) return task ################################################################################################### class VNIC(BridgeABC): brctl = None bridge_type = "VNIC" def __init__(self, ip_addr, prefix_len, mtu, cm): super(VNIC, self).__init__("VirtNic", ip_addr, prefix_len, mtu, cm) def del_br(self): pass def add_port(self, port_name): self.name = port_name net = "{0}/{1}".format(self.ip_addr, self.prefix_len) Modlib.runshell([VNIC.iptool, "addr", "add", net, "dev", self.name]) Modlib.runshell([VNIC.iptool, "link", "set", self.name, "mtu", str(self.mtu)]) Modlib.runshell([VNIC.iptool, "link", "set", "dev", self.name, "up"]) def del_port(self, port_name): pass ################################################################################################### def BridgeFactory(overlay_id, dev_type, config, cm, sdn_config=None): br = None if dev_type == VNIC.bridge_type: br = VNIC(ip_addr=config.get("IP4", None), prefix_len=config.get("PrefixLen", None), mtu=config.get("MTU", 1410), cm=cm) elif dev_type == LinuxBridge.bridge_type: br = LinuxBridge(name=config["NamePrefix"][:8] + overlay_id[:7], ip_addr=config.get("IP4", None), prefix_len=config.get("PrefixLen", None), mtu=config.get("MTU", 1410), cm=cm, stp_enable=(True if config.get("SwitchProtocol", "STP").casefold() == "stp" else False)) elif dev_type == OvsBridge.bridge_type: br = OvsBridge(name=config["NamePrefix"][:8] + overlay_id[:7], ip_addr=config.get("IP4", None), prefix_len=config.get("PrefixLen", None), mtu=config.get("MTU", 1410), cm=cm, stp_enable=(config.get("SwitchProtocol", "").casefold() == "stp"), sdn_ctrl_cfg=sdn_config) return br ################################################################################################### class BridgeController(ControllerModule): def __init__(self, cfx_handle, module_config, module_name): super(BridgeController, self).__init__(cfx_handle, module_config, module_name) self._bfproxy = None self._server_thread = None self._ovl_net = dict() self._appbr = dict() self._lock = threading.Lock() self._tunnels = dict() def initialize(self): ign_br_names = dict() for olid in self.overlays: self._tunnels[olid] = dict() br_cfg = self.overlays[olid] ign_br_names[olid] = set() self._ovl_net[olid] = BridgeFactory(olid, br_cfg["NetDevice"]["Type"], br_cfg["NetDevice"], self, br_cfg.get("SDNController", {})) if "AppBridge" in br_cfg["NetDevice"]: name = self._create_app_bridge(olid, br_cfg["NetDevice"]["AppBridge"]) ign_br_names[olid].add(name) ign_br_names[olid].add(self._ovl_net[olid].name) self.log("LOG_DEBUG", "ignored bridges=%s", ign_br_names) # start the BF proxy if at least one overlay is configured for it if "BoundedFlood" in self.config: proxy_listen_address = self.config["BoundedFlood"]["ProxyListenAddress"] proxy_listen_port = self.config["BoundedFlood"]["ProxyListenPort"] self._bfproxy = BoundedFloodProxy( (proxy_listen_address, proxy_listen_port), BFRequestHandler, self) self._server_thread = threading.Thread(target=self._bfproxy.serve_forever, name="BFProxyServer") self._server_thread.setDaemon(True) self._server_thread.start() # start the BF RYU module self._bfproxy.start_bf_client_module() self.register_cbt("LinkManager", "LNK_ADD_IGN_INF", ign_br_names) #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_INFO", # "OverlayVisualizer module not loaded." # " Visualization data will not be sent.") self._cfx_handle.start_subscription("LinkManager", "LNK_TUNNEL_EVENTS") self.register_cbt("Logger", "LOG_INFO", "Module Loaded") def req_handler_manage_bridge(self, cbt): try: olid = cbt.request.params["OverlayId"] br = self._ovl_net[olid] tnlid = cbt.request.params["TunnelId"] if cbt.request.params["UpdateType"] == "LnkEvConnected": port_name = cbt.request.params["TapName"] self._tunnels[olid][tnlid] = { "PeerId": cbt.request.params["PeerId"], "TunnelId": tnlid, "ConnectedTimestamp": cbt.request.params["ConnectedTimestamp"], "TapName": port_name, "MAC": Modlib.delim_mac_str(cbt.request.params["MAC"]), "PeerMac": Modlib.delim_mac_str(cbt.request.params["PeerMac"]) } br.add_port(port_name) self.log("LOG_INFO", "Port %s added to bridge %s", port_name, str(br)) elif cbt.request.params["UpdateType"] == "LnkEvRemoved": self._tunnels[olid].pop(tnlid, None) if br.bridge_type == OvsBridge.bridge_type: port_name = cbt.request.params.get("TapName") if port_name: br.del_port(port_name) self.log("LOG_INFO", "Port %s removed from bridge %s", port_name, str(br)) except RuntimeError as err: self.register_cbt("Logger", "LOG_WARNING", str(err)) cbt.set_response(None, True) self.complete_cbt(cbt) def timer_method(self): pass def process_cbt(self, cbt): if cbt.op_type == "Request": if cbt.request.action == "LNK_TUNNEL_EVENTS": self.req_handler_manage_bridge(cbt) elif cbt.request.action == "VIS_DATA_REQ": self.req_handler_vis_data(cbt) else: self.req_handler_default(cbt) elif cbt.op_type == "Response": 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 terminate(self): try: if self._bfproxy: self._bfproxy.server_close() self._bfproxy.shutdown() for olid in self._ovl_net: if olid in self._appbr and self.overlays[olid]["NetDevice"]["AppBridge"].get("AutoDelete", False): self._appbr[olid].del_br() br = self._ovl_net[olid] if self.overlays[olid]["NetDevice"].get("AutoDelete", False): br.del_br() else: if br.bridge_type == OvsBridge.bridge_type: for port in br.ports: br.del_port(port) except RuntimeError as err: self.register_cbt("Logger", "LOG_WARNING", str(err)) def req_handler_vis_data(self, cbt): br_data = dict() is_data_available = False for olid in self.overlays: is_data_available = True br_data[olid] = {} br_data[olid]["Type"] = self.overlays[olid]["NetDevice"]["Type"] br_data[olid]["BridgeName"] = self.overlays[olid]["NetDevice"]["NamePrefix"] if "IP4" in self.overlays[olid]["NetDevice"]: br_data[olid]["IP4"] = self.overlays[olid]["NetDevice"]["IP4"] if "PrefixLen" in self.overlays[olid]["NetDevice"]: br_data[olid]["PrefixLen"] = self.overlays[olid]["NetDevice"]["PrefixLen"] if "MTU" in self.overlays[olid]["NetDevice"]: br_data[olid]["MTU"] = self.overlays[olid]["NetDevice"]["MTU"] br_data[olid]["AutoDelete"] = self.overlays[olid]["NetDevice"].get("AutoDelete", False) cbt.set_response({"BridgeController": br_data}, is_data_available) self.complete_cbt(cbt) def _create_app_bridge(self, olid, abr_cfg): name = abr_cfg["NamePrefix"] + olid[:7] gbr = BridgeFactory(olid, abr_cfg["Type"], abr_cfg, self) gbr.add_patch_port(self._ovl_net[olid].get_patch_port_name()) self._ovl_net[olid].add_patch_port(gbr.get_patch_port_name()) self._appbr[olid] = gbr return name def get_ovl_topo(self, overlay_id): return self._tunnels.get(overlay_id, None) def tunnel_request(self, req_params): self.register_cbt("Topology", "TOP_REQUEST_OND_TUNNEL", req_params)