前面的幾篇部落格介紹了hub、流表的操作、資料包的解析等知識(以下若有不明白之處,建議先把前幾篇部落格看完)。接下來,根據這些知識就可以編寫自學習交換機的執行個體了。
第一部分:相關知識
轉發表、路由表、ARP表之間的關系需要先行了解(https://cloud.tencent.com/developer/article/1173761)
參考ryubook(https://github.com/Yang-Jianlin/ryu/tree/master/spec_book)
OpenFlow 交換器會接受來自于 controller 的指令并達到以下功能:
- 對于接收到的封包進行修改或針對指定的端口進行轉送
- 對于接收到的封包進行轉送到 Controller 的動作( Packet-In )
- 對于接收到來自 Controller 的封包轉送到指定的端口( Packet-Out )。
上述的功能所組合起來的就是一台交換器的實作。
首先,利用 Packet-In 的功能來達到 MAC 位址的學習。 Controller 使用 Packet-In 接收來自交換器的封包之後進行分析,得到端口相關資料以及所連接配接的 host的 MAC 位址。
在學習之後,對所收到的封包進行轉送。将封包的目的位址,在已經學習的 host 資料中進行檢索,根據檢索的結果會進行下列處理。
- 如果是已經存在記錄中的 host:使用 Packet-Out 功能轉送至先前所對應的端口
- 如果是尚未存在記錄中的 host:使用 Packet-Out 功能來達到 Flooding
具體步驟如下圖所示。
1. 初始狀态
此時的Flow table 為空白。将 host A 接到端口 1, host B 接到端口 4, host C 接到端口 3。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2cs0TPB9EMJpWTwEERNBDOsJGcohVYsR2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL0YzMxAjN0kTMxIDNwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
2. hostA —> hostB
當 host A 向 host B 發送封包。這時後會觸發 Packet-In 訊息。 host A 的 MAC位址會被端口 1 給記錄下來。由于 host B 的 MAC 位址尚未被學習,是以會進行 Flooding 并将封包往 host B 和 host C 發送。
3. host B → host A
封包從 host B 向 host A 傳回時,在 Flow table 中新增一筆 Flow Entry,并将封包轉送到端口 1。是以該封包并不會被 host C收到。
4. host A → host B
再一次, host A 向 host B 發送封包,在 Flow table 中新增一個 Flow Entry 接着轉送封包到端口 4。
第二部分:代碼
接下來,重點介紹以下代碼的編寫。
建立一個類Switch,内容如下:
from ryu.base import app_manager
class Switch(app_manager.RyuApp):
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.mac_table = {} # mac表,即轉發表,初始化為空
這裡我使用v1.3版本的openflow,并且初始化一個字典,用來當作轉發表。當然,由于現在尚未添加任何處理代碼,是以這段程式什麼也做不了。
接下來,接需要繼續向類中添加代碼以完成添加流表功能的開發。
定義一個發送流表的方法,内容如下:
# 流表的操作函數
# 詳細參見:https://blog.csdn.net/weixin_40042248/article/details/115832995?spm=1001.2014.3001.5501
def doflow(self, datapath, command, priority, match, actions):
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
inst = [ofp_parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions)]
req = ofp_parser.OFPFlowMod(datapath=datapath, command=command,
priority=priority, match=match, instructions=inst)
datapath.send_msg(req)
這段代碼就是發送流表的方法,寫完之後,我們隻需要在想要修改流表的時候調用就可以了。
控制器和交換機在最開始的時候進行握手,也就是features資訊的請求和傳回,當控制器收到features消息之後,我們希望controller會向交換機下發一條預設流表項(table-miss),用來處理沒有流表比對時,交換機将資訊發送到控制器。
# 當控制器和交換機開始的握手動作完成後,進行table-miss(預設流表)的添加
# 關于這一段代碼的詳細解析,參見:https://blog.csdn.net/weixin_40042248/article/details/115749340
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
msg = ev.msg
datapath = msg.datapath
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
# add table-miss
command = ofp.OFPFC_ADD
match = ofp_parser.OFPMatch()
actions = [ofp_parser.OFPActionOutput(ofp.OFPP_CONTROLLER, ofp.OFPCML_NO_BUFFER)]
self.doflow(datapath, command, 0, match, actions)
接下來,就是需要對packet_in消息進行處理,控制器根據收到的交換機的消息進行轉發表的學習和流表的下發等操作。具體的解釋,看代碼注釋。
# 關鍵部分,轉發表的學習,流表的下發,控制器的指令等
@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def packet_in_handler(self, ev):
global src, dst
msg = ev.msg
datapath = msg.datapath
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
dpid = datapath.id
# msg實際上是json格式的資料,通過解析,找出in_port
# 可用print(msg)檢視詳細資料
in_port = msg.match['in_port']
# 接下來,主要是解析出源mac位址和目的mac位址
pkt = packet.Packet(msg.data)
for p in pkt.protocols:
if p.protocol_name == 'ethernet':
src = p.src
dst = p.dst
print('src:{0} dst:{1}'.format(src, dst))
# 字典的樣式如下
# {'dpid':{'src':in_port, 'dst':out_port}}
self.mac_table.setdefault(dpid, {})
# 轉發表的每一項就是mac位址和端口,是以在這裡不需要額外的加上dst,port的對應關系,其實傳回的時候目的就是源
self.mac_table[dpid][src] = in_port
# 若轉發表存在對應關系,就按照轉發表進行;沒有就需要廣播得到目的ip對應的mac位址
if dst in self.mac_table[dpid]:
out_port = self.mac_table[dpid][dst]
else:
out_port = ofp.OFPP_FLOOD
actions = [ofp_parser.OFPActionOutput(out_port)]
# 如果執行的動作不是flood,那麼此時應該依據流表項進行轉發操作,是以需要添加流表到交換機
if out_port != ofp.OFPP_FLOOD:
match = ofp_parser.OFPMatch(in_port=in_port, eth_dst=dst, eth_src=src)
command = ofp.OFPFC_ADD
self.doflow(datapath=datapath, command=command, priority=1,
match=match, actions=actions)
data = None
if msg.buffer_id == ofp.OFP_NO_BUFFER:
data = msg.data
# 控制器指導執行的指令
out = ofp_parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
in_port=in_port, actions=actions, data=data)
datapath.send_msg(out)
第三部分:實驗
首先,利用mininet建構網絡拓撲,如下所示。
拓撲設定完成,運作拓撲。然後在Ubuntu指令行運作ryu程式,指令如下。
[email protected]:/home/yang/ryu/ryu/app# ryu-manager switch_yjl.py
ryu程式運作後,可用檢視s1和s2的流表,可用發現s1s2中已經添加了預設的流表項。
接下來,在mininet指令行輸入h1 ping h2,h2 ping h3,h1 ping h3,結果如下。
結果表明,所有主機之間都可以互相ping通,接下來,檢視流表項。
下面附上全部代碼
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 MAIN_DISPATCHER
from ryu.controller.handler import CONFIG_DISPATCHER
from ryu.controller import ofp_event
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
'''
自學習交換機的實作
結合了握手資料解析、流表下發、轉發表學習等操作
'''
class Switch(app_manager.RyuApp):
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.mac_table = {} # mac表,即轉發表,初始化為空
# 流表的操作函數
# 詳細參見:https://blog.csdn.net/weixin_40042248/article/details/115832995?spm=1001.2014.3001.5501
def doflow(self, datapath, command, priority, match, actions):
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
inst = [ofp_parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions)]
req = ofp_parser.OFPFlowMod(datapath=datapath, command=command,
priority=priority, match=match, instructions=inst)
datapath.send_msg(req)
# 當控制器和交換機開始的握手動作完成後,進行table-miss(預設流表)的添加
# 關于這一段代碼的詳細解析,參見:https://blog.csdn.net/weixin_40042248/article/details/115749340
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
msg = ev.msg
datapath = msg.datapath
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
# add table-miss
command = ofp.OFPFC_ADD
match = ofp_parser.OFPMatch()
actions = [ofp_parser.OFPActionOutput(ofp.OFPP_CONTROLLER, ofp.OFPCML_NO_BUFFER)]
self.doflow(datapath, command, 0, match, actions)
# 關鍵部分,轉發表的學習,流表的下發,控制器的指令等
@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def packet_in_handler(self, ev):
global src, dst
msg = ev.msg
datapath = msg.datapath
ofp = datapath.ofproto
ofp_parser = datapath.ofproto_parser
dpid = datapath.id
# msg實際上是json格式的資料,通過解析,找出in_port
# 可用print(msg)檢視詳細資料
in_port = msg.match['in_port']
# 接下來,主要是解析出源mac位址和目的mac位址
pkt = packet.Packet(msg.data)
for p in pkt.protocols:
if p.protocol_name == 'ethernet':
src = p.src
dst = p.dst
print('src:{0} dst:{1}'.format(src, dst))
# 字典的樣式如下
# {'dpid':{'src':in_port, 'dst':out_port}}
self.mac_table.setdefault(dpid, {})
# 轉發表的每一項就是mac位址和端口,是以在這裡不需要額外的加上dst,port的對應關系,其實傳回的時候目的就是源
self.mac_table[dpid][src] = in_port
# 若轉發表存在對應關系,就按照轉發表進行;沒有就需要廣播得到目的ip對應的mac位址
if dst in self.mac_table[dpid]:
out_port = self.mac_table[dpid][dst]
else:
out_port = ofp.OFPP_FLOOD
actions = [ofp_parser.OFPActionOutput(out_port)]
# 如果執行的動作不是flood,那麼此時應該依據流表項進行轉發操作,是以需要添加流表到交換機
if out_port != ofp.OFPP_FLOOD:
match = ofp_parser.OFPMatch(in_port=in_port, eth_dst=dst, eth_src=src)
command = ofp.OFPFC_ADD
self.doflow(datapath=datapath, command=command, priority=1,
match=match, actions=actions)
data = None
if msg.buffer_id == ofp.OFP_NO_BUFFER:
data = msg.data
# 控制器指導執行的指令
out = ofp_parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
in_port=in_port, actions=actions, data=data)
datapath.send_msg(out)
github位址:https://github.com/Yang-Jianlin/ryu/blob/master/ryu/app/switch_yjl.py