ryu 代码案例分析到理解简单 SDN 网络

First Post:

Last Update:

ryu 作为一个由 python 编写的控制器,其 app 均可直接通过 python 来创建,本文仅做学习之余的备忘录,用以熟悉其开发流程。

一般来说,通过 ryu-manage app.py 即可运行该应用。在需要运行多个应用时可按顺序添加参数。
下文会对此做一些额外的实验(希望)。

前置

ryu 提供了前置的各项环境,在开发应用时,只需要声明一个继承自 app_manager.RyuApp ,该类就会具备各项所需条件和环境而无需再做额外操作:

1
2
3
4
from ryu.base import app_manager
class SimpleSwitch13(app_manager.RyuApp):
def __init__(self, *args, **kwargs):
super(SimpleSwitch13, self).__init__(*args, **kwargs)

以上为固定的范式,一般所有的应用都可以这么声明。SimpleSwitch13 为应用名称。

行为

修饰器与事件

在完成应用框架的声明以后,接下来是要定义应用的具体行为。ryu 提供了特定的修饰器 set_ev_cls 以帮助用户解释不同命令的行为模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SimpleSwitch13(app_manager.RyuApp):
#...此处省略...

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

# install table-miss flow entry
#
# We specify NO BUFFER to max_len of the output action due to
# OVS bug. At this moment, if we specify a lesser number, e.g.,
# 128, OVS will send Packet-In with invalid buffer_id and
# truncated packet data. In that case, we cannot output packets
# correctly. The bug has been fixed in OVS v2.1.0.
match = parser.OFPMatch()
actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
ofproto.OFPCML_NO_BUFFER)]
self.add_flow(datapath, 0, match, actions)

set_ev_cls 的参数分别对应了:
- 所监听的事件名
- 所处的阶段

监听事件名包括了这些内容:

  • ryu.controller.ofp_event
    • datapath state 通知系
      • EventOFPStateChange
    • EventOFPMsgBase 基类
      • EventOFPHello
      • EventOFPSwitchFeatures
      • EventOFPPortDescStatsReply
      • EventOFPEchoRequest
      • EventOFPEchoReply
      • EventOFPPortStatus
      • EventOFPErrorMsg
    • port state 通知系
      • EventOFPPortStateChange - 在 EventOFPPortStatus 之后发出

在一个常规的连接行为下,会按照如下的顺序分阶段完成:

  • 控制器与交换机完成 TCP/STL 连接建立
  • 控制器监听到 EventOFPStateChange 事件
  • 交换机发送 OFPT_HELLO - 此时控制器监听 EventOFPHello
    • from: ryu.controller.controller.Datapath._recv_loop
    • ryu.controller.ofp_handler.OFPHandler.hello_handler [HANDSHAKE_DISPATCHER]
  • 控制器监听到 EventOFPStateChange 事件
    • ryu.controller.ofp_handler.OFPHandler.hello_handler [CONFIG_DISPATCHER] 发出
  • controller.ofp_handler.OFPHandler.hello_handler 发出 OFPFeaturesRequest
  • 交换机发送 OFPT_FEATURES 事件
  • 控制器监听 EventOFPSwitchFeatures
    •  ryu.controller.controller.Datapath._recv_loop
    • ryu.controller.ofp_handler.OFPHandler.switch_features_handler [CONFIG_DISPATCHER]
    • 或到 ryu.controller.dpset.DPSet.switch_features_handler [CONFIG_DISPATCHER]
  • ryu.controller.ofp_handler.OFPHandler.switch_features_handler 发送 OFPPortDescStatsRequest
  • 交换机发送 OFPT_MULTIPART_REPLY/OFPMP_PORT_DESC
  • 接受事件 EventOFPPortDescStatsReply
    • ryu.controller.controller.Datapath._recv_loop
    • ryu.controller.ofp_handler.OFPHandler.multipart_reply_handler [CONFIG_DISPATCHER]
  • 接受事件 EventOFPStateChange
    • ryu.controller.ofp_handler.OFPHandler.multipart_reply_handler [MAIN_DISPATCHER]
    • ryu.controller.dpset.DPSet.dispatcher_change [MAIN_DISPATCHER]
  • 注册 DP 服务:EventDP
    • ryu.controller.dpset.DPSet._register

在这个流程中分别有三个阶段:HANDSHAKE_DISPATCHER/CONFIG_DISPATCHER/MAIN_DISPATCHER,也就是代码中的第二个参数。以及各类事件,对应代码中的第一个参数。

