一:自學習交換機(二層MAC交換機)的編程思路
(一)明確問題
如何實現軟件定義的自學習交換機?
(二)設計解決方案
通過控制器來實現自學習交換算法,然后指導數據平面實現交換機操作
(三)確定具體的技術方案
控制器選用Ryu,數據平面通過Mininet模擬
(四)部署實施
在控制器上編程開發交換機應用,創建實驗網絡為驗證方案做准備
(五)驗證方案
運行程序,調試程序,驗證程序
(六)優化
驗證成功后,優化程序
二:自學習交換機原理
(一)普通交換機實現
交換機MAC地址表記錄了統一網段中的各個主機對應交換機的端口和主機的MAC地址
當主機A要和主機B通信時,初始交換機MAC表是空的,會先記錄主機A的MAC地址和對應的交換機端口,然后查找交換機MAC中是否有目標MAC地址,沒有找到,會向其他所有端口泛洪查找
泛洪,通知其他主機。主機C接收到數據包,發現不是自己的,則不處理,丟棄數據包。當主機B接收后,發現是找自己的,則可以進行消息通信。交換機先進行MAC學習,記錄主機B的MAC信息,再進行查表轉發,單播發送給主機A
(二)SDN中交換機實現
SDN中交換機不存儲MAC表,(datapath)只存在流表。其地址學習操作由控制器(控制器中包含MAC 地址表)實現,之后控制器下發流表項給交換機
1.主機A向主機B發送信息,流表中只存在默認流表,告訴交換機將數據包發送給控制器。
2.控制器先進行MAC地址學習,記錄主機A的MAC地址和其對應交換機端口,然后查詢MAC地址表,查找主機B信息。沒有則下發流表項告訴交換機先泛洪試試
3.泛洪后,主機C接收后丟棄數據包,不處理。主機B發現是尋找自己的,則進行消息回送,由於交換機流表中沒有處理主機B到主機A的信息的流表項,所以只能向控制器發送數據包。控制器先學習主機B的MAC地址和對應交換機端口,之后查詢MAC地址表,找到主機A的MAC信息,下發流表項,告訴交換機如何處理主機B->主機A的消息
4.注意:這里交換機的流表項中只存在主機B->主機A的流表項處理方案,不存在主機A->主機B的處理流表項(但是控制器MAC地址表中是存在主機B的信息),所以會在下一次數據傳送中,控制器下發響應的流表項。但是其實可以實現(在3中一次下發兩個流表項)
三:代碼實現
(一)全部代碼
from ryu.base import app_manager from ryu.ofproto import ofproto_v1_3 from ryu.controller import ofp_event from ryu.controller.handler import set_ev_cls from ryu.controller.handler import CONFIG_DISPATCHER,MAIN_DISPATCHER from ryu.lib.packet import packet from ryu.lib.packet import ethernet class SelfLearnSwitch(app_manager.RyuApp): OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] #set openflow protocol version while we support def __init__(self,*args,**kwargs): super(SelfLearnSwitch,self).__init__(*args,**kwargs) #set a data construction to save MAC Address Table self.Mac_Port_Table={} @set_ev_cls(ofp_event.EventOFPSwitchFeatures) def switch_features_handler(self,ev): ''' manage the initial link, from switch to controller ''' #first parse event to get datapath and openflow protocol msg = ev.msg datapath = msg.datapath ofproto = datapath.ofproto ofp_parser = datapath.ofproto_parser self.logger.info("datapath: %s link to controller",datapath.id) #secondly set match and action match = ofp_parser.OFPMatch() #all data message match successful actions = [ofp_parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,ofproto.OFPCML_NO_BUFFER)] #set receive port and buffer for switch #add flow and send it to switch in add_flow self.add_flow(datapath,0,match,actions,"default flow entry") def add_flow(self,datapath,priority,match,actions,extra_info): """ add flow entry to switch """ #get open flow protocol infomation ofproto = datapath.ofproto ofp_parser = datapath.ofproto_parser #set instruction infomation from openflow protocol 1.3 inst = [ofp_parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,actions)] #set flow entry mod mod = ofp_parser.OFPFlowMod(datapath=datapath,priority=priority,match=match,instructions=inst) print("send "+extra_info) #send flow entry to switch datapath.send_msg(mod) @set_ev_cls(ofp_event.EventOFPPacketIn,MAIN_DISPATCHER) def packet_in_handler(self,ev): ''' manage infomation from switch ''' #first parser openflow protocol msg = ev.msg datapath = msg.datapath ofproto = datapath.ofproto ofp_parser = datapath.ofproto_parser #get datapath id from datapath, and save dpid into MAC table (default) dpid = datapath.id self.Mac_Port_Table.setdefault(dpid, {}) #analysize packet, get ethernet data, get host MAC info pkt = packet.Packet(msg.data) eth_pkt = pkt.get_protocol(ethernet.ethernet) dst = eth_pkt.dst src = eth_pkt.src #get switch port where host packet send in in_port = msg.match['in_port'] self.logger.info("Controller %s get packet, Mac address from: %s send to: %s , send from datapath: %s,in port is: %s" ,dpid,src,dst,dpid,in_port) #save src data into dictionary---MAC address table self.Mac_Port_Table[dpid][src] = in_port #query MAC address table to get destinction host`s port from current datapath #---first: find port to send packet #---second: not find port,so send packet by flood if dst in self.Mac_Port_Table[dpid]: Out_Port = self.Mac_Port_Table[dpid][dst] else: Out_Port = ofproto.OFPP_FLOOD #set match-action from above status actions = [ofp_parser.OFPActionOutput(Out_Port)] #add a new flow entry to switch by add_flow if Out_Port != ofproto.OFPP_FLOOD: #if Out_port == ofproto.OFPP_FLOOD ---> flow entry == default flow entry, it already exist match = ofp_parser.OFPMatch(in_port=in_port,eth_dst = dst) self.add_flow(datapath, 1, match, actions,"a new flow entry by specify port") self.logger.info("send packet to switch port: %s",Out_Port) #finally send the packet to datapath, to achive self_learn_switch Out = ofp_parser.OFPPacketOut(datapath=datapath,buffer_id=msg.buffer_id, in_port=in_port,actions=actions,data=msg.data) datapath.send_msg(Out)
(二)代碼講解(一)
from ryu.base import app_manager from ryu.ofproto import ofproto_v1_3 from ryu.controller import ofp_event from ryu.controller.handler import set_ev_cls from ryu.controller.handler import CONFIG_DISPATCHER,MAIN_DISPATCHER from ryu.lib.packet import packet from ryu.lib.packet import ethernet class SelfLearnSwitch(app_manager.RyuApp): OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] #set openflow protocol version while we support def __init__(self,*args,**kwargs): super(SelfLearnSwitch,self).__init__(*args,**kwargs) #set a data construction to save MAC Address Table self.Mac_Port_Table={} @set_ev_cls(ofp_event.EventOFPSwitchFeatures) def switch_features_handler(self,ev): ''' manage the initial link, from switch to controller ''' #first parse event to get datapath and openflow protocol msg = ev.msg datapath = msg.datapath ofproto = datapath.ofproto ofp_parser = datapath.ofproto_parser self.logger.info("datapath: %s link to controller",datapath.id) #secondly set match and action match = ofp_parser.OFPMatch() #all data message match successful actions = [ofp_parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,ofproto.OFPCML_NO_BUFFER)] #set receive port and buffer for switch #add flow and send it to switch in add_flow self.add_flow(datapath,0,match,actions,"default flow entry") def add_flow(self,datapath,priority,match,actions,extra_info): """ add flow entry to switch """ #get open flow protocol infomation ofproto = datapath.ofproto ofp_parser = datapath.ofproto_parser #set instruction infomation from openflow protocol 1.3 inst = [ofp_parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,actions)] #set flow entry mod mod = ofp_parser.OFPFlowMod(datapath=datapath,priority=priority,match=match,instructions=inst) print("send "+extra_info) #send flow entry to switch datapath.send_msg(mod)
以上代碼同SDN實驗---Ryu的應用開發(一)Hub實現,實現了設備與控制器初始連接,下發默認流表項,使得默認情況下,交換機在無法匹配到流表項時,直接去找控制器。一個一個公共函數add_flow實現流表下發。注意:在__init__方法中實現了數據結構《字典》去存儲MAC地址表,為下面做准備
(三)代碼講解(二)
@set_ev_cls(ofp_event.EventOFPPacketIn,MAIN_DISPATCHER) def packet_in_handler(self,ev): ''' manage infomation from switch ''' #first parser openflow protocol 先解析OpenFlow協議信息 msg = ev.msg datapath = msg.datapath ofproto = datapath.ofproto ofp_parser = datapath.ofproto_parser #get datapath id from datapath, and save dpid into MAC table (default) 獲取datapath(虛擬交換機的id),用dpid初始化一個鍵值 dpid = datapath.id self.Mac_Port_Table.setdefault(dpid, {}) #analysize packet, get ethernet data, get host MAC info 分析packert數據包,因為轉發的包,都是基於以太網協議的,所以我們需要用到以太網協議進行解析,獲取源MAC和目的MAC pkt = packet.Packet(msg.data) eth_pkt = pkt.get_protocol(ethernet.ethernet) dst = eth_pkt.dst src = eth_pkt.src #get switch port where host packet send in 獲取datapath的數據輸入端口 in_port = msg.match['in_port'] self.logger.info("Controller %s get packet, Mac address from: %s send to: %s , send from datapath: %s,in port is: %s" ,dpid,src,dst,dpid,in_port) #打印調試信息 #save src data into dictionary---MAC address table 將源MAC地址保存,學習,放入MAC表中 self.Mac_Port_Table[dpid][src] = in_port #query MAC address table to get destinction host`s port from current datapath 查詢MAC表,是否有目標MAC地址的鍵值 #---first: find port to send packet 如果找到,我們則按照該端口發送 #---second: not find port,so send packet by flood 如果沒有找到,我們需要泛洪發送給下一個(或者下幾個)交換機,依次查詢 if dst in self.Mac_Port_Table[dpid]: Out_Port = self.Mac_Port_Table[dpid][dst] else: Out_Port = ofproto.OFPP_FLOOD #set match-action from above status 開始設置match-actions匹配動作 actions = [ofp_parser.OFPActionOutput(Out_Port)] #add a new flow entry to switch by add_flow 進行對應的流表項下發 《重點》 if Out_Port != ofproto.OFPP_FLOOD: match = ofp_parser.OFPMatch(in_port=in_port,eth_dst = dst) self.add_flow(datapath, 1, match, actions,"a new flow entry by specify port") self.logger.info("send packet to switch port: %s",Out_Port) #finally send the packet to datapath, to achive self_learn_switch 最后我們將之前交換機發送上來的數據,重新發給交換機 Out = ofp_parser.OFPPacketOut(datapath=datapath,buffer_id=msg.buffer_id, in_port=in_port,actions=actions,data=msg.data) #我們必須加上這個data,才可以將packet數據包發送回去《重點》不然會出錯×××××× datapath.send_msg(Out)
(四)實驗演示
1.啟動Ryu控制器
2.啟動mininet
3.Ryu進行響應
注意:這里我一啟動Mininet,就已經獲取了所有的MAC信息,應該是主機接入網絡后發送某些數據包,導致控制器獲得了MAC表(需要使用wireshark抓包工具進行分析....后面進行補充)
網絡可達,說明實現自學習交換機
四:補充知識
(一)pkt = packet.Packet(msg.data) 一個類,在Ryu/lib/packet/模塊下,用於包的解碼/編碼
class Packet(StringifyMixin): """A packet decoder/encoder class. An instance is used to either decode or encode a single packet. *data* is a bytearray to describe a raw datagram to decode. data是一個未加工的報文數據, 即msg.data直接從事件的msg中獲取的數據 When decoding, a Packet object is iteratable. Iterated values are protocol (ethernet, ipv4, ...) headers and the payload. Protocol headers are instances of subclass of packet_base.PacketBase. The payload is a bytearray. They are iterated in on-wire order. *data* should be omitted when encoding a packet. """ # Ignore data field when outputting json representation. _base_attributes = ['data'] def __init__(self, data=None, protocols=None, parse_cls=ethernet.ethernet): 協議解析,默認是按照以太網協議 super(Packet, self).__init__() self.data = data if protocols is None: self.protocols = [] else: self.protocols = protocols if self.data: self._parser(parse_cls)
(二)eth_pkt = pkt.get_protocol(ethernet.ethernet) 返回與指定協議匹配的協議列表。從packet包中獲取協議信息(協議包含我們需要的dst,src等,如三中所示)
class Packet(StringifyMixin): def add_protocol(self, proto): """Register a protocol *proto* for this packet. This method is legal only when encoding a packet. When encoding a packet, register a protocol (ethernet, ipv4, ...) header to add to this packet. Protocol headers should be registered in on-wire order before calling self.serialize. """ self.protocols.append(proto) def get_protocols(self, protocol): """Returns a list of protocols that matches to the specified protocol. """ if isinstance(protocol, packet_base.PacketBase): protocol = protocol.__class__ assert issubclass(protocol, packet_base.PacketBase) return [p for p in self.protocols if isinstance(p, protocol)]
(三)eth_pkt = pkt.get_protocol(ethernet.ethernet) 一個類,也在Ryu/lib/packet/模塊下,用於以太網報頭編碼器/解碼器類。
class ethernet(packet_base.PacketBase): """Ethernet header encoder/decoder class. An instance has the following attributes at least. MAC addresses are represented as a string like '08:60:6e:7f:74:e7'. __init__ takes the corresponding args in this order. ============== ==================== ===================== Attribute Description Example ============== ==================== ===================== dst destination address 'ff:ff:ff:ff:ff:ff' src source address '08:60:6e:7f:74:e7' ethertype ether type 0x0800 ============== ==================== ===================== """ _PACK_STR = '!6s6sH' _MIN_LEN = struct.calcsize(_PACK_STR) _MIN_PAYLOAD_LEN = 46 _TYPE = { 'ascii': [ 'src', 'dst' ] } def __init__(self, dst='ff:ff:ff:ff:ff:ff', src='00:00:00:00:00:00', ethertype=ether.ETH_TYPE_IP): super(ethernet, self).__init__() self.dst = dst self.src = src self.ethertype = ethertype @classmethod def parser(cls, buf): dst, src, ethertype = struct.unpack_from(cls._PACK_STR, buf) return (cls(addrconv.mac.bin_to_text(dst), addrconv.mac.bin_to_text(src), ethertype), ethernet.get_packet_type(ethertype), buf[ethernet._MIN_LEN:]) def serialize(self, payload, prev): # Append padding if the payload is less than 46 bytes long pad_len = self._MIN_PAYLOAD_LEN - len(payload) if pad_len > 0: payload.extend(b'\x00' * pad_len) return struct.pack(ethernet._PACK_STR, addrconv.mac.text_to_bin(self.dst), addrconv.mac.text_to_bin(self.src), self.ethertype) @classmethod def get_packet_type(cls, type_): """Override method for the ethernet IEEE802.3 Length/Type field (self.ethertype). If the value of Length/Type field is less than or equal to 1500 decimal(05DC hexadecimal), it means Length interpretation and be passed to the LLC sublayer.""" if type_ <= ether.ETH_TYPE_IEEE802_3: type_ = ether.ETH_TYPE_IEEE802_3 return cls._TYPES.get(type_)