#!/usr/bin/env python3 # Ryu OpenFlow controller that connects to the Zeek OpenFlow # framework using Broker. # # Start with ./ryu/bin/ryu-manager controller.py import datetime import ipaddress import logging import time import re import ryu.app.ofctl.api from netaddr import IPNetwork from ryu.base import app_manager from ryu.controller import dpset from ryu.controller import controller from ryu.controller import ofp_event from ryu.controller.handler import MAIN_DISPATCHER from ryu.controller.handler import CONFIG_DISPATCHER from ryu.controller.handler import set_ev_cls from ryu.ofproto import ofproto_v1_0 from ryu.ofproto import ofproto_v1_2 from ryu.ofproto import ofproto_v1_3 from ryu.lib import ofctl_v1_0 from ryu.lib import ofctl_v1_2 from ryu.lib import ofctl_v1_3 from ryu.lib import hub import broker from select import select supported_ofctl = { ofproto_v1_0.OFP_VERSION: ofctl_v1_0, ofproto_v1_2.OFP_VERSION: ofctl_v1_2, ofproto_v1_3.OFP_VERSION: ofctl_v1_3, } queuename = "zeek/openflow" # for monkey-patching. # Barf. def zeek_send_msg(self, msg): assert isinstance(msg, self.ofproto_parser.MsgBase) if not hasattr(self, 'zeeksend'): self.zeeksend = 0 self.zeekmessage = None # if we set before that we just want the message returned # without sending it on - return it to us so we can use # it further... if ( self.zeeksend == 1 ): self.zeeksend = 0 self.zeekmessage = msg return msg self.send_msg_orig(msg) # sorry about all that. This is just to get the API somewhere where we actually can work # with it. # :/ ryu.controller.controller.Datapath.send_msg_orig = ryu.controller.controller.Datapath.send_msg ryu.controller.controller.Datapath.send_msg = zeek_send_msg class ZeekController(app_manager.RyuApp): OFP_VERSIONS = [ofproto_v1_0.OFP_VERSION, ofproto_v1_2.OFP_VERSION, ofproto_v1_3.OFP_VERSION] _CONTEXTS = {'dpset': dpset.DPSet} def __init__(self, *args, **kwargs): super(ZeekController, self).__init__(*args, **kwargs); self.dpset = kwargs['dpset'] self.data = {} self.data['dpset'] = self.dpset # store DPID->name mapping. We create the mapping implicitely # for all flow_clear and flow_mod events that we get and use # it for the returning flow_removed events self.dpids = {} self.epl = broker.Endpoint() def start(self): self.epl.listen("127.0.0.1", 9999) self.status_subscriber = self.epl.make_status_subscriber(True) self.subscriber = self.epl.make_subscriber(queuename) self.threads.append(hub.spawn(self._broker_loop)) self.logger.info("Started broker communication...") self.threads.append(hub.spawn(self._event_loop)) @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER) def _switch_features_handler(self, ev): dp = ev.msg.datapath # here we could alarm that we have seen a new switch def _broker_loop(self): self.logger.info("Broker loop...") while 1==1: self.logger.info("Waiting for broker message") readable, writable, exceptional = select( [self.status_subscriber.fd(), self.subscriber.fd()], [],[]) if ( self.status_subscriber.fd() in readable ): self.logger.info("Got broker status message") msg = self.status_subscriber.get() self.handle_broker_message(msg) elif ( self.subscriber.fd() in readable ): self.logger.info("Got broker message") msg = self.subscriber.get() self.handle_broker_message(msg) def handle_broker_message(self, m): if isinstance(m, broker.Status): if m.code() == broker.SC.PeerAdded: self.logger.info("Incoming connection established.") return return if ( type(m).__name__ != "tuple" ): self.logger.error("Unexpected type %s, expected tuple", type(m).__name__) return if ( len(m) < 1 ): self.logger.error("Tuple without content?") return (topic, event) = m ev = broker.zeek.Event(event) event_name = ev.name() if ( event_name == "OpenFlow::broker_flow_clear" ): self.event_flow_clear(ev.args()) elif ( event_name == "OpenFlow::broker_flow_mod" ): self.event_flow_mod(ev.args()) elif event_name == "OpenFlow::flow_mod_success": pass elif event_name == "OpenFlow::flow_mod_failure": pass elif event_name == "OpenFlow::flow_removed": pass else: self.logger.error("Unknown event %s", event_name) return def event_flow_clear(self, m): if ( len(m) != 2 ) or ( not isinstance(m[0], str) ) or ( not isinstance(m[1], broker.Count) ): self.logger.error("wrong number of elements or type in tuple for event_flow_clear") return # since this is only a convenience function we should return it and just do the # flow-mod from Zeek ourselves name = m[0] dpid = m[1].value self.logger.info("flow_clear for %s %d", name, dpid) dp = ryu.app.ofctl.api.get_datapath(self, int(dpid)) if dp is None: self.logger.error("dpid %d not found for clear", dpid) return self.dpids[dp.id] = name flow = {'table_id': dp.ofproto.OFPTT_ALL} _ofp_version = dp.ofproto.OFP_VERSION _ofctl = supported_ofctl.get(_ofp_version, None) if _ofctl is None: self.logger.error("unsupported openflow protocol") return dp.brosend = 1 # give it to us _ofctl.mod_flow_entry(dp, flow, dp.ofproto.OFPFC_DELETE) msg = dp.bromessage ryu.app.ofctl.api.send_msg(self, msg) def send_error(self, name, match, flow_mod, msg): ev = broker.zeek.Event("OpenFlow::flow_mod_failure", name, match, flow_mod, msg) self.epl.publish(queuename, ev) def send_success(self, name, match, flow_mod, msg): ev = broker.zeek.Event("OpenFlow::flow_mod_success", name, match, flow_mod, msg) self.epl.publish(queuename, ev) def event_flow_mod(self, m): if ( len(m) != 4 ) or ( not isinstance(m[0], str) ) or ( not isinstance(m[1], broker.Count) ) or ( not isinstance(m[2], tuple) ) or ( not isinstance(m[3], tuple) ): self.logger.error("wrong number of elements or type in tuple for event_flow_mod") return name = m[0] dpid = m[1].value match = self.parse_ofp_match(m[2]) flow_mod = self.parse_ofp_flow_mod(m[3]) dp = ryu.app.ofctl.api.get_datapath(self, int(dpid)) if dp is None: self.logger.error("name %s dpid %d not found for flow_mod", name, dpid) self.send_error(name, m[2], m[3], "dpid not found") return self.dpids[dp.id] = name if dp.ofproto.OFP_VERSION != ofproto_v1_0.OFP_VERSION: if 'nw_dst' in match: if ":" in match['nw_dst']: match['ipv6_dst'] = match['nw_dst'] else: match['ipv4_dst'] = match['nw_dst'] del match['nw_dst'] if 'nw_src' in match: if ":" in match['nw_src']: match['ipv6_src'] = match['nw_src'] else: match['ipv4_src'] = match['nw_src'] del match['nw_src'] if 'tp_src' in match: proto = match.get('nw_proto', None); if proto == None: self.logger.error("Cannot determine proto for flow mod") return if proto == 0x06: match['tcp_src'] = match['tp_src'] del match['tp_src'] elif proto == 0x11: match['udp_src'] = match['tp_src'] del match['tp_src'] elif proto == 0x01: match['icmpv4_type'] = match['tp_src'] del match['tp_src'] if 'tp_dst' in match: proto = match.get('nw_proto', None); if proto == None: self.logger.error("Cannot determine proto for flow mod") return if proto == 0x06: match['tcp_dst'] = match['tp_dst'] del match['tp_dst'] elif proto == 0x11: match['udp_dst'] = match['tp_dst'] del match['tp_dst'] elif proto == 0x01: match['icmpv4_code'] = match['tp_dst'] del match['tp_dst'] self.logger.info("flow_mod for %d", dpid) #print match #print flow_mod cmdstr = flow_mod['command'] cmd = self.string_to_command(dp, cmdstr) flow_mod['match'] = match #flow_mod['flags'] = 1 # remove, we actually want overlapping entries sometimes. actions = [] if dp.ofproto.OFP_VERSION == ofproto_v1_0.OFP_VERSION: for k, v in flow_mod['actions'].iteritems(): if k == 'vlan_vid': actions.append(dp.ofproto_parser.OFPActionVlanVid(v)) elif k == 'vlan_pcp': actions.append(dp.ofproto_parser.OFPActionVlanPcp(k)) elif k == 'vlan_strip' and v == True: actions.append(dp.ofproto_parser.OFPActionStripVlan()) elif k == 'dl_src': dl_src = haddr_to_bin(v) actions.append(dp.ofproto_parser.OFPActionSetDlSrc(dl_src)) elif k == 'dl_dst': dl_dst = haddr_to_bin(v) actions.append(dp.ofproto_parser.OFPActionSetDlDst(dl_dst)) elif k == 'nw_tos': actions.append(dp.ofproto_parser.OFPActionSetNwTos(v)) elif k == 'nw_src': actions.append(dp.ofproto_parser.OFPActionSetNwSrc(ipv4_to_int(v))) elif k == 'nw_dst': actions.append(dp.ofproto_parser.OFPActionSetNwDst(ipv4_to_int(v))) elif k == 'tp_src': actions.append(dp.ofproto_parser.OFPActionSetTpSrc(v)) elif k == 'tp_dst': actions.append(dp.ofproto_parser.OFPActionSetTpDst(v)) else: if 'nw_dst' in flow_mod['actions']: if ":" in flow_mod['actions']['nw_dst']: flow_mod['actions']['ipv6_dst'] = flow_mod['actions']['nw_dst'] else: flow_mod['actions']['ipv4_dst'] = flow_mod['actions']['nw_dst'] del flow_mod['actions']['nw_dst'] if 'nw_src' in flow_mod['actions']: if ":" in flow_mod['actions']['nw_src']: flow_mod['actions']['ipv6_src'] = flow_mod['actions']['nw_src'] else: flow_mod['actions']['ipv4_src'] = flow_mod['actions']['nw_src'] del flow_mod['actions']['nw_src'] if 'tp_src' in flow_mod['actions']: proto = match.get('nw_proto', None); if proto == None: self.logger.error("Cannot determine proto for flow mod") return if proto == 0x06: flow_mod['actions']['tcp_src'] = flow_mod['actions']['tp_src'] elif proto == 0x11: flow_mod['actions']['udp_src'] = flow_mod['actions']['tp_src'] elif proto == 0x01: flow_mod['actions']['icmpv4_type'] = flow_mod['actions']['tp_src'] if 'tp_dst' in flow_mod['actions']: proto = match.get('nw_proto', None); if proto == None: self.logger.error("Cannot determine proto for flow mod") return if proto == 0x06: flow_mod['actions']['tcp_dst'] = flow_mod['actions']['tp_dst'] elif proto == 0x11: flow_mod['actions']['udp_dst'] = flow_mod['actions']['tp_dst'] elif proto == 0x01: flow_mod['actions']['icmpv4_code'] = flow_mod['actions']['tp_dst'] for k, v in flow_mod['actions'].iteritems(): if k == 'vlan_strip' and v == True: actions.append(dp.ofproto_parser.OFPActionStripVlan()) elif ( k == 'vlan_vid' ) or ( k == 'vlan_pcp' ) or ( k == 'nw_tos' ) or ( k == 'ipv4_src' ) or ( k == 'ipv4_dst' ) or ( k == 'tcp_src' ) or ( k == 'tcp_dst' ) or ( k == 'udp_src' ) or ( k == 'udp_dst' ) or ( k == 'icmpv4_code' ) or ( k == 'icmpv4_type' ) or ( k == 'ipv6_src' ) or ( k == 'ipv6_dst' ): pass actions.append(dp.ofproto_parser.OFPActionSetField(**{k: v})) elif ( k == 'dl_src' ) or ( k == 'dl_dst' ): #dl = haddr_to_bin(v) actions.append(dp.ofproto_parser.OFPActionSetField(**{k: v})) # do out-ports separately because it has to be last... if 'out_ports' in flow_mod['actions']: for i in flow_mod['actions']['out_ports']: max_len = 0xffe5 if dp.ofproto.OFP_VERSION != ofproto_v1_0.OFP_VERSION: max_len = dp.ofproto.OFPCML_MAX if dp.ofproto.OFP_VERSION == ofproto_v1_0.OFP_VERSION: if i == 0xfffffff8: i = dp.ofproto.OFPP_IN_PORT elif i == 0xfffffff9: i = dp.ofproto.OFPP_TABLE elif i == 0xfffffffa: i = dp.ofproto.OFPP_NORMAL elif i == 0xfffffffb: i = dp.ofproto.OFPP_FLOOD elif i == 0xfffffffc: i = dp.ofproto.OFPP_ALL elif i == 0xfffffffd: i = dp.ofproto.OFPP_CONTROLLER elif i == 0xfffffffe: i = dp.ofproto.OFPP_LOCAL elif i == 0xffffffff: i = dp.ofproto.OFPP_ANY actions.append(dp.ofproto_parser.OFPActionOutput(i, max_len)) del flow_mod['actions'] if cmd is None: self.logger.error("command %s could not be parsed", cmdstr) self.send_error(m[2], m[3], "cmd not recognized") return _ofp_version = dp.ofproto.OFP_VERSION _ofctl = supported_ofctl.get(_ofp_version, None) if _ofctl is None: self.logger.error("unsupported openflow protocol") self.send_error(m[2], m[3], "unsupported openflow protocol") return dp.brosend = 1 # give it to us... _ofctl.mod_flow_entry(dp, flow_mod, cmd) msg = dp.bromessage # naming and calling changed in the api for of1.0 vs 1.3 insts = [] if dp.ofproto.OFP_VERSION == ofproto_v1_0.OFP_VERSION: msg.actions = actions else: insts.append(dp.ofproto_parser.OFPInstructionActions(dp.ofproto.OFPIT_APPLY_ACTIONS, actions)) msg.instructions = insts #print "Sending to switch:" #print msg try: ryu.app.ofctl.api.send_msg(self, msg) self.send_success(name, m[2], m[3], "") except ryu.app.ofctl.exception.OFError as err: self.logger.error("flow_mod execution error %s", err) self.send_error(name, m[2], m[3], str(err)) def parse_ofp_match(self, m): match = ['in_port', 'dl_src', 'dl_dst', 'dl_vlan', 'dl_vlan_pcp', 'dl_type', 'nw_tos', 'nw_proto', 'nw_src', 'nw_dst', 'tp_src', 'tp_dst'] return self.record_to_record(match, m) def parse_ofp_flow_mod(self, m): match = ['cookie', 'table_id', 'command', 'idle_timeout', 'hard_timeout', 'priority', 'out_port', 'out_group', 'flags'] rec = self.record_to_record(match, m) # ok, now we have to get the actions, which are after flags. This is kind of cheating, but... whatever :) match_actions = ['out_ports', 'vlan_vid', 'vlan_pcp', 'vlan_strip', 'dl_src', 'dl_dst', 'nw_tos', 'nw_src', 'nw_dst', 'tp_src', 'tp_dst'] rm = m rl = rm[9] recaction = self.record_to_record(match_actions, rl) rec['actions'] = recaction return rec @set_ev_cls(ofp_event.EventOFPFlowRemoved, MAIN_DISPATCHER) def _flow_removed_handler(self, ev): msg = ev.msg dp = msg.datapath ofp = dp.ofproto match = msg.match if dp.id not in self.dpids: self.logger.error("Flow remove for unknown DPID %d", dp.id) return #print "Flow removed" match_vec = vector_of_field([]) match_vec = self.vec_add_field(match_vec, match, 'in_port'); match_vec = self.vec_add_field(match_vec, match, 'eth_src'); match_vec = self.vec_add_field(match_vec, match, 'eth_dst'); match_vec = self.vec_add_field(match_vec, match, 'vlan_vid'); match_vec = self.vec_add_field(match_vec, match, 'vlan_pcp'); match_vec = self.vec_add_field(match_vec, match, 'eth_type'); match_vec = self.vec_add_field(match_vec, match, 'ip_dscp'); match_vec = self.vec_add_field(match_vec, match, 'ip_proto'); src = match.get('ipv4_src', match.get('ipv6_src', match.get('nw_src', None))) dst = match.get('ipv4_dst', match.get('ipv6_dst', match.get('nw_dst', None))) if src != None: sn = None if ( not isinstance(src, tuple) ) and( ":" in src ): sn = subnet(address.from_string(src), 128) elif not isinstance(src, tuple): sn = subnet(address.from_string(src), 32) else: adr = address.from_string(src[0]) ip = IPNetwork(src[0]+"/"+src[1]) sn = subnet(adr, ip.prefixlen) match_vec.push_back(field(data(sn))) else: match_vec.push_back(field()) if dst != None: sn = None if ( not isinstance(dst, tuple) ) and( ":" in dst ): sn = subnet(address.from_string(dst), 128) elif not isinstance(dst, tuple): sn = subnet(address.from_string(dst), 32) else: adr = address.from_string(dst[0]) ip = IPNetwork(dst[0]+"/"+dst[1]) sn = subnet(adr, ip.prefixlen) match_vec.push_back(field(data(sn))) else: match_vec.push_back(field()) srcp = match.get('tcp_src', match.get('udp_src', match.get('icmpv4_type', match.get('tp_src', None)))) dstp = match.get('tcp_dst', match.get('udp_dst', match.get('icmpv4_type', match.get('tp_dst', None)))) if srcp != None: match_vec.push_back(field(data(srcp))) else: match_vec.push_back(field()) if dstp != None: match_vec.push_back(field(data(dstp))) else: match_vec.push_back(field()) args = [self.dpids[dp.id], match_vec, broker.Count(msg.cookie), broker.Count(msg.priority), broker.Count(msg.reason), broker.Count(msg.duration_sec), broker.Count(msg.idle_timeout), broker.Count(msg.packet_count), broker.Count(msg.byte_count)] ev = broker.zeek.Event("OpenFlow::flow_removed", args) self.epl.publish(queuename, ev) def vec_add_field(self, match_vec, match , el): if el in match: match_vec.push_back(field(data(match[el]))) else: match_vec.push_back(field()) return match_vec def string_to_command(self, dp, cmdstr): if ( cmdstr == "OFPFC_ADD" ): return dp.ofproto.OFPFC_ADD elif ( cmdstr == "OFPFC_MODIFY" ): return dp.ofproto.OFPFC_MODIFY elif ( cmdstr == "OFPFC_MODIFY_STRICT" ): return dp.ofproto.OFPFC_MODIFY_STRICT elif ( cmdstr == "OFPFC_DELETE" ): return dp.ofproto.OFPFC_DELETE elif ( cmdstr == "OFPFC_DELETE_STRICT" ): return dp.ofproto.OFPFC_DELETE_STRICT else: return None def record_to_record(self, match, m): #if len(match) != len(m): # self.logger.error("wrong number of elements in parse_ofp_match") # return if not isinstance(m, tuple): self.logger.error("Got non record element") rec = m dict = {} for i in range(0, len(match)): if rec[i] is None: #dict[match[i]] = None # most of the functions expect this to be undefined, not none. We oblige. continue dict[match[i]] = self.convert_element(rec[i]) return dict def convert_element(self, el): if isinstance(el, broker.Count): return el.value if isinstance(el, ipaddress.IPv4Address): return str(el); if isinstance(el, ipaddress.IPv6Address): return str(el); if isinstance(el, ipaddress.IPv4Network): return str(el); if isinstance(el, ipaddress.IPv6Network): return str(el); if isinstance(el, broker.Port): p = str(el) ex = re.compile('([0-9]+)(.*)') res = ex.match(p) return (res.group(1), res.group(2)) if isinstance(el, broker.Enum): tmp = el.name return re.sub(r'.*::', r'', tmp) if isinstance(el, tuple): return tuple(self.convert_element(ell) for ell in el); if isinstance(el, datetime.datetime): return el if isinstance(el, datetime.timedelta): return el if isinstance(el, int): return el if isinstance(el, str): return el logger.error("Unsupported type %s", type(el) ) return el;