ryu 代码案例分析到理解简单 SDN 网络
Last Update:
ryu 作为一个由 python 编写的控制器,其 app 均可直接通过 python 来创建,本文仅做学习之余的备忘录,用以熟悉其开发流程。
一般来说,通过
ryu-manage app.py
即可运行该应用。在需要运行多个应用时可按顺序添加参数。
下文会对此做一些额外的实验(希望)。
前置
ryu 提供了前置的各项环境,在开发应用时,只需要声明一个继承自 app_manager.RyuApp
,该类就会具备各项所需条件和环境而无需再做额外操作:
1 |
|
以上为固定的范式,一般所有的应用都可以这么声明。SimpleSwitch13
为应用名称。
行为
修饰器与事件
在完成应用框架的声明以后,接下来是要定义应用的具体行为。ryu 提供了特定的修饰器 set_ev_cls
以帮助用户解释不同命令的行为模式:
1 |
|
set_ev_cls
的参数分别对应了:
- 所监听的事件名
- 所处的阶段
监听事件名包括了这些内容:
- ryu.controller.ofp_event
- datapath state 通知系
- EventOFPStateChange
- EventOFPMsgBase 基类
- EventOFPHello
- EventOFPSwitchFeatures
- EventOFPPortDescStatsReply
- EventOFPEchoRequest
- EventOFPEchoReply
- EventOFPPortStatus
- EventOFPErrorMsg
- port state 通知系
- EventOFPPortStateChange - 在 EventOFPPortStatus 之后发出
- datapath state 通知系
在一个常规的连接行为下,会按照如下的顺序分阶段完成:
- 控制器与交换机完成 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 |
|
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 |
|
分别构造了一个空流表,这意味着它会匹配到任何(Any)项;以及一个输出行为 OFPActionOutput
。
如何下发流表
代码中定义了如下函数来完成一个流表的下发:
1 |
|
假设我们使用 TCP 的方式建立连接,那么这个函数的行为就相当朴素了。通过构造一份 OFPFlowMod
报文,并通过常规的 TCP 请求包发送给交换机即可完成下发。
这里附带一份常规报文的基本格式:
思考:不过这里或许也会存在一定的安全问题?比如说通过 TCP 连接的信道是没有相关的鉴权和身份验证的。尽管这份连接往往必须由交换机发出,即攻击者一般不能决定哪些设备能够访问交换机,但是一旦信道建立完毕,后续在这个信道上通信的报文都会被认为是合法的,因此如果能有办法劫持信道,或许有办法干扰交换机的工作。
处理 pack in
上文中,我们为交换机添加的默认流表会使得所有发往交换机的报文都被转发给控制器。
根据一些书籍或资料表示,交换机转发给控制器的报文分为 有缓存
和 无缓存
。前者只会转发报文的 header
,而不会将报文的具体内容一并转发,而后者则两者都会转发。不过在 2014 年的时候,还因为 Open vSwitch 的一些 bug(网上的表述为”臭虫“) 导致所有的报文都会被完全转发给控制器。现在或许已经修好了?
以下是完整代码:
1 |
|
先解释一下相关变量:
- 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
- 如果是,那么设置 data 为
- 构造
PacketOut
事件作为输出报文,并设置对应成员、 - 发回交换机
测试
1 |
|
这里我们限定使用 TCP 完成连接,并指定了控制器,ryu 默认会启动在 6633 端口上,未指定地址时默认本地。同时,我们令主机 h1 去尝试访问 h3,并通过日志来观察控制器行为。
日志如下:
1 |
|
不过这个日志读起来也挺麻烦的,这里简单过一遍行为。
- 首先由于目前没有任何其他流表,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 |
|
参考
写的很详细,本文部分图片来自本文,还有一些分类内容也摘自本文
参考了本文的代码分析,并额外做了补充
给出了一部分 api 的表格,本文中的表格取自本文
文档,但大多时候不如直接看代码