除了上述这三个阶段,还有一个在终止时进入的 DEAD_DISPATCHER 阶段。

因此 set_ev_cls 的行为是:监听 参数2 阶段下的 参数一 事件。

协商 描述
ryu.controller.handler.HANDSHAKE_DISPATCHER 发送并且等待 hello 消息
ryu.controller.handler.CONFIG_DISPATCHER 版本协商和发送功能请求信息
ryu.controller.handler.MAIN_DISPATCHER 收到交换机特性信息并发送设置配置信息
ryu.controller.handler.DEAD_DISPATCHER 与对等设备断开连接。或由于某些无法恢复的错误而断开连接。

不过除了以上的这些事件外,还有另外一类事件定义在 ryu.controller.dpset 下,其用于 DP 服务:

  • EventDP
  • EventDPReconnected
  • EventPortAdd
  • EventPortDelete
  • EventPortModify

思考:所以 OpenFlow 实际上仍算是一个应用层协议?假设交换机和控制器间通过 TCP 进行通信,那么这或许也说明该协议的工作层级并不低,属于应用层的范畴,可以直接在设备的网卡上捕获的完整的流量?(或许,这仅为笔者的思考)

Datapath

在本文案例中,来自于:

1
datapath = ev.msg.datapath

ryu.controller.controller.Datapath

该实例用来描述 OpenFlow 交换机的类,它的实例包含下面属性。

Attribute Description
id 64 位 OpenFlow Datapath ID。仅适用于 ryu.controller.handler.MAIN_DISPATCHER 阶段。
ofproto 针对协商的 OpenFlow 版本导出 OpenFlow 定义(主要是规范中出现的常量)的模块。例如,ryu.ofproto.ofprot_v1_0 用于 OpenFlow 1.0。
ofproto_parser 为协商的 OpenFlow 版本导出 OpenFlow 线报文编码器和解码器的模块。例如,用于 OpenFlow 1.0 的 ryu.ofproto.ofprot_v1_0_parser。
ofproto_parser.OFPxxxx(datapath,…) 用于为给定交换机准备 OpenFlow 报文的可调用程序。xxxx 是报文名称。例如,OFPFlowMod 表示 flow-mod 消息。参数取决于报文。
set_xid(self, msg) 生成 OpenFlow XID 并将其放入 msg.xid。
send_msg(self, msg) 将 OpenFlow 报文排队发送到相应的交换机。如果 msg.xid 为 None,则会在队列之前自动调用报文的 set_xid。
send_packet_out deprecated
send_flow_mod deprecated
send_flow_del deprecated
send_delete_all_flows deprecated
send_barrier 排队向交换机发送 OpenFlow 屏障报文。
send_nxt_set_flow_format deprecated
is_reserved_port deprecated
简单来说,DataPath 实例包含了一个来自交换机事件的所有必要信息。

ofproto_parser

相关的代码定义在 ryu.ryu.ofproto.ofproto_v1_3_parser.py 下,后面的具体版本取决于所用的 OF 版本,这里笔者以 1.3 为例。

该成员下定义了具体的报文构造函数,由于数量众多,这里就只列了前 16 个,实际上不止这些:

  • OFPHello - 开始连接时,交换机和控制器之间会交换 hello 信息
  • OFPErrorMsg - 交换机通过此信息通知控制器出现问题
  • OFPEchoRequest -  Echo request message
  • OFPEchoReply - Echo reply message
  • OFPExperimenter - Experimenter extension message
  • OFPSwitchFeatures - 交换机用功能回复信息响应功能请求
  • OFPGetConfigReply - 交换机通过获取配置回复信息来响应配置请求
  • OFPPacketIn - 交换机通过此报文将收到的数据包发送给控制器
  • OFPFlowRemoved - 当流量条目超时或被删除时,交换机会通过此信息通知控制器
  • OFPPortStatus - 交换机通知控制器端口的更改
  • OFPFlowMod - 控制器发送此信息以修改流量表
  • OFPMultipartReply
  • OFPBarrierReply - 交换机会用此信息响应屏障请求
  • OFPQueueGetConfigReply - 交换机通过此信息响应队列配置请求
  • OFPRoleReply - 交换机用此信息响应角色请求
  • OFPGetAsyncReply - 交换机以此信息响应获取异步配置请求

