1 # Copyright (C) 2014 SDN Hub
3 # Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3.
4 # You may not use this file except in compliance with this License.
5 # You may obtain a copy of the License at
7 # http://www.gnu.org/licenses/gpl-3.0.txt
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
17 from ryu.base import app_manager
18 from ryu.controller import ofp_event
19 from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER, DEAD_DISPATCHER
20 from ryu.controller.handler import set_ev_cls
21 from ryu.ofproto import ofproto_v1_3
22 from ryu.lib import ofctl_v1_3
23 from ryu.ofproto import ether, inet
24 from ryu.lib.packet import packet
25 from ryu.lib.packet import ethernet
26 from ryu.lib.packet import ipv4
27 from ryu.lib.packet import tcp
28 from ryu.lib.packet import arp
30 DEFAULT_IDLE_TIMEOUT = 60
31 DEFAULT_HARD_TIMEOUT = 300
33 LOG = logging.getLogger('ryu.app.sdnhub_apps.learning_switch')
35 class L2LearningSwitch(app_manager.RyuApp):
36 OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
38 def __init__(self, *args, **kwargs):
39 super(L2LearningSwitch, self).__init__(*args, **kwargs)
42 self.switch_flows = {}
44 def get_switch_flows(self):
45 return self.switch_flows
47 def get_switch_flows(self, dpid):
48 return self.switch_flows[dpid]
50 def add_exemption(self, match=None):
52 self.exemption.append(match)
54 def clear_exemption(self):
57 def get_attachment_port(self, dpid, mac):
58 if dpid in self.mac_to_port:
59 table = self.mac_to_port[dpid]
65 def is_packet_exempted(self, pkt):
67 eth = pkt.get_protocols(ethernet.ethernet)[0]
69 fields['dl_src'] = eth.src
70 fields['dl_dst'] = eth.dst
71 fields['dl_type'] = eth.ethertype
73 if eth.ethertype == ether.ETH_TYPE_ARP:
74 arp_hdr = pkt.get_protocols(arp.arp)[0]
75 fields['nw_src'] = arp_hdr.src_ip
76 fields['nw_dst'] = arp_hdr.dst_ip
78 elif eth.ethertype == ether.ETH_TYPE_IP:
79 ip_hdr = pkt.get_protocols(ipv4.ipv4)[0]
80 fields['nw_src'] = ip_hdr.src
81 fields['nw_dst'] = ip_hdr.dst
82 fields['nw_proto'] = ip_hdr.proto
84 if ip_hdr.proto == inet.IPPROTO_TCP:
85 tcp_hdr = pkt.get_protocols(tcp.tcp)[0]
86 fields['tp_src'] = tcp_hdr.src_port
87 fields['tp_dst'] = tcp_hdr.dst_port
88 elif ip_hdr.proto == inet.IPPROTO_UDP:
89 tcp_hdr = pkt.get_protocols(tcp.tcp)[0]
90 fields['tp_src'] = tcp_hdr.src_port
91 fields['tp_dst'] = tcp_hdr.dst_port
93 for match in self.exemption:
94 # the match specified for exemption should be a
95 # superset of the flows to exclude processing.
98 for key,val in match.iteritems():
102 elif val != fields[key]:
106 # This exemption rule matched
111 def add_flow(self, datapath, priority=ofproto_v1_3.OFP_DEFAULT_PRIORITY, match=None,
112 actions=None,idle_timeout=0, hard_timeout=0, buffer_id=ofproto_v1_3.OFP_NO_BUFFER):
113 ofp = datapath.ofproto
114 ofp_parser = datapath.ofproto_parser
117 inst = [ofp_parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions)]
121 cookie = random.randint(0, 0xffffffffffffffff)
123 mod = ofp_parser.OFPFlowMod(datapath=datapath, priority=priority,
124 buffer_id=buffer_id,cookie=cookie,
125 match=match, idle_timeout=idle_timeout,
126 hard_timeout=hard_timeout, instructions=inst,
127 flags=ofp.OFPFF_SEND_FLOW_REM)
129 datapath.send_msg(mod)
131 match_str = ofctl_v1_3.match_to_str(match),
132 self.switch_flows[datapath.id].append({'cookie':cookie,
135 'priority':priority})
137 LOG.debug("Flow inserted to switch %x: cookie=%s, match=%s, actions=%s, priority=%d",
138 datapath.id, str(cookie), match_str, str(actions), priority)
141 @set_ev_cls(ofp_event.EventOFPStateChange,
142 [MAIN_DISPATCHER, DEAD_DISPATCHER])
143 def state_change_handler(self, ev):
144 datapath = ev.datapath
145 assert datapath is not None
147 if ev.state == MAIN_DISPATCHER:
148 ofp = datapath.ofproto
149 ofp_parser = datapath.ofproto_parser
151 self.mac_to_port.setdefault(datapath.id, {})
152 self.switch_flows.setdefault(datapath.id, [])
154 # install table-miss flow entry
155 match = ofp_parser.OFPMatch()
156 actions = [ofp_parser.OFPActionOutput(ofp.OFPP_CONTROLLER, ofp.OFPCML_NO_BUFFER)]
157 self.add_flow(datapath=datapath, priority=0, match=match, actions=actions)
159 elif ev.state == DEAD_DISPATCHER:
160 if datapath.id != None:
161 del self.mac_to_port[datapath.id]
162 del self.switch_flows[datapath.id]
165 @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
166 def packet_in_handler(self, ev):
168 datapath = msg.datapath
169 ofp = datapath.ofproto
170 ofp_parser = datapath.ofproto_parser
171 in_port = msg.match['in_port']
173 pkt = packet.Packet(msg.data)
174 if self.is_packet_exempted(pkt):
177 eth = pkt.get_protocols(ethernet.ethernet)[0]
181 # Skip processing LLDP packets. Leave it to the topology module
182 if eth.ethertype == ether.ETH_TYPE_LLDP:
187 # Learn a mac address to avoid FLOOD next time.
188 self.mac_to_port[dpid][src] = in_port
190 # Following is an optimization to stop troubling the controller
191 # too often. But, it has an effect of preventing the controller
192 # from seeing a few hosts because the ARP reply matches this
193 # rule and goes hidden from controller.
195 # On packet_in for any packet, program a low priority rule for
196 # the destination MAC so that we don't keep troubling controller
197 # actions = [ofp_parser.OFPActionOutput(in_port)]
198 # match = ofp_parser.OFPMatch(eth_dst=src)
199 # self.add_flow(datapath=datapath, priority=1,
200 # match=match, actions=actions)
202 # Learning switch logic below
203 if dst in self.mac_to_port[dpid]:
204 out_port = self.mac_to_port[dpid][dst]
206 out_port = ofp.OFPP_FLOOD
208 actions = [ofp_parser.OFPActionOutput(out_port)]
210 # install a flow to avoid packet_in next time
211 if out_port != ofp.OFPP_FLOOD:
212 match = ofp_parser.OFPMatch(in_port=in_port, eth_dst=dst)
213 self.add_flow(datapath=datapath, priority=2,
214 match=match, actions=actions,
215 idle_timeout=DEFAULT_IDLE_TIMEOUT,
216 hard_timeout=DEFAULT_HARD_TIMEOUT,
217 buffer_id=msg.buffer_id)
219 # Are we done, or do we need to forward this packet?
220 if msg.buffer_id != ofp.OFP_NO_BUFFER:
223 # For the case of ARP packets and unknown destination, just do
224 # single packet-out with the same action
226 if msg.buffer_id == ofp.OFP_NO_BUFFER:
229 out = ofp_parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
230 in_port=in_port, actions=actions, data=data)
231 datapath.send_msg(out)
233 @set_ev_cls(ofp_event.EventOFPFlowRemoved, MAIN_DISPATCHER)
234 def flow_removed_handler(self, ev):
236 dpid = msg.datapath.id
238 match_str = ofctl_v1_3.match_to_str(msg.match)
239 index_to_delete = None
241 # Ensure that the flow removed is for a known switch
242 if dpid not in self.switch_flows:
245 for index, flow in enumerate(self.switch_flows[dpid]):
246 if flow['cookie'] == cookie:
247 index_to_delete = index
250 if index_to_delete is not None:
251 del self.switch_flows[dpid][index_to_delete]
252 LOG.debug("Flow removed on switch %d: match=%s, cookie=%s",
253 dpid, match_str, cookie)