stateful openflow

weixin_33871366發表於2018-03-16
整理openstate原理以及具體應用
openstate資料

openstate基本思想就是控制器下放一部分功能,交換機不再是簡單的dumb,而是保留一些簡單的wise。
論文中以埠鎖定為例,提出了米粒型狀態機在交換機內部的應用從而可以大大減少交換機和控制器之間的互動,減緩了控制器的效能瓶頸。
傳統SDN架構,對一些安全應用如埠鎖定,只有一定順序的埠請求才會開放目標埠,如請求22埠進行資料傳輸,但設定的埠請求列表是[10,12,13,14,22],就是說只有在之前目標主機依次收到這些埠請求後才會開放22號埠。這個策略如果需要由控制器來完成,將會十分繁瑣,每次來一個資料包請求控制器,控制器根據歷史埠生成處理策略。
論文中指出,SDN的最大優勢就是全域性能力,而這樣的操作完全是本交換機自己的事情,交給控制器處理並沒有獲得控制器的優勢,反而增加了控制器的處理負擔。因此提出了交換機內部可以維持一個狀態機。


3635313-71e1fdb0706c4884.png
image.png

要在交換機中實現上述狀態轉移功能,交換機需要維持兩張表,狀態表和流表,流表在原來的基礎上進行了擴充,增加了state的查詢。具體如下:


3635313-904383edba136ecb.png
image.png

知識補充:lookup 和 update
lookup和update是交換機實現的功能,由lookup extractor和update extractor兩個功能模組完成,在交換機中實現。兩個功能模組作用就是在資料包中取出唯一識別符號,可以由控制器自定義。當資料包進入交換機時,呼叫lookup extractor,比如以源IP為識別符號,則為這條資料流生成了以IP地址為標識的唯一識別符號。交換機根據這條標識去state table中查詢對應的狀態,並以該狀態和其他匹配欄位去查詢流表,最終完成了整個功能實現。

埠鎖定的實現程式:
LOG = logging.getLogger('app.openstate.portknock')

""" Last port is the one to be opened after knocking all the others """
port_list = [10, 11, 12, 13, 22]
final_port = port_list[-1]
second_last_port =  port_list[-2]

LOG.info("Port knock sequence is %s" % port_list[0:-1])
LOG.info("Final port to open is %s" % port_list[-1])

class OpenStatePortKnocking(app_manager.RyuApp):

    def __init__(self, *args, **kwargs):
        super(OpenStatePortKnocking, self).__init__(*args, **kwargs)

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath

        LOG.info("Configuring switch %d..." % datapath.id)

        """ Set table 0 as stateful """
        req = osparser.OFPExpMsgConfigureStatefulTable(datapath=datapath,
                                                    table_id=0,
                                                    stateful=1)
        datapath.send_msg(req)

        """ Set lookup extractor = {ip_src} """
        req = osparser.OFPExpMsgKeyExtract(datapath=datapath,
                                        command=osproto.OFPSC_EXP_SET_L_EXTRACTOR,
                                        fields=[ofproto.OXM_OF_IPV4_SRC],
                                        table_id=0)
        datapath.send_msg(req)

        """ Set update extractor = {ip_src} (same as lookup) """
        req = osparser.OFPExpMsgKeyExtract(datapath=datapath,
                                    command=osproto.OFPSC_EXP_SET_U_EXTRACTOR,
                                    fields=[ofproto.OXM_OF_IPV4_SRC],
                                    table_id=0)
        datapath.send_msg(req)

        """ ARP packets flooding """
        match = ofparser.OFPMatch(eth_type=0x0806)
        actions = [ofparser.OFPActionOutput(ofproto.OFPP_FLOOD)]
        self.add_flow(datapath=datapath, table_id=0, priority=100,
                        match=match, actions=actions)

        # 提前下發狀態表
        """ Flow entries for port knocking """
        for i in range(len(port_list)):
            match = ofparser.OFPMatch(eth_type=0x0800, ip_proto=17,
                                        state=i, udp_dst=port_list[i])

            if port_list[i] != final_port and port_list[i] != second_last_port:
                # If state not OPEN, set state and drop (implicit)
                actions = [osparser.OFPExpActionSetState(state=i+1, table_id=0, idle_timeout=5)]        
            elif port_list[i] == second_last_port:
                # In the transaction to the OPEN state, the timeout is set to 10 sec
                actions = [osparser.OFPExpActionSetState(state=i+1, table_id=0, idle_timeout=10)]
            else:
                actions = [ofparser.OFPActionOutput(2)]
            self.add_flow(datapath=datapath, table_id=0, priority=10,
                            match=match, actions=actions)

        """ Get back to DEFAULT if wrong knock (UDP match, lowest priority) """
        match = ofparser.OFPMatch(eth_type=0x0800, ip_proto=17)
        actions = [osparser.OFPExpActionSetState(state=0, table_id=0)]
        self.add_flow(datapath=datapath, table_id=0, priority=0,
                        match=match, actions=actions)

        """ Test port 1300, always forward on port 2 """
        match = ofparser.OFPMatch(eth_type=0x0800, ip_proto=17, udp_dst=1300)
        actions = [ofparser.OFPActionOutput(2)]
        self.add_flow(datapath=datapath, table_id=0, priority=10,
                        match=match, actions=actions)


    def add_flow(self, datapath, table_id, priority, match, actions):
        inst = [ofparser.OFPInstructionActions(
                ofproto.OFPIT_APPLY_ACTIONS, actions)]
        mod = ofparser.OFPFlowMod(datapath=datapath, table_id=table_id,
                                priority=priority, match=match, instructions=inst)
        datapath.send_msg(mod)