不过大致上是这样划分的:

  • Controller-to-Switch消息:SDN控制器主动发送给OpenFlow交换机的消息
    • Features:用于获取交换机特性
    • Configuration:用来配置和查询交换机参数
    • Modify-State:用来修改交换机状态信息(增删改流表项、组表项等)
      • Table-Mod消息
      • Flow-Mod消息(流表操作,添加、删除、修改流表项)
      • Group-Mod消息
      • Port-Mod消息
      • Meter-Mod消息
    • Read-State:用来读取交换机状态信息(当前配置、统计信息等)
      • Port-Stats消息
      • Flow-Stats消息
    • Packet-Out:用来指定交换机将数据包从指定端口转发出去
    • Barrier:在不同消息之间使用,确保操作顺序执行
    • Role Request:控制器用于询问或设置自身在交换机中的角色,常用于交换机与多控制器连接的场景
    • Asynchronous-Configuration:控制器设置异步消息过滤器,只接收感兴趣的异步消息,一般在多控制器场景下使用
  • Asynchronous(异步)消息:OpenFlow交换机主动发送给SDN控制器的异步消息
    • Packet-In:将数据包交给控制器处理,一般流表匹配中出现Table-Miss时或流表项显示指定将数据包交给控制器时,触发该消息
    • Flow-Removed:通知控制器,流表项被删除;流表项超时或控制器删除流表项时触发该消息(需要在交换机配置时使能该消息)
    • Port-status :通知控制器,交换机端口状态发生变化
    • Role-status:通知控制器,控制器在交换机中的角色发送变化
    • Controller-Status:通知控制器,OpenFlow通道状态发生变化
    • Flow-monitor:通知控制器,流表发送变化
  • Symmetric(对称)消息:可由SDN控制器或OpenFlow交换机主动发送的消息
    • Hello:建立控制器与交换机之间的OpenFlow通道
    • Echo:检测交换机与控制器之间的连接状态或测量OpenFlow通道的时延和带宽
    • Error:用于通告错误
    • Experiment:用于实验,测试新特性

除此之外,还包括了一些其他数据的构造类,比如 OFPMatch 用以构造流表的匹配结构;或是 OFPInstruction 用于构造 OF 指令结构。

在本文案例代码中,通过如下代码:

1
2
3
4
5
      parser = datapath.ofproto_parser
match = parser.OFPMatch()
actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
ofproto.OFPCML_NO_BUFFER)]
self.add_flow(datapath, 0, match, actions)

分别构造了一个空流表,这意味着它会匹配到任何(Any)项;以及一个输出行为 OFPActionOutput

如何下发流表

代码中定义了如下函数来完成一个流表的下发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def add_flow(self, datapath, priority, match, actions, buffer_id=None):
ofproto = datapath.ofproto
parser = datapath.ofproto_parser

inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
actions)]
if buffer_id:
mod = parser.OFPFlowMod(datapath=datapath, buffer_id=buffer_id,
priority=priority, match=match,
instructions=inst)
else:
mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
match=match, instructions=inst)
datapath.send_msg(mod)

假设我们使用 TCP 的方式建立连接,那么这个函数的行为就相当朴素了。通过构造一份 OFPFlowMod 报文,并通过常规的 TCP 请求包发送给交换机即可完成下发。

这里附带一份常规报文的基本格式:

思考:不过这里或许也会存在一定的安全问题?比如说通过 TCP 连接的信道是没有相关的鉴权和身份验证的。尽管这份连接往往必须由交换机发出,即攻击者一般不能决定哪些设备能够访问交换机,但是一旦信道建立完毕,后续在这个信道上通信的报文都会被认为是合法的,因此如果能有办法劫持信道,或许有办法干扰交换机的工作。

处理 pack in

上文中,我们为交换机添加的默认流表会使得所有发往交换机的报文都被转发给控制器。

根据一些书籍或资料表示,交换机转发给控制器的报文分为 有缓存无缓存。前者只会转发报文的 header ,而不会将报文的具体内容一并转发,而后者则两者都会转发。不过在 2014 年的时候,还因为 Open vSwitch 的一些 bug(网上的表述为”臭虫“) 导致所有的报文都会被完全转发给控制器。现在或许已经修好了?

以下是完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def _packet_in_handler(self, ev):
# If you hit this you might want to increase
# the "miss_send_length" of your switch
if ev.msg.msg_len < ev.msg.total_len:
self.logger.debug("packet truncated: only %s of %s bytes",
ev.msg.msg_len, ev.msg.total_len)
msg = ev.msg
datapath = msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
in_port = msg.match['in_port']

