SDN:Lab 練習三
0x01 作業要求
- Mininet:
- 修正 mininet script,switches 為鏈狀連接,頭尾各接一台 host,在執行時能夠加上參數指定中間 switch 數量
- hw2_ryuapp.py 應該要能直接運作在這個 hw3_net.py topo 上,若有錯誤請修正作業二
- Ryu:
- controller 對每個 switch 初始設置一條 flow entry: 當收到的封包型態為 LLDP 時發給 controller
- controller 讓 switch 每個使用中的 port 送出 LLDP 封包
- 設計一個資料結構存取 LLDP 得到的 topo
- 需要 packet in handler 取得 switch 收到 LLDP 封包中的資訊
0x01 Mininet Script
- hw3_net.py
#!/usr/bin/env python # -*- coding: utf8 -*- """ 2016.07.31 2 host n switch, topo is link, with failmode setting h1--s1--...--sn--h2 $ sudo python hw3_net.py [switchnum] or sudo ./hw3_net.py [switchnum] """ import sys from mininet.log import setLogLevel, info from mininet.net import Mininet from mininet.cli import CLI from mininet.node import RemoteController, OVSSwitch def MininetTopo(switchnum): switchlist = [] net = Mininet() info("Create host nodes.\n") lefthost = net.addHost("h1") righthost = net.addHost("h2") info("Create switch node.\n") count = 1 while count <= int(switchnum): switchname = "s" + str(count) switchlist.append(net.addSwitch(switchname, switch=OVSSwitch, protocols='OpenFlow13', failMode='secure')) count+=1 info("Connect to controller node.\n") net.addController(name='c1',controller=RemoteController,ip='192.168.1.22',port=6633) info("Create Links.\n") net.addLink(lefthost, switchlist[0]) count=1 while count <= int(switchnum)-1: net.addLink(switchlist[count-1],switchlist[count]) count+=1 net.addLink(righthost, switchlist[len(switchlist)-1]) info("build and start.\n") net.build() net.start() CLI(net) if __name__ == '__main__': setLogLevel('info') if len(sys.argv) > 2: print "Too much argv!!" sys.exit(0) elif len(sys.argv) == 2: MininetTopo(sys.argv[1]) else: MininetTopo(1)
- 首先我們透過 sys.argv 去取得輸入的參數,此為 topo 中 switch 數量
- 接著中間利用迴圈產生 switch 並將參照存在 switchlist
- 頭尾 switch 連接 host,中間 switch 則都與前一台相連
0x02 Ryu Application
- hw3_ryuapp.py
#!/usr/bin/env python # -*- coding: utf8 -*- # 2016.07.31 kshuang from ryu.base import app_manager from ryu.ofproto import ofproto_v1_3 from ryu.controller.handler import set_ev_cls from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER from ryu.controller import ofp_event from ryu.ofproto import ofproto_v1_3_parser from ryu.lib.packet import ethernet from ryu.lib.packet import ether_types from ryu.lib.packet import lldp from ryu.lib.packet import packet from ryu import utils class MyRyu(app_manager.RyuApp): OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] normal_port = [] lldp_topo = {} def __init__(self, *args, **kwargs): super(MyRyu, self).__init__(*args, **kwargs) @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER) def switch_features_handler(self, ev): msg = ev.msg datapath = msg.datapath self.send_port_stats_request(datapath) def send_port_stats_request(self, datapath): ofp = datapath.ofproto ofp_parser = datapath.ofproto_parser req = ofp_parser.OFPPortDescStatsRequest(datapath, 0, ofp.OFPP_ANY) datapath.send_msg(req) @set_ev_cls(ofp_event.EventOFPPortDescStatsReply, MAIN_DISPATCHER) def port_stats_reply_handler(self, ev): msg = ev.msg datapath = msg.datapath ofproto = datapath.ofproto parser = datapath.ofproto_parser # LLDP packet to controller match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_LLDP) actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER)] self.add_flow(datapath, 0, match, actions) for stat in ev.msg.body: if stat.port_no < ofproto.OFPP_MAX: self.normal_port.append(stat.port_no) self.send_lldp_packet(datapath, stat.port_no, stat.hw_addr) if len(self.normal_port) == 2: # port B to port A match = parser.OFPMatch(in_port=self.normal_port[0]) actions = [parser.OFPActionOutput(self.normal_port[1])] self.add_flow(datapath, 0, match, actions) # port A to port B match = parser.OFPMatch(in_port=self.normal_port[1]) actions = [parser.OFPActionOutput(self.normal_port[0])] self.add_flow(datapath, 0, match, actions) # clear port record after add flow entry self.normal_port = [] def add_flow(self, datapath, priority, match, actions): ofp = datapath.ofproto parser = datapath.ofproto_parser inst = [parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS,actions)] mod = parser.OFPFlowMod(datapath=datapath, priority=priority, command=ofp.OFPFC_ADD, match=match, instructions=inst) datapath.send_msg(mod) def send_lldp_packet(self, datapath, port_no, hw_addr): ofp = datapath.ofproto pkt = packet.Packet() pkt.add_protocol(ethernet.ethernet(ethertype=ether_types.ETH_TYPE_LLDP,src=hw_addr ,dst=lldp.LLDP_MAC_NEAREST_BRIDGE)) tlv_chassis_id = lldp.ChassisID(subtype=lldp.ChassisID.SUB_LOCALLY_ASSIGNED, chassis_id=str(datapath.id)) tlv_port_id = lldp.PortID(subtype=lldp.PortID.SUB_LOCALLY_ASSIGNED, port_id=str(port_no)) tlv_ttl = lldp.TTL(ttl=10) tlv_end = lldp.End() tlvs = (tlv_chassis_id, tlv_port_id, tlv_ttl, tlv_end) pkt.add_protocol(lldp.lldp(tlvs)) pkt.serialize() data = pkt.data parser = datapath.ofproto_parser actions = [parser.OFPActionOutput(port=port_no)] out = parser.OFPPacketOut(datapath=datapath, buffer_id=ofp.OFP_NO_BUFFER, in_port=ofp.OFPP_CONTROLLER, actions=actions, data=data) datapath.send_msg(out) @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER) def packet_in_handler(self, ev): msg = ev.msg datapath = msg.datapath port = msg.match['in_port'] pkt = packet.Packet(data=msg.data) pkt_ethernet = pkt.get_protocol(ethernet.ethernet) if not pkt_ethernet: return pkt_lldp = pkt.get_protocol(lldp.lldp) if pkt_lldp: self.handle_lldp(datapath, port, pkt_ethernet, pkt_lldp) def handle_lldp(self, datapath, port, pkt_ethernet, pkt_lldp): self.lldp_topo.setdefault(datapath.id,{}) port_connect = {} self.lldp_topo[datapath.id].setdefault(port, [pkt_lldp.tlvs[0].chassis_id, pkt_lldp.tlvs[1].port_id]) print self.lldp_topo @set_ev_cls(ofp_event.EventOFPErrorMsg,MAIN_DISPATCHER) def error_msg_handler(self, ev): msg = ev.msg self.logger.debug('OFPErrorMsg received: type=0x%02x code=0x%02x message=%s',msg.type, msg.code, utils.hex_array(msg.data))
定義和初始化類別
class MyRyu(app_manager.RyuApp): OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] normal_port = [] lldp_topo = {} def __init__(self, *args, **kwargs): super(MyRyu, self).__init__(*args, **kwargs) # ...
首先,我們寫了一個類別 MyRyu, 繼承 app_manager.RyuApp 這個 Ryu applications 的基礎類別
接著 OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
這邊列出了這個 Ryu applications 所支援的 OpenFlow version,可以填寫多個,預設是 all versions
source code 可參考: Ryu GitHub: base/app_manager.py
OpenFlow Handshark
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER) def switch_features_handler(self, ev): msg = ev.msg datapath = msg.datapath self.send_port_stats_request(datapath) # ...
這部分跟作業二相同,我們註冊一個 EventOFPSwitchFeatures 監聽事件,這個事件在 openflow handshark 結束後被觸發
事件被觸發後表示有某 switch 已經和 controller 建立 openflow connection,此時我們呼叫 send_port_stats_request 這個我們寫的 function 去嘗試取得 switch port 的資訊
嘗試取得 Switch port 資訊
def send_port_stats_request(self, datapath): ofp = datapath.ofproto ofp_parser = datapath.ofproto_parser req = ofp_parser.OFPPortDescStatsRequest(datapath, 0, ofp.OFPP_ANY) datapath.send_msg(req) # ...
這邊與 SDN:Lab 作業二 比較不同之處在於 OFPPortDescStatsRequest
作業二使用的 OFPPortStatsRequest
是請求 switch port 上一些傳送接收等統計資訊
而作業三在接下來的部分我們需要發送 LLDP 封包,所以這邊需要用 OFPPortDescStatsRequest
來取得 switch port 的 mac address(hw_addr)
對於作業二只需取得 active port, 兩種方法是都可達到目的,但邏輯上而言使用 OFPPortDescStatsRequest
應該較佳
參考資料: ryu.ofproto.ofproto_v1_3_parser.OFPPortDescStatsRequest
監聽回應並設置 flow entry
@set_ev_cls(ofp_event.EventOFPPortDescStatsReply, MAIN_DISPATCHER) def port_stats_reply_handler(self, ev): msg = ev.msg datapath = msg.datapath ofproto = datapath.ofproto parser = datapath.ofproto_parser # LLDP packet to controller match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_LLDP) actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER)] self.add_flow(datapath, 0, match, actions) for stat in ev.msg.body: if stat.port_no < ofproto.OFPP_MAX: self.normal_port.append(stat.port_no) self.send_lldp_packet(datapath, stat.port_no, stat.hw_addr) if len(self.normal_port) == 2: # port B to port A match = parser.OFPMatch(in_port=self.normal_port[0]) actions = [parser.OFPActionOutput(self.normal_port[1])] self.add_flow(datapath, 0, match, actions) # port A to port B match = parser.OFPMatch(in_port=self.normal_port[1]) actions = [parser.OFPActionOutput(self.normal_port[0])] self.add_flow(datapath, 0, match, actions) # clear port record after add flow entry self.normal_port = []
這邊 @set_ev_cls(ofp_event.EventOFPPortDescStatsReply, MAIN_DISPATCHER)
監聽著 EventOFPPortDescStatsReply 事件,也就是我們前面發出的請求回應
除了保留作業二的 flow entries 之外,收到 switch 的回應後我們也另外對 switch 下一條 flow entry: 凡是封包型態 match LLDP 的都送往 Controller
接著使用迴圈取出switch 上所有的 normal active port,並從該 switch port 發送 lldp 封包
新增 flow entry
def add_flow(self, datapath, priority, match, actions): ofp = datapath.ofproto parser = datapath.ofproto_parser inst = [parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS,actions)] mod = parser.OFPFlowMod(datapath=datapath, priority=priority, command=ofp.OFPFC_ADD, match=match, instructions=inst) datapath.send_msg(mod)
這個 function 跟作業二是相同的
發送 LLDP 封包
def send_lldp_packet(self, datapath, port_no, hw_addr): ofp = datapath.ofproto pkt = packet.Packet() pkt.add_protocol(ethernet.ethernet(ethertype=ether_types.ETH_TYPE_LLDP,src=hw_addr ,dst=lldp.LLDP_MAC_NEAREST_BRIDGE)) tlv_chassis_id = lldp.ChassisID(subtype=lldp.ChassisID.SUB_LOCALLY_ASSIGNED, chassis_id=str(datapath.id)) tlv_port_id = lldp.PortID(subtype=lldp.PortID.SUB_LOCALLY_ASSIGNED, port_id=str(port_no)) tlv_ttl = lldp.TTL(ttl=10) tlv_end = lldp.End() tlvs = (tlv_chassis_id, tlv_port_id, tlv_ttl, tlv_end) pkt.add_protocol(lldp.lldp(tlvs)) pkt.serialize() data = pkt.data parser = datapath.ofproto_parser actions = [parser.OFPActionOutput(port=port_no)] out = parser.OFPPacketOut(datapath=datapath, buffer_id=ofp.OFP_NO_BUFFER, in_port=ofp.OFPP_CONTROLLER, actions=actions, data=data) datapath.send_msg(out) # ...
這邊首先參考一下 維基百科-鏈路層發現協議
可以得知一個 LLDP 封包要包含的欄位有:
- dst mac: 是固定三種的其中一種 ( lldp.LLDP_MAC_NEAREST_BRIDGE )
- src mac: 這邊我們需要填發送 LLDP 封包的 switch port 的 mac ( 前面傳入的 hw_addr )
- eth type: LLDP 固定為 0x88cc ( ether_types.ETH_TYPE_LLDP )
- Chassis ID TLV: 我們在這邊放發送 LLDP 封包的 switch id ( chassis_id=str(datapath.id) )
- Port ID TLV: 我們在這邊放發送 LLDP 封包的 switch port id ( port_id=str(port_no) )
- Time to live TLV ( 10 )
- [可選 TLV] ( NONE )
- End of LLDPDU TLV ( lldp.End() )
Ryu 要產生一個封包大概會經過幾個過程
- 產生 ryu.lib.packet.packet.Packet 類別的物件
- 產生相對應的協定物件: 封包是層層封裝的,所以像這邊 LLDP 的封包除了要產生 LLDP 的協定物件外,前面 dst mac, src mac, eth type 欄位則由 ethernet 協定物件封裝
- 使用 add_protocol 函式將協定物件加入 packet
- 經過序列化將 python 物件轉換成 byte string
參考資料:
封包包好之後我們需要將它傳送出去
action 我們透過 parser.OFPActionOutput 指定 switch 要把封包往哪個 port 送出去
這邊是官方 source code 內的註解
@OFPAction.register_action_type(ofproto.OFPAT_OUTPUT, ofproto.OFP_ACTION_OUTPUT_SIZE) class OFPActionOutput(OFPAction): """ Output action This action indicates output a packet to the switch port. ================ ====================================================== Attribute Description ================ ====================================================== port Output port max_len Max length to send to controller ================ ====================================================== """ def __init__(self, port, max_len=ofproto.OFPCML_MAX, type_=None, len_=None): # ...
接著看一下 OFPPacketOut
@_set_msg_type(ofproto.OFPT_PACKET_OUT) class OFPPacketOut(MsgBase): """ Packet-Out message The controller uses this message to send a packet out throught the switch. ================ ====================================================== Attribute Description ================ ====================================================== buffer_id ID assigned by datapath (OFP_NO_BUFFER if none) in_port Packet's input port or ``OFPP_CONTROLLER`` actions list of OpenFlow action class data Packet data ================ ====================================================== Example:: def send_packet_out(self, datapath, buffer_id, in_port): ofp = datapath.ofproto ofp_parser = datapath.ofproto_parser actions = [ofp_parser.OFPActionOutput(ofp.OFPP_FLOOD, 0)] req = ofp_parser.OFPPacketOut(datapath, buffer_id, in_port, actions) datapath.send_msg(req) """ def __init__(self, datapath, buffer_id=None, in_port=None, actions=None, data=None, actions_len=None):
這樣 controller 就能將 LLDP 發送至 switch,且透過 OFPActionOutput,switch 就能知道這個封包他該往哪個 port 發出去
這邊做的事是 controller 做好封包,設好 OFPActionOutput,對 switch 來說是他必須將這個封包“發送”出去
前面下的 flow entry 則是告訴 switch 若接收到 (其他 switch發過來) LLDP 封包就轉發給 controller
packet_in_handler
@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER) def packet_in_handler(self, ev): msg = ev.msg datapath = msg.datapath port = msg.match['in_port'] pkt = packet.Packet(data=msg.data) pkt_ethernet = pkt.get_protocol(ethernet.ethernet) if not pkt_ethernet: return pkt_lldp = pkt.get_protocol(lldp.lldp) if pkt_lldp: self.handle_lldp(datapath, port, pkt_ethernet, pkt_lldp) # ...
因為 switch 會將收到的 LLDP 封包發給 controller,所以我們必須寫一個 packet in 的監聽事件
這個監聽事件中 port = msg.match['in_port'] 先取出封包是從哪個 port 進入 switch 的
將著嘗試取出 ethernet packet 和 lldp packet
因為 packet in handler 邏輯上可能收到各種封包,所以這邊確認有取到 LLDP 封包後我們呼叫 handle_lldp function 來處理
handle_lldp
def handle_lldp(self, datapath, port, pkt_ethernet, pkt_lldp): swp1 = ["s"+str(datapath.id), "port "+str(port)] swp2 = ["s"+str(pkt_lldp.tlvs[0].chassis_id), "port "+str(pkt_lldp.tlvs[1].port_id)] self.sw_port_to_sw_port.append([swp1, swp2]) print self.sw_port_to_sw_port # ...
這邊 swp1 我們紀錄封包是從哪個 switch 的哪個 port 發出
swp2 則紀錄是由哪個 switch 的哪個 port 收到
接著將這比對應資料存入 sw_port_to_sw_port 這個 list 中
error_msg_handler
@set_ev_cls(ofp_event.EventOFPErrorMsg,MAIN_DISPATCHER) def error_msg_handler(self, ev): msg = ev.msg self.logger.debug('OFPErrorMsg received: type=0x%02x code=0x%02x message=%s',msg.type, msg.code, utils.hex_array(msg.data))
這個部分是我一開始用 OpenFlow15 去做,卻發現 LLDP 封包不知為何完全送不出去,tcpdump 在 controller 跟 switch 完全沒看到東西,所以加上去用來除錯的
但是加上去也沒看到噴什麼錯誤,所以最後先改回 OpenFlow13 版本了
@_register_parser @_set_msg_type(ofproto.OFPT_ERROR) class OFPErrorMsg(MsgBase): """ Error message The switch notifies controller of problems by this message. ========== ========================================================= Attribute Description ========== ========================================================= type High level type of error code Details depending on the type data Variable length data depending on the type and code ========== ========================================================= ``type`` attribute corresponds to ``type_`` parameter of __init__. Types and codes are defined in ``ryu.ofproto.ofproto``. ============================= =========== Type Code ============================= =========== OFPET_HELLO_FAILED OFPHFC_* OFPET_BAD_REQUEST OFPBRC_* OFPET_BAD_ACTION OFPBAC_* OFPET_BAD_INSTRUCTION OFPBIC_* OFPET_BAD_MATCH OFPBMC_* OFPET_FLOW_MOD_FAILED OFPFMFC_* OFPET_GROUP_MOD_FAILED OFPGMFC_* OFPET_PORT_MOD_FAILED OFPPMFC_* OFPET_TABLE_MOD_FAILED OFPTMFC_* OFPET_QUEUE_OP_FAILED OFPQOFC_* OFPET_SWITCH_CONFIG_FAILED OFPSCFC_* OFPET_ROLE_REQUEST_FAILED OFPRRFC_* OFPET_METER_MOD_FAILED OFPMMFC_* OFPET_TABLE_FEATURES_FAILED OFPTFFC_* OFPET_EXPERIMENTER N/A ============================= =========== Example:: @set_ev_cls(ofp_event.EventOFPErrorMsg, [HANDSHAKE_DISPATCHER, CONFIG_DISPATCHER, MAIN_DISPATCHER]) def error_msg_handler(self, ev): msg = ev.msg self.logger.debug('OFPErrorMsg received: type=0x%02x code=0x%02x ' 'message=%s', msg.type, msg.code, utils.hex_array(msg.data)) """ def __init__(self, datapath, type_=None, code=None, data=None):
0x03 測試
測試部分可以參考SDN:Lab 作業二
基本上作法差不多,如果有成功 Ryu 這邊就會印訊息了
也可以用 tcpdump 去過濾來 debug
例如要過濾 s2-eth1 LLDP 封包
mininet> s2 tcpdump -i s2-eth1 ether proto 0x88cc