資料流狀態的轉移是根據已經存在的狀態表來進行查詢和更新的,對一條資料流,先進行狀態查詢,如果符合match = ofparser.OFPMatch(eth_type=0x0800, ip_proto=17, state=i, udp_dst=port_list[i])則會執行相應的動作,actions = [osparser.OFPExpActionSetState(state=i+1, table_id=0, idle_timeout=5)](已經提前下發),所以整個的重點就是狀態表的建立(對每條資料流建立一個狀態表)和更新以及帶狀態的流表的查詢。

class OpenStateMacLearning(app_manager.RyuApp):
    def __init__(self, *args, **kwargs):
        super(OpenStateMacLearning, self).__init__(*args, **kwargs)

    def add_flow(self, datapath, table_id, priority, match, actions):
        if len(actions) > 0:
            inst = [ofparser.OFPInstructionActions(
                    ofproto.OFPIT_APPLY_ACTIONS, actions)]
        else:
            inst = []
        mod = ofparser.OFPFlowMod(datapath=datapath, table_id=table_id,
                                priority=priority, match=match, instructions=inst)
        datapath.send_msg(mod)

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, event):

        """ Switche sent his features, check if OpenState supported """
        msg = event.msg
        datapath = msg.datapath

        LOG.info("Configuring switch %d..." % datapath.id)

        """ Set table 0 as stateful """
        req = osparser.OFPExpMsgConfigureStatefulTable(
                datapath=datapath,
                table_id=0,
                stateful=1)
        datapath.send_msg(req)

        """ Set lookup extractor = {eth_dst} """
        req = osparser.OFPExpMsgKeyExtract(datapath=datapath,
                command=osproto.OFPSC_EXP_SET_L_EXTRACTOR,
                fields=[ofproto.OXM_OF_ETH_DST],
                table_id=0)
        datapath.send_msg(req)

        """ Set update extractor = {eth_src}  """
        req = osparser.OFPExpMsgKeyExtract(datapath=datapath,
                command=osproto.OFPSC_EXP_SET_U_EXTRACTOR,
                fields=[ofproto.OXM_OF_ETH_SRC],
                table_id=0)
        datapath.send_msg(req)

        # for each input port, for each state
        for i in range(1, N+1):
            for s in range(N+1):
                match = ofparser.OFPMatch(in_port=i, state=s)
                if s == 0:
                    out_port = ofproto.OFPP_FLOOD
                else:
                    out_port = s
                actions = [osparser.OFPExpActionSetState(state=i, table_id=0, hard_timeout=10),
                            ofparser.OFPActionOutput(out_port)]
                self.add_flow(datapath=datapath, table_id=0, priority=0,
                                match=match, actions=actions)

上面是用來進行埠學習的,乍看還是有點難懂。他最大的好處就是不用和控制器進行互動,提前預下發流表,然後在交換機內部根據狀態資訊進行學習。
重要的還是對狀態表的查詢和更新的理解。對每一條流有一個狀態,流進入交換機會進行查詢操作,查詢欄位為目的地址;查詢到相應狀態並進行匹配流表和實施action後,會進行更新操作,更新欄位是源地址,也就是說對到來的資料會記錄其IP與埠號(用狀態表示)。
主要思想如下:
第一步:給所有的(假設n個)埠下發n+1個帶狀態的流表。如4口交換機則埠1就有生成5個匹配流表,匹配項為入埠號和state值,action為outport,出埠值就是非0的state值,state為0表示泛洪。
但交換機又是如何學習的呢,一條資料流進入,首先通過lookup查詢當前流的狀態,當沒有學習到目的地址時,state值還是為0,泛洪。同時,因為update以源MAC地址更新,匹配到流表後會取出源MAC地址,更新state為進埠號,osparser.OFPExpActionSetState(state=i, table_id=0, hard_timeout=10)
所以下次當有該目的MAC地址來世,查詢到的state也就是出埠號。設計的有點反常識,但還是證明stateful 資料平面確實是挺強大的,可以在交換機內部完成一些簡單的操作。
stateful openflow 最大的問題就是需要下發的流表太多,在實際應用場景中會有很大的限制。