pkt = packet.Packet(msg.data)
eth = pkt.get_protocols(ethernet.ethernet)[0]

if eth.ethertype == ether_types.ETH_TYPE_LLDP:
# ignore lldp packet
return
dst = eth.dst
src = eth.src

dpid = format(datapath.id, "d").zfill(16)
self.mac_to_port.setdefault(dpid, {})

self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)

# learn a mac address to avoid FLOOD next time.
self.mac_to_port[dpid][src] = in_port

if dst in self.mac_to_port[dpid]:
out_port = self.mac_to_port[dpid][dst]
else:
out_port = ofproto.OFPP_FLOOD

actions = [parser.OFPActionOutput(out_port)]

# install a flow to avoid packet_in next time
if out_port != ofproto.OFPP_FLOOD:
match = parser.OFPMatch(in_port=in_port, eth_dst=dst, eth_src=src)
# verify if we have a valid buffer_id, if yes avoid to send both
# flow_mod & packet_out
if msg.buffer_id != ofproto.OFP_NO_BUFFER:
self.add_flow(datapath, 1, match, actions, msg.buffer_id)
return
else:
self.add_flow(datapath, 1, match, actions)
data = None
if msg.buffer_id == ofproto.OFP_NO_BUFFER:
data = msg.data

out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
in_port=in_port, actions=actions, data=data)
datapath.send_msg(out)

先解释一下相关变量:

  • dpid - 每个交换机独有的位移 ID,在连接控制器时分配
  • mac_to_port - 控制器维护了一张 mac-port 表,用于记录每个 mac 地址与端口的对应关系
  • buffer_id - 在没有上传完整报文时,该变量标识的报文在交换机下缓冲区的位置
  • OFPP_FLOOD - 泛洪/广播。意味着将报文广播到除了入端口外的所有端口

主要的代码逻辑如下:

  • 接到 Packet in 事件后解析相关参数
  • 过滤掉 LLDP 流量
  • 将输入端口记录到该交换机的 mac_to_port[dpid][src]
  • 判断目标 mac 地址是否已经有 mac_to_port[dpid][dst] 条目
    • 如果有,那么出端口就是该条目的值
    • 否则,出端口为广播端口
  • 判断出端口是否为广播端口
    • 如果不是,构造流表,再判断是否上传了完整报文
      • 如果上传了报文,那么下发流表,并直接返回
      • 否则,先下发流表,等待后续重新回传数据包
  • 判断数据包是否被全部上传
    • 如果是,那么设置 data 为 msg.data
  • 构造 PacketOut 事件作为输出报文,并设置对应成员、
  • 发回交换机

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# coding=UTF-8
from mininet.net import Mininet
from mininet.node import CPULimitedHost
from mininet.link import TCLink
from mininet.node import RemoteController, Host, OVSKernelSwitch
import TokaSdn
net = Mininet(host=CPULimitedHost, link=TCLink)
# 创建网络节点
c0 = net.addController(name='c0',controller=RemoteController,protocol='tcp',port=6633)
h1 = net.addHost('h1',mac='00:00:00:00:00:01')
h2 = net.addHost('h2')
h3 = net.addHost('h3',mac='00:00:00:00:00:03')
s1 = net.addSwitch('s1')
s2 = net.addSwitch('s2')
s3 = net.addSwitch('s3')
# 创建节点间的链路
net.addLink(h1, s1, bw=10, delay='5ms',max_queue_size=1000, loss=0, use_htb=True)
net.addLink(h2, s2)
net.addLink(h3, s3)
net.addLink(s1, s2)
net.addLink(s2, s3)
# 配置主机 ip
h1.setIP('10.0.0.1', 24)
h2.setIP('10.0.0.2', 24)
h3.setIP('10.0.0.3', 24)
net.start()
net.ping((h1,h3))
net.stop()

这里我们限定使用 TCP 完成连接,并指定了控制器,ryu 默认会启动在 6633 端口上,未指定地址时默认本地。同时,我们令主机 h1 去尝试访问 h3,并通过日志来观察控制器行为。

