SDN-based RYU application

By muzi ยท Original source

Muzixing

SDN-based RYU application - ARP_PROXY

Foreword

In traditional networks, broadcast traffic exists, consuming a portion of network bandwidth. Furthermore, in loop-based topologies, broadcast data can cause network storms and paralyze the network if certain protocols are not running. The traditional solution is to run STP (Spanning Tree Protocol) to mitigate the storm risks posed by loops. However, with the advent of SDN, solving this problem seems to have become simpler. This article will introduce how to develop an ARP proxy module on the RYU controller to proxy ARP request responses and resolve the problem of loop topology storms.

Algorithm Logic

The specific algorithm flowchart is as follows:

packet_in
    |
    |
  ARP learning
  MAC_to_Port learning
    |
    |
    |               No  
Multicast? -------------------------------------------->|
    |                                                   |
    | Yes                                               |
    |                                                   |
    |                                                   |
    |      No                                           |
   loop? ----->(dpid,eth_src,dst_ip)learning            |
    |                   |                               |
    |                   |                               |
    |                   |               No              |         No
    |Yes        dst_ip in arp_table? ------->dst in mac_to_port? ---->Flood
    |                   |                               |               |
    |                   |Yes                            |Yes            |
    |                   |                               |               |
   drop             ARP_REPLY                       flow_mod            |
    |                   |                               |               |
    |                   |                               |               |
    |<------------------|<------------------------------|<--------------|               
    |
    |
    end

Solving the ring storm

Before replying to ARP requests, the network loop problem must be addressed. Our solution is to record the in_port of the first data packet using (dpid, eth_src, arp_dst_ip) as the key, and discard packets returning from the network. This ensures that a broadcast packet within the same switch can only have one entry point, thus preventing loops. In this application, it is assumed that the first data packet initiating communication in the network is an ARP packet.

sw[(datapath.id, eth_src, arp_dst_ip)] = in_port

The code is as follows:

if eth_dst == ETHERNET_MULTICAST and ARP in header_list:
    arp_dst_ip = header_list[ARP].dst_ip
    if (datapath.id, eth_src, arp_dst_ip) in self.sw:  # Break the loop
        if self.sw[(datapath.id, eth_src, arp_dst_ip)] != in_port:
            out = datapath.ofproto_parser.OFPPacketOut(
                datapath=datapath,
                buffer_id=datapath.ofproto.OFP_NO_BUFFER,
                in_port=in_port,
                actions=[], data=None)
            datapath.send_msg(out)
            return True
    else:
        self.sw[(datapath.id, eth_src, arp_dst_ip)] = in_port

ARP reply

After resolving the broadcast storm issue in the loop topology, we also need to leverage the SDN controller's ability to obtain global network information to proxy ARP request responses, thereby reducing the flood of ARP request data in the network. This logic is very simple, essentially the same as the Layer 2 learning principle: it learns the host's ARP records, then queries and responds to those records. The specific code implementation is as follows:

if ARP in header_list:
    hwtype = header_list[ARP].hwtype
    proto = header_list[ARP].proto
    hlen = header_list[ARP].hlen
    plen = header_list[ARP].plen
    opcode = header_list[ARP].opcode

    arp_src_ip = header_list[ARP].src_ip
    arp_dst_ip = header_list[ARP].dst_ip

    actions = []

    if opcode == arp.ARP_REQUEST:
        if arp_dst_ip in self.arp_table:  # arp reply
            actions.append(datapath.ofproto_parser.OFPActionOutput(
                in_port)
            )

            ARP_Reply = packet.Packet()
            ARP_Reply.add_protocol(ethernet.ethernet(
                ethertype=header_list[ETHERNET].ethertype,
                dst=eth_src,
                src=self.arp_table[arp_dst_ip]))
            ARP_Reply.add_protocol(arp.arp(
                opcode=arp.ARP_REPLY,
                src_mac=self.arp_table[arp_dst_ip],
                src_ip=arp_dst_ip,
                dst_mac=eth_src,
                dst_ip=arp_src_ip))

            ARP_Reply.serialize()

            out = datapath.ofproto_parser.OFPPacketOut(
                datapath=datapath,
                buffer_id=datapath.ofproto.OFP_NO_BUFFER,
                in_port=datapath.ofproto.OFPP_CONTROLLER,
                actions=actions, data=ARP_Reply.data)
            datapath.send_msg(out)
            return True
return False

Postscript

In ring topologies, STP (Standard Protocol Layer) is commonly used to resolve broadcast storms, while SDN (Software-Defined Networking) offers a more efficient solution. Furthermore, after storm resolution, ARP proxy applications can be implemented. However, this simple application doesn't adequately address ARP issues because ARP records have a limited lifespan. Outdated data can disrupt network operations. Therefore, further optimization involves setting the refresh time for ARP records, as well as the refresh time for sw{dpid, eth_src, arp_dst_ip}, to ensure data validity.

Extending this approach, we can process other broadcast data, such as DHCP, using the same pattern. The proxying of more function data packets or signaling data packets can also be implemented following this workflow. The complete code implementation is provided at the end of the article.

# Author:muzixing
# Time:2014/10/19
#

