資訊人筆記

Work hard, Have fun, Make history!

使用者工具

網站工具


ccis_lab:sdn:hw3

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 要產生一個封包大概會經過幾個過程

  1. 產生 ryu.lib.packet.packet.Packet 類別的物件
  2. 產生相對應的協定物件: 封包是層層封裝的,所以像這邊 LLDP 的封包除了要產生 LLDP 的協定物件外,前面 dst mac, src mac, eth type 欄位則由 ethernet 協定物件封裝
  3. 使用 add_protocol 函式將協定物件加入 packet
  4. 經過序列化將 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

0x04 參考資料

ccis_lab/sdn/hw3.txt · 上一次變更: 127.0.0.1