日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
packet in 0000000000000001 00:00:00:00:00:01 ff:ff:ff:ff:ff:ff 1
{'0000000000000002': {'92:83:5b:9d:3d:62': 3}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1}}
packet in 0000000000000002 00:00:00:00:00:01 ff:ff:ff:ff:ff:ff 2
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1}}
packet in 0000000000000003 00:00:00:00:00:01 ff:ff:ff:ff:ff:ff 2
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1}, '0000000000000003': {'00:00:00:00:00:01': 2}}
packet in 0000000000000003 00:00:00:00:00:03 00:00:00:00:00:01 1
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1}, '0000000000000003': {'00:00:00:00:00:01': 2, '00:00:00:00:00:03': 1}}
add flow be called!
packet in 0000000000000002 00:00:00:00:00:03 00:00:00:00:00:01 3
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2, '00:00:00:00:00:03': 3}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1}, '0000000000000003': {'00:00:00:00:00:01': 2, '00:00:00:00:00:03': 1}}
add flow be called!
packet in 0000000000000001 00:00:00:00:00:03 00:00:00:00:00:01 2
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2, '00:00:00:00:00:03': 3}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1, '00:00:00:00:00:03': 2}, '0000000000000003': {'00:00:00:00:00:01': 2, '00:00:00:00:00:03': 1}}
add flow be called!
packet in 0000000000000001 00:00:00:00:00:01 00:00:00:00:00:03 1
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2, '00:00:00:00:00:03': 3}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1, '00:00:00:00:00:03': 2}, '0000000000000003': {'00:00:00:00:00:01': 2, '00:00:00:00:00:03': 1}}
add flow be called!
packet in 0000000000000002 00:00:00:00:00:01 00:00:00:00:00:03 2
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2, '00:00:00:00:00:03': 3}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1, '00:00:00:00:00:03': 2}, '0000000000000003': {'00:00:00:00:00:01': 2, '00:00:00:00:00:03': 1}}
add flow be called!
packet in 0000000000000003 00:00:00:00:00:01 00:00:00:00:00:03 2
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2, '00:00:00:00:00:03': 3}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1, '00:00:00:00:00:03': 2}, '0000000000000003': {'00:00:00:00:00:01': 2, '00:00:00:00:00:03': 1}}
add flow be called!
packet in 0000000000000003 00:00:00:00:00:03 33:33:ff:00:00:03 1
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2, '00:00:00:00:00:03': 3}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1, '00:00:00:00:00:03': 2}, '0000000000000003': {'00:00:00:00:00:01': 2, '00:00:00:00:00:03': 1}}
packet in 0000000000000003 9a:45:4b:da:a9:ba 33:33:00:00:00:16 2
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2, '00:00:00:00:00:03': 3}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1, '00:00:00:00:00:03': 2}, '0000000000000003': {'00:00:00:00:00:01': 2, '00:00:00:00:00:03': 1, '9a:45:4b:da:a9:ba': 2}}
packet in 0000000000000002 00:00:00:00:00:03 33:33:ff:00:00:03 3
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2, '00:00:00:00:00:03': 3}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1, '00:00:00:00:00:03': 2}, '0000000000000003': {'00:00:00:00:00:01': 2, '00:00:00:00:00:03': 1, '9a:45:4b:da:a9:ba': 2}}
packet in 0000000000000001 00:00:00:00:00:03 33:33:ff:00:00:03 2
{'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2, '00:00:00:00:00:03': 3}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1, '00:00:00:00:00:03': 2}, '0000000000000003': {'00:00:00:00:00:01': 2, '00:00:00:00:00:03': 1, '9a:45:4b:da:a9:ba': 2}}

不过这个日志读起来也挺麻烦的,这里简单过一遍行为。

  • 首先由于目前没有任何其他流表,h1 发出的数据包会被直接转发给控制器,此时的目标 mac 是广播地址 ff:ff:ff:ff:ff:ff
    • packet in 0000000000000001 00:00:00:00:00:01 ff:ff:ff:ff:ff:ff 1
  • 控制器记录了交换机 s1 下 h1 主机所连接的端口
  • 然后这个广播被 s2 接受到,由于仍然没有匹配的 mac 地址,继续广播
    • packet in 0000000000000002 00:00:00:00:00:01 ff:ff:ff:ff:ff:ff 2
  • 由于 s3 下也没有对应地址,继续广播
    • packet in 0000000000000003 00:00:00:00:00:01 ff:ff:ff:ff:ff:ff 2
  • 当 h3 主机接收到该广播后,向 h1 回复,此时交换机 s3 会接受报文
    • packet in 0000000000000003 00:00:00:00:00:03 00:00:00:00:00:01 1
  • 此时交换机的 mac_to_port 会记录下来相关条目数据,并下发流表
    • {'0000000000000002': {'92:83:5b:9d:3d:62': 3, '00:00:00:00:00:01': 2}, '0000000000000001': {'92:83:5b:9d:3d:62': 2, '00:00:00:00:00:01': 1}, '0000000000000003': {'00:00:00:00:00:01': 2, '00:00:00:00:00:03': 1}}
    • 流表匹配项包括入端口、目标 mac、源 mac,在三者都匹配时转发到 out_port 端口
  • 同样,交换机 s3 根据先前的路径访问 s2,由于已经有过记录,因此可以下发流表
  • s2 回复 s1 的路径也同样下发流表
  • 最后 h3 又完成了一次 ping h1,但这次通过流表完成,很多日志就不需要打印了。

不过需要注意一点,有些日志可能不会被打印,因此这个逻辑大致如此,但实际上有些区别。
我们下发的流表中优先级是大于默认流表的,而只有默认流表会打印日志,因此有一部分流量通过我们下发的流表完成转发时不会再有日志输出。

案例:simple_switch_13.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# Copyright (C) 2011 Nippon Telegraph and Telephone Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
from ryu.lib.packet import ether_types


class SimpleSwitch13(app_manager.RyuApp):
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]