from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
from ryu.lib.packet import arp

ETHERNET = ethernet.ethernet.__name__
ETHERNET_MULTICAST = "ff:ff:ff:ff:ff:ff"
ARP = arp.arp.__name__

class ARP_PROXY_13(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]

    def __init__(self, *args, **kwargs):
        super(ARP_PROXY_13, self).__init__(*args, **kwargs)
        self.mac_to_port = {}
        self.arp_table = {}
        self.sw = {}

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        datapath = ev.msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        # install table-miss flow entry
        #
        # We specify NO BUFFER to max_len of the output action due to
        # OVS bug. At this moment, if we specify a lesser number, e.g.,
        # 128, OVS will send Packet-In with invalid buffer_id and
        # truncated packet data. In that case, we cannot output packets
        # correctly.

        match = parser.OFPMatch()
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                          ofproto.OFPCML_NO_BUFFER)]
        self.add_flow(datapath, 0, match, actions)

    def add_flow(self, datapath, priority, match, actions):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
                                             actions)]

        mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
                                idle_timeout=5, hard_timeout=15,
                                match=match, instructions=inst)
        datapath.send_msg(mod)

    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        in_port = msg.match['in_port']

        pkt = packet.Packet(msg.data)

        eth = pkt.get_protocols(ethernet.ethernet)[0]
        dst = eth.dst
        src = eth.src
        dpid = datapath.id

        header_list = dict(
            (p.protocol_name, p)for p in pkt.protocols if type(p) != str)
        if ARP in header_list:
            self.arp_table[header_list[ARP].src_ip] = src  # ARP learning

        self.mac_to_port.setdefault(dpid, {})
        self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)

        # learn a mac address to avoid FLOOD next time.
        self.mac_to_port[dpid][src] = in_port

        if dst in self.mac_to_port[dpid]:
            out_port = self.mac_to_port[dpid][dst]
        else:
            if self.arp_handler(header_list, datapath, in_port, msg.buffer_id):
                # 1:reply or drop;  0: flood
                print "ARP_PROXY_13"
                return None
            else:
                out_port = ofproto.OFPP_FLOOD
                print 'OFPP_FLOOD'

        actions = [parser.OFPActionOutput(out_port)]

        # install a flow to avoid packet_in next time
        if out_port != ofproto.OFPP_FLOOD:
            match = parser.OFPMatch(in_port=in_port, eth_dst=dst)
            self.add_flow(datapath, 1, match, actions)

        data = None
        if msg.buffer_id == ofproto.OFP_NO_BUFFER:
            data = msg.data
        out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
                                  in_port=in_port, actions=actions, data=data)
        datapath.send_msg(out)

    def arp_handler(self, header_list, datapath, in_port, msg_buffer_id):
        header_list = header_list
        datapath = datapath
        in_port = in_port

        if ETHERNET in header_list:
            eth_dst = header_list[ETHERNET].dst
            eth_src = header_list[ETHERNET].src

        if eth_dst == ETHERNET_MULTICAST and ARP in header_list:
            arp_dst_ip = header_list[ARP].dst_ip
            if (datapath.id, eth_src, arp_dst_ip) in self.sw:  # Break the loop
                if self.sw[(datapath.id, eth_src, arp_dst_ip)] != in_port:
                    out = datapath.ofproto_parser.OFPPacketOut(
                        datapath=datapath,
                        buffer_id=datapath.ofproto.OFP_NO_BUFFER,
                        in_port=in_port,
                        actions=[], data=None)
                    datapath.send_msg(out)
                    return True
            else:
                self.sw[(datapath.id, eth_src, arp_dst_ip)] = in_port

        if ARP in header_list:
            hwtype = header_list[ARP].hwtype
            proto = header_list[ARP].proto
            hlen = header_list[ARP].hlen
            plen = header_list[ARP].plen
            opcode = header_list[ARP].opcode

            arp_src_ip = header_list[ARP].src_ip
            arp_dst_ip = header_list[ARP].dst_ip

            actions = []

            if opcode == arp.ARP_REQUEST:
                if arp_dst_ip in self.arp_table:  # arp reply
                    actions.append(datapath.ofproto_parser.OFPActionOutput(
                        in_port)
                    )

                    ARP_Reply = packet.Packet()
                    ARP_Reply.add_protocol(ethernet.ethernet(
                        ethertype=header_list[ETHERNET].ethertype,
                        dst=eth_src,
                        src=self.arp_table[arp_dst_ip]))
                    ARP_Reply.add_protocol(arp.arp(
                        opcode=arp.ARP_REPLY,
                        src_mac=self.arp_table[arp_dst_ip],
                        src_ip=arp_dst_ip,
                        dst_mac=eth_src,
                        dst_ip=arp_src_ip))

                    ARP_Reply.serialize()

                    out = datapath.ofproto_parser.OFPPacketOut(
                        datapath=datapath,
                        buffer_id=datapath.ofproto.OFP_NO_BUFFER,
                        in_port=datapath.ofproto.OFPP_CONTROLLER,
                        actions=actions, data=ARP_Reply.data)
                    datapath.send_msg(out)
                    return True
        return False