使用 POX 的主要目的之一是开发 OpenFlow 控制应用程序 - 也就是说,POX 充当 OpenFlow 交换机的控制器。
由于 POX 经常与 OpenFlow 一起使用,因此有一种特殊的需求加载机制,它通常会检测你何时尝试使用 OpenFlow,并使用默认值加载与 OpenFlow 相关的组件。如果需求加载没有检测到你正在尝试使用它,你可以调整组件以明确你要使用它(只需在启动函数中访问 core.openflow),或者只需指定在命令行指定以 “openflow” 开头的组件。
POX OpenFlow API 的主要部分是 OpenFlow “nexus” 对象。通常,有一个这样的注册为 core.openflow 的对象作为上述需求加载过程的一部分。
实际与 OpenFlow 交换机通信的 POX 组件是 openflow.of_01(01 表示该组件使用 OpenFlow 1.0 版本)。需求加载功能通常会使用默认值初始化此组件(侦听端口6633)。但是同样地,你可以自动调用它来更改选项,或者多次运行它(例如,侦听普通 TCP 和 SSL 或多个端口)。
DPID
OpenFlow 规范指定每个数据路径(交换机)都具有唯一的数据路径 ID(DPID),它有 64 位,并且在握手期间通过 ofp_switch_features 消息从交换机传送到控制器。48 位是以太网地址,16 位是“实现者定义的”(通常是零)。由于 OpenFlow Connection 对象(下面讨论)绑定到特定交换机,因此使用 .dpid 属性时在 Connection 对象上可以使用 DPID,使用 .eth_addr 属性时可以使用相应的以太网地址。
POX 内部将 DPID 视为 Python 整数类型。不过这对我们来说并不好。如果将它们打印出来,它们只是一个十进制数,可能不容易查看或与以太网地址相关联。因此,POX 定义了格式化 DPID 的特定方式,该方法在 pox.lib.util.dpid_to_str()
中实现。在 16 个“实现者定义”位为 0 的常见情况下传递 DPID 时,DPID 值是一个看起来非常像以太网地址的字符串,除了用破折号代替冒号作为分隔符。如果实现者定义的位非零,则将它们视为十进制数,并附加在上述字符串之后,例如 “00-00-00-00-00-05 | 123”。
默认情况下,Mininet 以直接的方式为交换机分配 DPID。如果一个交换机是 “s3”,那么它的 DPID 将是 3。当与 --mac
选项一起使用时,这可能会有问题。--mac
选项以相同的方式为主机分配 MAC 地址。如果主机为 “h3”,则其 MAC 将为 00:00:00:00:00:03,与 “s3” 的 MAC 地址相同。这是混淆和问题的根源,因为 MAC 通常被认为应该是唯一的。
与数据路径(交换机)通信
当通信是从控制器到交换机时,控制器代码将 OpenFlow 消息发送到特定交换机(稍后会详细介绍)。当消息来自交换机时,它们在 POX 中表示为“事件”,你可以为它编写事件处理程序——通常会有一个与交换机可能发送的每种消息类型相对应的事件类型。
在 POX 中,你可以通过两种方式与数据路径进行通信:通过该特定数据路径的 Connection 对象,或通过管理该数据路径的 OpenFlow Nexus。连接到 POX 的每个数据路径都有一个 Connection 对象,通常有一个 OpenFlow Nexus 管理所有连接。在正常配置中,有一个 OpenFlow 连接,通过 core.openflow 可以获取。Connections 和 Nexus 之间有很多重叠。任何一个都可用于向交换机发送消息,并且大多数事件都在两者上引发。有时使用其中一个更方便。如果你的应用程序对所有交换机的事件感兴趣,那么收听
Nexus 可能是有意义的,这会引发所有交换机的事件。如果你只对一个交换机感兴趣,那么收听特定连接可能是有意义的。
Connection 对象
每次交换机连接到 POX 时,都会有一个关联的 Connection 对象。 如果您的代码具有对该 Connection 对象的引用,则可以使用其 send()
方法将消息发送到数据路径。
Connection 对象能够将命令发送到交换机并作为交换机事件的源,还具有许多其他有用的属性。我们在这里列出一些:
成员 | 描述 |
---|---|
dpid | 交换机的数据路径标识符 |
ofnexus | 对与此连接关联的 nexus 对象的引用 |
ports | 交换机上的端口。由于这些可能在连接的生命周期内发生变化,POX 会尝试跟踪此类更改。但是总有可能这些已经过时。此属性是对特殊 PortCollection 对象的引用。这个对象有点像字典,其中值是 p_phy_port 对象,键是灵活的_可以通过 OpenFlow 端口号(ofp_phy_port 的 .port_no)、它们的以太网地址(ofp_phy_port 的 .hw_addr)或它们的端口名称(ofp_phy_port 的 .name,例如 “eth0”)查找端口。 |
send(msg) | 一种用于向交换机发送 OpenFlow 消息的方法。 |
sock | 连接到对等的套接字。这是一个 Python 套接字对象,因此您可以使用 connection.sock.getpeername() 检索连接的交换机端的地址。 |
除了其属性和 send()
方法之外,Connection 对象还会引发与特定数据路径相对应的事件,例如当数据路径断开连接或发送通知时。你可以通过在关联的 Connection 上注册事件侦听器来为特定数据路径上的事件创建处理程序。
获取对 Connection 对象的引用
如果希望使用任何上述 Connection 对象的属性,需要引用与你感兴趣的数据路径相关联的 Connection 对象。有三种主要方法可以获得这个引用:
- 在 nexus 上侦听
ConnectionUp
事件——这些事件将新的 Connection 对象传递进来; - 使用 nexus 的
getConnection(
方法通过交换机的 DPID 查找连接;) - 通过 nexus 的 connections 属性枚举所有的连接。
第一种方法中,你可以在自己的组件类中使用代码来跟踪连接并存储对它们的引用。它通过在 OpenFlow 连接上侦听 ConnectionUp 事件来完成此操作。此事件包括对新连接的引用,该连接将添加到其自己的连接集中。以下代码演示了这一点(注意,更完整的实现是使用 ConnectionDown 事件从集合中删除关闭的 connection)。
class MyComponent (object):
def __init__ (self):
self.connections = set()
core.openflow.addListeners(self)
def _handle_ConnectionUp (self, event):
self.connections.add(event.connection) # See ConnectionUp event documentation
OpenFlow Nexus:core.openflow
OpenFlow 连接本质上是一组 OpenFlow 连接的管理器。通常,有一个管理所有交换机的连接的 nexus,通过 core.openflow 来使用。
在这里,我们列出了一个 nexus 的一些属性:
属性 | 描述 |
---|---|
miss_send_len | 当数据包与交换机上的任何流表都不匹配时,交换机将在 packet-in 消息中将数据包转发到控制器。为了节省带宽,交换机实际上不会发送整个数据包,而只会发送前 miss_send_len 字节。通过在此处调整此值,随后连接的任何交换机将配置为仅发送此字节数。默认值为 128 字节。 |
clear_flows_of_connect | 当为 True(默认值)时,POX 将删除新连接的交换机第一个表上的所有流。 |
connections | 一个特殊的集合(见下文),包含此 nexus 正在处理的所有连接的引用。 |
getConnection(<dpid>) | 通过交换机的 DPID 获取其 Connection 对象。 |
sendToDPID(<dpid>, <msg>) | 向特定的交换机发送 OpenFlow 消息,如果交换机未连接,丢弃该消息(并记录一条 warning)。类似于执行 core.openflow.getConnection(dpid).send(msg) 。 |
connections 集合本质上是一个字典,其中键是 DPID,值是 Connection 对象。但是如果执行迭代操作,它将迭代 Connections 而不是 DPID,这与普通字典不同。要迭代 DPID,可以使用 .iter_dpids()
方法。此外,您可以使用 “in” 运算符来检查 Connection 是否在此集合中以及 DPID 是否在集合中,并且 .dpids()
属性与 .keys()
实际上相同。
与 Connection 对象一样,也可以在 nexus 对象本身上设置事件侦听器。Connection 对象仅引发与该特定 Connection 关联的数据路径相关的事件,而 nexus 对象会引发与其管理的任何连接相关的事件。
OpenFlow 事件:响应交换机
大多数 OpenFlow 相关事件都是由直接响应从交换机收到的消息而引发的。一般 OpenFlow 相关事件具有以下三个属性:
属性 | 类型 | 描述 |
---|---|---|
connection | Connection | 到相关交换机的连接 |
dpid | long | 相关交换机的数据路径 ID(使用 dpid_to_str() 将其格式化以便显示)。 |
ofp | ofp_header subclass | 产生此事件的 OpenFlow 消息对象。 |
接下来介绍一些 OpenFlow 模块和拓扑模块提供的一些事件。下面是一个非常简单的 POX 组件,它侦听来自所有交换机的 ConnectionUp
事件,并在发生消息时记录消息。你可以将其放入一个文件(例如 ext/connection_watcher.py),然后运行它(使用 ./pox.py connection_watcher
)并观察交换机连接。
from pox.core import core
from pox.lib.util import dpid_to_str
log = core.getLogger()
class MyComponent (object):
def __init__ (self):
core.openflow.addListeners(self)
def _handle_ConnectionUp (self, event):
log.debug("Switch %s has come up.", dpid_to_str(event.dpid))
def launch ():
core.registerNew(MyComponent)
ConnectionUp
此事件因建立与交换机的新控制信道而触发。
另请注意,虽然大多数 OpenFlow 事件在 Connection 和 OpenFlow nexus 上都会触发,但 ConnectionUp 事件仅在 nexus 上触发。这是有道理的,因为 ConnectionUp 事件后 Connection 才存在。
附加属性信息(除标准 OpenFlow 事件属性外):
属性 | 类型 | 描述 |
---|---|---|
ofp | ofp_switch_features | 包含有关交换机的信息,例如支持的操作类型(例如字段重写是否可用)和端口信息(例如 MAC 地址和名称)(这也可以在 Connection 的 features 属性中实现)。 |
此事件可以按如下所示进行处理:
def _handle_ConnectionUp (self, event):
print "Switch %s has come up." % event.dpid
ConnectionDown
与 ConnectionUp 不同,此事件在 nexus 和 Connection 本身都会引发。此事件没有 .ofp 属性。
PortStatus
当控制器从交换机收到 OpenFlow 端口状态消息(ofp_port_status)时,会引发 PortStatus 事件,这表示端口已更改。因此,其 .ofp 属性是 ofp_port_status。
class PortStatus (Event):
def __init__ (self, connection, ofp):
Event.__init__(self)
self.connection = connection
self.dpid = connection.dpid
self.ofp = ofp
self.modified = ofp.reason == of.OFPPR_MODIFY
self.added = ofp.reason == of.OFPPR_ADD
self.deleted = ofp.reason == of.OFPPR_DELETE
self.port = ofp.desc.port_no
小例子:
def _handle_PortStatus (self, event):
if event.added:
action = "added"
elif event.deleted:
action = "removed"
else:
action = "modified"
print "Port %s on Switch %s has been %s." % (event.port, event.dpid, action)
FlowRemoved
当控制器从交换机接收到 OpenFlow 流删除消息(ofp_flow_removed)时,会引发 FlowRemoved 事件,这些消息是由于超时或显式删除而在交换机上删除表条目时发送的。只有在该流设置了 OFPFF_SEND_FLOW_REM 标志时才会发送此类通知。
虽然你可以像往常一样通过事件的 .ofp 属性直接访问 ofp_flow_removed,但为方便起见,该事件有几个属性:
attribute | type | meaning |
---|---|---|
idleTimeout | bool | 由于闲置(没有流量匹配的时间超过设定值)而被删除 |
hardTimeout | bool | 由于硬超时(被插入表中的总时间超过设定值)而被删除 |
timeout | bool | 由于无论何种超时而被删除 |
deleted | bool | 被显式删除 |
统计事件
当控制器从交换机接收到 OpenFlow 统计回复消息(ofp_stats_reply/OFPT_STATS_REPLY)时,会引发统计事件,该消息是响应控制器发送的统计请求的。
PacketIn
当控制器从交换机收到 OpenFlow 数据包输入消息(ofp_packet_in/OFPT_PACKET_IN)时触发,指示到达交换机端口的数据包未能匹配,或者匹配条目的操作指定将数据包发送到控制器。
除了通常的 OpenFlow 事件属性外:
- port(int)——数据包进入的端口号
- data(字节)——原始分组数据
- parsed(数据包子类)——pox.lib.packet 的解析版本
- ofp(ofp_packet_in)——导致此事件的 OpenFlow 消息
ErrorIn
BarrierIn
OpenFlow 消息
POX 包含与 OpenFlow 协议元素对应的类和常量,这些在文件 pox/openflow/libopenflow_01.py 中定义。
ofp_packet_out:从交换机发送数据包
此消息的主要目的是指示交换机发送数据包(或将其排入队列)。它也可以用于指示交换机丢弃缓冲分组(不指定任何动作即可)。
ofp_flow_mod:流表修改(重要!)
- cookie(int):此流规则的标识符(可选);
- command(int):以下值之一:
- OFPFC_ADD:向数据路径添加规则(默认);
- OFPFC_MODIFY:修改任何匹配规则;
- OFPFC_MODIFY_STRICT:修改严格匹配通配符值的规则;
- OFPFC_DELETE:删除任何匹配的规则;
- OFPFC_DELETE_STRICT:删除严格匹配通配符值的规则。
- idle_timeout(int):如果在 “idle_timeout” 秒内未匹配,则规则将过期。值 OFP_FLOW_PERMANENT(默认)表示没有 idle_timeout;
- hard_timeout(int):规则将在 'hard_timeout' 秒后过期。值 OFP_FLOW_PERMANENT(默认)表示它永不过期;
- priority(int):规则匹配的优先级,数字越大优先级越高。注意:完全匹配将具有最高优先级;
- buffer_id(int):新流将应用的数据路径上的缓冲区。没有就设成 None。对流删除没有意义;
- out_port(int):该字段用于匹配 DELETE 命令。OFPP_NONE 可用于表示没有限制;
- flags(int):可以设置以下标志位的整数位域:
- OFPFF_SEND_FLOW_REM:当规则到期时,将流删除的消息发送到控制器;
- OFPFF_CHECK_OVERLAP:安装时检查重叠条目。如果存在,则向控制器发送错误;
- OFPFF_EMERG:将此流程视为紧急流程,仅在交换机控制器连接断开时使用它。
- actions(list):动作定义如下,每个所需的动作对象随后被附加到该列表中,并按顺序执行;
- match(ofp_match):匹配规则的匹配结构(见下文)。
【例子:安装表条目】
# Traffic to 192.168.101.101:80 should be sent out switch port 4
# One thing at a time...
msg = of.ofp_flow_mod()
msg.priority = 42
msg.match.dl_type = 0x800 # 以太网协议类型
msg.match.nw_dst = IPAddr("192.168.101.101")
msg.match.tp_dst = 80
msg.actions.append(of.ofp_action_output(port = 4))
self.connection.send(msg)
# Same exact thing, but in a single line...
self.connection.send( of.ofp_flow_mod( action=of.ofp_action_output( port=4 ),
priority=42,
match=of.ofp_match( dl_type=0x800,
nw_dst="192.168.101.101",
tp_dst=80 )))
【例子:清除所有交换机上的表】
# create ofp_flow_mod message to delete all flows
# (note that flow_mods match all flows by default)
msg = of.ofp_flow_mod(command=of.OFPFC_DELETE)
# iterate over all connected switches and delete all their flows
for connection in core.openflow.connections: # _connections.values() before betta
connection.send(msg)
log.debug("Clearing all flows from %s." % (dpidToStr(connection.dpid),))
ofp_stats_request:请求来自交换机的统计信息
【例子:Web 流量统计】
从交换机请求流表并转储有关 Web 流量的信息。此例与 forwarding.l2_learning 组件一起运行,可以粘贴到 POX 交互式解释器中。
import pox.openflow.libopenflow_01 as of
log = core.getLogger("WebStats")
# When we get flow stats, print stuff out
def handle_flow_stats (event):
web_bytes = 0
web_flows = 0
for f in event.stats:
if f.match.tp_dst == 80 or f.match.tp_src == 80:
web_bytes += f.byte_count
web_flows += 1
log.info("Web traffic: %s bytes over %s flows", web_bytes, web_flows)
# Listen for flow stats
core.openflow.addListenerByName("FlowStatsReceived", handle_flow_stats)
# Now actually request flow stats from all switches
for con in core.openflow.connections: # make this _connections.keys() for pre-betta
con.send(of.ofp_stats_request(body=of.ofp_flow_stats_request()))
匹配结构
匹配结构在类 ofp_match 中的 pox/openflow/libopenflow_01.py 中定义。ofp_match 属性有:
属性 | 含义 |
---|---|
dl_dst | 以太网目的地址 |
dl_src | 以太网源地址 |
dl_type | 以太网类型 / 长度(e.g. 0x0800 = IPv4) |
dl_vlan | VLAN ID |
dl_vlan_pcp | VLAN 优先级 |
in_port | 数据包到达交换机的端口号 |
nw_dst | IP 目的地址 |
nw_proto | IP 协议(e.g., 6 = TCP)或低八位的 ARP 操作码 |
nw_src | IP 源地址 |
nw_tos | IP TOS/DS 位 |
tp_dst | TCP/UDP 目的端口 |
tp_src | TCP/UDP 源端口 |
部分匹配和通配符
IP 地址字段可以像其他字段一样完全通配,但也可以部分匹配。这允许你匹配整个子网。以下是一些等价的方法:
my_match.nw_src = "192.168.42.0/24"
my_match.nw_src = (IPAddr("192.168.42.0"), 24)
my_match.nw_src = "192.168.42.0/255.255.255.0"
my_match.set_nw_src(IPAddr("192.168.42.0"), 24)
特别要注意的是,在处理部分匹配时,nw_src 和 nw_dst 属性可能不明确,特别是在读取匹配结构时(例如在 flow_removed 消息或 flow_stats 回复中返回)。为了解决这个问题,你可以使用明确的 .get_nw_src()
、.set_nw_src()
,目的地址同理。它们返回一个元组,如 (IPAddr(“192.168.42.0”), 24)。
请注意,某些字段具有先决条件。这意味着您不能指定更高层的字段而不指定相应的下层字段。例如,如果不指定希望匹配 TCP 流量,则无法在 TCP 端口上创建匹配项。并且为了匹配 TCP 流量,您必须指定您希望匹配 IP 流量。例如,仅与 tp_dst = 80 的匹配是无效的。您还必须指定 nw_proto = 6(TCP)和 dl_type = 0x800(IPv4)。如果您违反此规则,则应收到警告消息 “Fields ignored due to unspecified prerequisites(由于未指定的先决条件而忽略字段)”。有关此主题的更多信息,请参阅 FAQ “I tried to install a table entry but got a different one. Why?”
ofp_match 方法
方法 | 描述 |
---|---|
from_packet(packet, in_port=None, spec_frags=False) | 类工厂。 |
clone() | 返回一个此 ofp_match 的副本。 |
flip() | 返回其源和目标相反的副本。 |
show() | 返回一个字符串表示。 |
get_nw_src() | 返回 IP 源地址和匹配位数组成的元组。例如:(IPAddr(“192.168.42.0”, 24)。注意,当第二个为 0 时,元组的第一个元素将为 None。 |
set_nw_src(IP and bits) | 设置 IP 源地址和要匹配的位数。参数可以是两个(一个用于 IP,一个用于位计数),或者是 get_nw_src() 使用的格式化的元组。 |
get_nw_dst() | 与 get_nw_src() 相同,但是是针对目标地址的。 |
set_nw_dst(IP and bits) | 与 set_nw_src() 相同,但是是针对目标地址的。 |
定义现有数据包的匹配项
有一种基于现有数据包对象(即 pox.lib.packet 中的以太网对象)或现有的 ofp_packet_in 创建完全匹配的简单方法。这是使用 ofp_match.from_packet() 工厂方法实现的。
my_match = ofp_match.from_packet(packet, in_port)
packet 参数是解析的数据包或 ofp_packet_in,用于创建匹配。
【例子:匹配 Web 流量】
import pox.openflow.libopenflow_01 as of # POX convention
import pox.lib.packet as pkt # POX convention
my_match = of.ofp_match(dl_type = pkt.ethernet.IP_TYPE,
nw_proto = pkt.ipv4.TCP_PROTOCOL,
tp_dst = 80)
OpenFlow 操作
OpenFlow 操作应用于与数据路径上安装的规则匹配的数据包。这里的代码片段可以在 pox/openflow 的 libopenflow_01.py 中找到。
输出
通过物理或虚拟端口转发数据包。物理端口由其整数值引用,而虚拟端口具有符号名称。物理端口的端口号应小于 0xFF00。
- port(int):此数据包的输出端口。值可以是实际端口号或以下虚拟端口之一:
- OFPP_IN_PORT:发回收到数据包的端口。除了 OFPP_NORMAL 之外,这是将数据包发送回其传入端口的唯一方法;
- OFPP_TABLE:执行流表中指定的操作。注意:仅适用于 ofp_packet_out 消息;
- OFPP_NORMAL:通过正常的 L2 / L3 传统交换机配置进行处理(如果可用的话,取决于交换机);
- OFPP_FLOOD:输出到除输入端口以及通过 OFPPC_NO_FLOOD 端口配置位禁用泛洪的端口之外的所有开放流端口;
- OFPP_ALL:输出除输入端口以外的所有开放流端口;
- OFPP_CONTROLLER:发送到控制器;
- OFPP_LOCAL:输出到本地 openflow 端口;
- OFPP_NONE:不输出。
【例子:发送一个 FlowMod】
msg = ofp_flow_mod()
msg.match = match
msg.idle_timeout = idle_timeout
msg.hard_timeout = hard_timeout
msg.actions.append(of.ofp_action_output(port = port))
msg.buffer_id =
connection.send(msg)
【例子:发送一个 PacketOut】
msg = of.ofp_packet_out(in_port=of.OFPP_NONE)
msg.actions.append(of.ofp_action_output(port = outport))
msg.buffer_id =
connection.send(msg)
交互式交换机会话示例
POX> INFO:openflow.of_01:[00-00-00-00-00-01 1] connected
POX> MySwitch.list_available_listeners()
INFO:samples.of_sw_tutorial_oo:SW_BADSWITCH
INFO:samples.of_sw_tutorial_oo:SW_LAZYHUB
INFO:samples.of_sw_tutorial_oo:SW_PAIRSWITCH
INFO:samples.of_sw_tutorial_oo:SW_IDEALPAIRSWITCH
INFO:samples.of_sw_tutorial_oo:SW_DUMBHUB
INFO:samples.of_sw_tutorial_oo:SW_PAIRHUB
POX> MySwitch.clear_all_flows()
DEBUG:samples.of_sw_tutorial_oo:Clearing all flows from 00-00-00-00-00-01.
POX> MySwitch.detach_packetin_listener()
DEBUG:samples.of_sw_tutorial_oo:Detaching switch SW_IDEALPAIRSWITCH.
POX> MySwitch.attach_packetin_listener('SW_LAZYHUB')
DEBUG:samples.of_sw_tutorial_oo:Attach switch SW_LAZYHUB.
POX> MySwitch.clear_all_flows()
DEBUG:samples.of_sw_tutorial_oo:Clearing all flows from 00-00-00-00-00-01.
POX> MySwitch.detach_packetin_listener()
DEBUG:samples.of_sw_tutorial_oo:Detaching switch SW_LAZYHUB.
POX> MySwitch.attach_packetin_listener('SW_BADSWITCH')
DEBUG:samples.of_sw_tutorial_oo:Attach switch SW_BADSWITCH.
···