def __init__(self, *args, **kwargs):
super(SimpleSwitch13, self).__init__(*args, **kwargs)
self.mac_to_port = {}

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

# install table-miss flow entry
#
# We specify NO BUFFER to max_len of the output action due to
# OVS bug. At this moment, if we specify a lesser number, e.g.,
# 128, OVS will send Packet-In with invalid buffer_id and
# truncated packet data. In that case, we cannot output packets
# correctly. The bug has been fixed in OVS v2.1.0.
match = parser.OFPMatch()
actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
ofproto.OFPCML_NO_BUFFER)]
self.add_flow(datapath, 0, match, actions)

def add_flow(self, datapath, priority, match, actions, buffer_id=None):
ofproto = datapath.ofproto
parser = datapath.ofproto_parser

inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
actions)]
if buffer_id:
mod = parser.OFPFlowMod(datapath=datapath, buffer_id=buffer_id,
priority=priority, match=match,
instructions=inst)
else:
mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
match=match, instructions=inst)
datapath.send_msg(mod)

@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def _packet_in_handler(self, ev):
# If you hit this you might want to increase
# the "miss_send_length" of your switch
if ev.msg.msg_len < ev.msg.total_len:
self.logger.debug("packet truncated: only %s of %s bytes",
ev.msg.msg_len, ev.msg.total_len)
msg = ev.msg
datapath = msg.datapath
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
in_port = msg.match['in_port']

pkt = packet.Packet(msg.data)
eth = pkt.get_protocols(ethernet.ethernet)[0]

if eth.ethertype == ether_types.ETH_TYPE_LLDP:
# ignore lldp packet
return
dst = eth.dst
src = eth.src

dpid = format(datapath.id, "d").zfill(16)
self.mac_to_port.setdefault(dpid, {})

self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)

# learn a mac address to avoid FLOOD next time.
self.mac_to_port[dpid][src] = in_port

if dst in self.mac_to_port[dpid]:
out_port = self.mac_to_port[dpid][dst]
else:
out_port = ofproto.OFPP_FLOOD

actions = [parser.OFPActionOutput(out_port)]

# install a flow to avoid packet_in next time
if out_port != ofproto.OFPP_FLOOD:
match = parser.OFPMatch(in_port=in_port, eth_dst=dst, eth_src=src)
# verify if we have a valid buffer_id, if yes avoid to send both
# flow_mod & packet_out
if msg.buffer_id != ofproto.OFP_NO_BUFFER:
self.add_flow(datapath, 1, match, actions, msg.buffer_id)
return
else:
self.add_flow(datapath, 1, match, actions)
data = None
if msg.buffer_id == ofproto.OFP_NO_BUFFER:
data = msg.data

out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
in_port=in_port, actions=actions, data=data)
datapath.send_msg(out)

参考

「OpenFlow」协议入门

写的很详细,本文部分图片来自本文,还有一些分类内容也摘自本文

SDN学习之实现环路通信

参考了本文的代码分析,并额外做了补充

Ryu学习一

给出了一部分 api 的表格,本文中的表格取自本文

ryu 文档

文档,但大多时候不如直接看代码