Source code for csle_ryu.controllers.learning_switch_controller

from ryu.controller import ofp_event
from ryu.controller.handler import MAIN_DISPATCHER, CONFIG_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
from ryu.lib.packet import ether_types
from csle_ryu.dao.ryu_controller_type import RYUControllerType
from csle_ryu.monitor.flow_and_port_stats_monitor import FlowAndPortStatsMonitor
import csle_ryu.constants.constants as constants


[docs]class LearningSwitchController(FlowAndPortStatsMonitor): """ RYU Controller implementing a learning L2 switch for OpenFlow 1.3 """ def __init__(self, *args, **kwargs): """ Initializes the switch :param args: RYU arguments :param kwargs: RYU arguments """ super(LearningSwitchController, self).__init__(*args, **kwargs) # MAC-to-PORT table to be learned self.mac_to_port = {} # Controller type self.controller_type = RYUControllerType.LEARNING_SWITCH
[docs] @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER) def switch_features_handler(self, ev): """ Handler called after OpenFlow handshake with switch completed. It adds teh Table-miss flow entry to the flow tables of the switch so that next packet which yield a flow-table-miss will be sent to the controller :param ev: the handshake complete event :return: """ # the datapath, i.e., abstraction of the link to the switch datapath = ev.msg.datapath self.logger.info(f"[SDN-Controller {self.controller_type}] OpenFlow handshake with switch DPID:{datapath.id}, " f"completed, adding the default flow-miss entry to the table") # Extract version and packet parser openflow_protocol = datapath.ofproto parser = datapath.ofproto_parser # Configure the table-miss flow to install at the table # First, setup the matcher that will match packets to flows based on the headers # It should match any packet match = parser.OFPMatch() # Second, define action when flows are matched actions = [parser.OFPActionOutput(openflow_protocol.OFPP_CONTROLLER, constants.RYU.PACKET_BUFFER_MAX_LEN)] # Third, define the priority, we set the lowest priority so that this flow rule is matched only if no other # flow is matched priority = 0 # Send request to the switch to add the flow self.add_flow(datapath, priority, match, actions)
[docs] def add_flow(self, datapath, priority, match, actions, buffer_id=None) -> None: """ Utility method for adding a flow to a switch :param datapath: the datapath, i.e., abstraction of the link to the switch :param priority: the priority of the flow (higher priority are prioritized) :param match: the pattern for matching packets to the flow based on the header :param actions: the actions to take when the flow is matched, e.g., where to send the packet :param buffer_id: the id of the buffer where packets for this flow are queued if they cannot be sent :return: None """ openflow_protocol = datapath.ofproto # Extract the openflow protocol used for communicating with the switch parser = datapath.ofproto_parser # extract packet parser # Define the instruction that the switch should perform if the flow is matched instruction = [parser.OFPInstructionActions(openflow_protocol.OFPIT_APPLY_ACTIONS, actions)] # Create an OpenFlow Modify-State message to add a new flow if buffer_id is not None: mod = parser.OFPFlowMod(datapath=datapath, buffer_id=buffer_id, priority=priority, match=match, instructions=instruction) else: mod = parser.OFPFlowMod(datapath=datapath, priority=priority, match=match, instructions=instruction) # Send the message to the switch datapath.send_msg(mod)
@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER) def _packet_in_handler(self, ev) -> None: """ Handler called when the switch has received a packet with an unknown destination and thus forwards the packet to the controller :param ev: the packet-in-event :return: None """ # Extract metadata from the event received from the switch msg = ev.msg datapath = msg.datapath # message protocol and parser and input port openflow_protocol = datapath.ofproto parser = datapath.ofproto_parser in_port = msg.match['in_port'] pkt = packet.Packet(msg.data) eth = pkt.get_protocols(ethernet.ethernet)[0] if eth.ethertype == ether_types.ETH_TYPE_LLDP: # ignore link-layer discovery protocol (lldp) packet return dst_mac_address = eth.dst src_mac_address = eth.src # Each switch is identified by a datapath 64-bit ID (DPID) containing the 48-bit MAC address # as well as 16-bit additional ID bits datapath_id = format(datapath.id, "d").zfill(16) # Initialize empty row in the mac-to-port table self.mac_to_port.setdefault(datapath_id, {}) self.logger.debug(f"[SDN-Controller {self.controller_type}] received packet in, DPID:{datapath_id}, " f"src_mac_address:{src_mac_address}, dst_mac_address:{dst_mac_address}, " f"port number:{in_port}") # learn a mac address to avoid FLOOD next time, i.e., map the port of the switch to the source MAC address self.mac_to_port[datapath_id][src_mac_address] = in_port if dst_mac_address in self.mac_to_port[datapath_id]: # If the destination MAC address already exists in the table, lookup the corresponding port out_port = self.mac_to_port[datapath_id][dst_mac_address] else: # If the destination MAC address does not exist in the table, tell the switch to send a flood message out_port = openflow_protocol.OFPP_FLOOD # Prepare the actions to be sent to the switch actions = [parser.OFPActionOutput(out_port)] # If the destination MAC already has been learned, install a flow in the switch that tells the switch # which outgoing port it should use whenever it receives frames that matches the given source port, source MAC, # and destination MAC. This defines a flow. This means that next time a frame of this flow is identified, # the switch does not need to flood the LAN, it can just lookup the correct destination port from the # flow table. if out_port != openflow_protocol.OFPP_FLOOD: # Create a matcher object which defines how packets are matched to the flow match = parser.OFPMatch(in_port=in_port, eth_dst=dst_mac_address, eth_src=src_mac_address) # verify if we have a valid buffer_id, if yes then we dont have to assign a new buffer id and # we dont have to send back the packet data since it is already queued in the buffer if msg.buffer_id != openflow_protocol.OFP_NO_BUFFER: self.add_flow(datapath, 1, match, actions, msg.buffer_id) return else: self.add_flow(datapath, 1, match, actions) # The switch normally keeps packet it does not know how to forward in a queue/buffer. We extract the # buffer id from the openflow event. However if the switch does not use buffering, it will send the complete # data message to the controller. In this case, we need to send the data back to the switch, otherwise # it will be lost. data = None if msg.buffer_id == openflow_protocol.OFP_NO_BUFFER: data = msg.data # Prepare OFP message to send to the switch and instruct it what to do with this packet. out = datapath.ofproto_parser.OFPPacketOut( datapath=datapath, buffer_id=msg.buffer_id, in_port=in_port, actions=actions, data=data) # Send the message to the Switch datapath.send_msg(out)