分类目录归档:SDN

LoxiGen 与 Indigo 项目介绍

Indigo 是由 Big Switch 开发, 现在托管在 Floodlight 组织下的 OpenFlow agent 开源实现. OpenFlow 控制器的开源实现有很多, 像是 Ryu, ODL, POX 等等, 但是 agent 方面的开源实现可能相对少一些, Indigo 是一个非常精巧的实现, 但是似乎网上 SDN 相关的中文社区对其介绍的文档却很少, 本文将对 Indigo 及其所依附的 LoxiGen 项目做一个简单的介绍.

关于 LOXI 与 Loxigen

LOXI (Logical OpenFlow eXtensible Interface), 是一种描述 OpenFlow 协议的逻辑语言.

LoxiGen 项目能够解读 LOXI 语言, 进而用来生成各种编程语言的 OpenFlow 协议库. 所以说 LoxiGen 实际上是一个 “编译器” 项目, 可想而知 LoxiGen 包含一个前端用来解析 LOXI 语言, 以及包含各种编程语言的后端来生成这些编程语言的代码. 目前包含 Java, Python 以及 C 语言的后端, 生成的 C 版本的协议库叫做 LOCI, Java 版本的叫做 OpenFlowJ, Python 版本的叫做 pyloxi.

LoxiGen is a tool that generate OpenFlow protocol library for a number of languages. It is composed of a frontend that parses wire protocol descriptions and a backend for each supported language (currently C, Python and Java, with an auto-generated wireshark dissector in Lua on the way).

LoxiGen 项目组维护了一个快照, 定期的生成各种语言的协议库, 并将其放在 loxigen-artifacts 仓库下, 这样对于使用者来说只需要定期从 loxigen-artifacts 仓库拿现成的就行了, 连构建都不需要自己构建.

Indigo 项目源码构成

Indigo 项目 (https://github.com/floodlight/indigo) 为我们提供了一些与平台无关的基础功能, 包括:

  • IO 复用与定时器管理框架: 提供了通用的 socket 回调注册机制与定时器回调处理机制, 让我们能够在单一线程中同时处理多个 sockets 的读写以及定时器的回调事件.
  • OpenFlow 连接管理: 维护每一条与控制器之间的连接, 包括连接的建立, 心跳维持, 消息收发以及连接的终止.
  • OpenFlow 状态管理: 包括与控制器之间的各种消息的处理以及交换机状态的上报都会在这里处理
  • 配置模块: 提供了平台无关的配置接口

另外 Indigo 中有一些平台相关的功能是需要厂商自己来实现的, 这包括:

  • Forwarding 模块: 负责实现将控制器下发的流表下发到交换机芯片中的接口
  • Port Manager 模块: 负责实现端口管理的接口

芯片厂商需要负责实现后两个模块, 一般来讲就是将这两个模块中 indigo 所定义的接口用自己芯片的 sdk 来实现. 关于 Indigo 适配有一个很好的例子就是 Broadcom 的 OF-DPA, 我们可以从 Broadcom 的 OF-DPA 项目仓库中获得实现了 Forwarding 和 Port Manager 模块之后的 indigo 代码.

OF-DPA 项目托管在 https://github.com/Broadcom-Switch/of-dpa/, 其对 Indigo 项目做了些许代码上的改动, 从这个地址获得 OF-DPA 的代码, indigo 的代码位于 ofagent/ 目录下:

ofagent/
    application/
    indigo/
    ofdpadriver/

其中 indigo/ 目录中就是 indigo 的源码; ofdpadriver/ 是 OF-DPA 实现好的 Forwarding 和 Port manager 模块的代码, 这两部分会调用芯片 sdk; application/ 目录中只有一个源文件: ofagent.c, 这是一个示例程序, 向我们展示了如何初始化以及启动 indigo.

在 indigo 的项目代码也就是 indigo/ 目录下, 又有两个子目录: modules/submodules/, 其中 modules/ 里面是 indigo 自身的代码, submodules/ 里是 indigo 所用到的非自身的代码, 目前有 bigcode, infra, loxigen-artifacts.

bigcode 和 infra 都是 Floodlight 开发的通用工具库, 另外你可能注意到了 Indigo 使用了 loxigen-artifacts 中的代码. 下面就看一下 indigo 具体是如何使用 loxigen 的.

Indigo 与 LoxiGen

Indigo 使用了 LoxiGen 项目生成的 LOCI 协议库, 并且是直接把源代码拿来使用的. 一开始将其放在 indigo/modules/loci/ 目录下, Indigo 的维护者从 loxigen-artifacts 获得最新的代码, 然后再将其与 modules/loci/ 下原本的 loxigen 项目代码合并或者直接替换. 因为这样每次都需要维护者手动去 pull loxigen-artifacts, 然后与 Indigo 的 codebase 合并, 后来估计是 Indigo 的维护者也觉得这样太笨了, 于是就干脆不往 modules/loci/ 里面合了, 而是直接借助了 git submodule 机制, 把 loxigen-artifacts 用 git pull 拉下来放到了 submodules/loxigen-artifacts/, 然后 modules/loci/make.mk 做了如下的改动, 直接指向 submodules/loxigen-artifacts/.

-loci_INCLUDES := -I $(dir $(lastword $(MAKEFILE_LIST)))inc
-loci_INTERNAL_INCLUDES := -I $(dir $(lastword $(MAKEFILE_LIST)))src
+LOCI := $(SUBMODULE_LOXIGEN_ARTIFACTS)/loci

+loci_INCLUDES := -I $(LOCI)/inc
+loci_INTERNAL_INCLUDES := -I $(LOCI)/src

+LIBRARY := loci
+loci_SUBDIR := $(LOCI)/src
+include $(BUILDER)/lib.mk

这样一来, 编译 indigo 项目时就会直接从 submodules/loxigen-artifacts/ 处获取 loxigen 的源码.

(End)

记一次 Indigo 与 Ryu 的连接建立问题

之前在研究 Indigo 和 Ryu 的衔接时碰到一个 OpenFlow 连接建立失败的问题, 特此记录下, 希望能够供别人参考.

在了解这个问题之前, 我们先回顾一下 OpenFlow 连接建立过程.

OpenFlow 协议的连接建立过程

根据 OpenFlow 协议标准的陈述我们能够知道, 交换机和控制器之间使用 TCP (或者 SSL) 传输协议, 交换机必须能够主动发起连接 (实际应用中, 连接一般都是都由交换机主动发起), 另外就是所有的 OpenFlow 消息, 都要用网络序 (大端序) 发送 (参见 OpenFlow Spec v1.3 以及 v1.4 的第 7 章)

TCP 连接的建立我们很熟悉了, 就是典型的三此握手过程. 在 TCP 连接建立以后, 交换机和控制器双方在 TCP 连接建立后需要立即发送给 OF_HELLO 消息给对方, 并且 OF_HELLO 必须是双发发送给对方的第一个消息, OF_HELLO 消息同时起到协商 OpenFlow 版本的功能.

当双方都收到了对方的 OF_HELLO 消息并且两边都共同支持一个最小版本, OpenFlow 连接就成功建立了, 接下来控制器就可以向交换机发送其它的消息, 比如一般第一次要发送的就是 OFPT_FEATURES_REQUEST 消息.

遇到的问题

在这部分工作中基于 Ryu 框架我写了个简单的 Ryu 小程序令它与 Indigo 通信, 但是发现似乎连接建立都不成功, 好在 indigo 项目的错误日志部分做的很好, 我打开了 verbose 级别的日志, 发现连接建立过程中 indigo 在收取 OF_HELLO 这个消息失败了, 结合代码发现是在读 socket 时发生了 EAGAIN (Resource temporary unvailable) 错误. 熟悉 Linux 开发的人都知道这个错误还有另一个名字叫做 EWOULDBLOCK, 其含义是指我去读一个非阻塞的 socket, 这个 socket 本来是被期望可读的, 但是实际读它的时候发现并没有数据.

这就有点奇怪了, 控制器发送 OF_HELLO 消息给交换机, 然而交换机读这个消息时却发生了这样的系统错误. 原本我怀疑是我的交换机系统环境有问题, 于是我写了一个最基本的 tcp socket server 和一个 tcp socket client, 让 tcp socket server 运行在控制器主机并监听控制器的标准端口 6653, tcp socket client 则跑在我的交换机系统上, 发现这两者是能够正常收发包的, 而且, 我按照 OpenFlow 协议组了一个 OF_HELLO 消息, 从 tcp socket server 发送给 tcp socket client, 我的 socket client 也能够正常的接收这个消息, 并没有出现 EAGAIN 错误.

所以说交换机系统环境是没有问题的, 可是为什么 indigo 的代码里收不到这个 OF_HELLO 消息呢? 看来必须得窥探一下 Ryu 发过来的这个 OF_HELLO 消息是什么样的.

问题的分析

为了确定 Ryu 发过来的 OF_HELLO 消息没问题, 用 tcpdump 抓包看一下两者通信的细节, 下面就是交换机和控制器连接建立过程中抓取的数据包:

09:59:26.578677 ethertype IPv4 (0x0800), length 74: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [S], seq 992465816, win 43690, options [mss 65495,sackOK,TS val 15346082 ecr 0,nop,wscale 7], length 0
        0x0000:  4510 003c fa3a 4000 4006 426f 7f00 0001  E..<.:@.@.Bo....
        0x0010:  7f00 0001 8296 19fd 3b27 d398 0000 0000  ........;'......
        0x0020:  a002 aaaa fe30 0000 0204 ffd7 0402 080a  .....0..........
        0x0030:  00ea 29a2 0000 0000 0103 0307            ..).........
09:59:26.578703 ethertype IPv4 (0x0800), length 74: 10.0.0.2.6653 > 10.0.0.1.33430: Flags [S.], seq 710500324, ack 992465817, win 43690, options [mss 65495,sackOK,TS val 15346082 ecr 15346082,nop,wscale 7], length 0
        0x0000:  4500 003c 0000 4000 4006 3cba 7f00 0001  E..<..@.@.<.....
        0x0010:  7f00 0001 19fd 8296 2a59 5fe4 3b27 d399  ........*Y_.;'..
        0x0020:  a012 aaaa fe30 0000 0204 ffd7 0402 080a  .....0..........
        0x0030:  00ea 29a2 00ea 29a2 0103 0307            ..)...).....
09:59:26.578723 ethertype IPv4 (0x0800), length 66: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [.], ack 710500325, win 342, options [nop,nop,TS val 15346082 ecr 15346082], length 0
        0x0000:  4510 0034 fa3b 4000 4006 4276 7f00 0001  E..4.;@.@.Bv....
        0x0010:  7f00 0001 8296 19fd 3b27 d399 2a59 5fe5  ........;'..*Y_.
        0x0020:  8010 0156 fe28 0000 0101 080a 00ea 29a2  ...V.(........).
        0x0030:  00ea 29a2                                ..).
10:10:56.193800 ethertype IPv4 (0x0800), length 76: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [P.], seq 992465817:992465827, ack 710500325, win 342, options [nop,nop,TS val 15518486 ecr 15346082]
        0x0000:  4510 003e fa3c 4000 4006 426b 7f00 0001  E..>.<@.@.Bk....
        0x0010:  7f00 0001 8296 19fd 3b27 d399 2a59 5fe5  ........;'..*Y_.
        0x0020:  8018 0156 fe32 0000 0101 080a 00ec cb16  ...V.2..........
        0x0030:  00ea 29a2 0400 0800 0000 0000            ..).........                  (注: 交换机发给控制器的 OF_HELLO 消息, 即 0400 0800 0000 0000)
10:10:56.193822 ethertype IPv4 (0x0800), length 66: 10.0.0.2.6653 > 10.0.0.1.33430: Flags [.], ack 992465827, win 342, options [nop,nop,TS val 15518486 ecr 15518486], length 0
        0x0000:  4500 0034 7647 4000 4006 c67a 7f00 0001  E..4vG@.@..z....
        0x0010:  7f00 0001 19fd 8296 2a59 5fe5 3b27 d3a3  ........*Y_.;'..
        0x0020:  8010 0156 fe28 0000 0101 080a 00ec cb16  ...V.(..........
        0x0030:  00ec cb16                                ....
10:11:01.246773 ethertype IPv4 (0x0800), length 75: 10.0.0.2.6653 > 10.0.0.1.33430: Flags [P.], seq 710500325:710500334, ack 992465827, win 342, options [nop,nop,TS val 15519749 ecr 15518486]
        0x0000:  4500 003d 7648 4000 4006 c670 7f00 0001  E..=vH@.@..p....
        0x0010:  7f00 0001 19fd 8296 2a59 5fe5 3b27 d3a3  ........*Y_.;'..
        0x0020:  8018 0156 fe31 0000 0101 080a 00ec d005  ...V.1..........
        0x0030:  00ec cb16 0400 0008 eed3 0ebf            ............                  (注: 控制器发给交换机的 OF_HELLO 消息, 即 0400 0008 eed3 0ebf)
10:11:01.246800 ethertype IPv4 (0x0800), length 66: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [.], ack 710500334, win 342, options [nop,nop,TS val 15519749 ecr 15519749], length 0
        0x0000:  4510 0034 fa3d 4000 4006 4274 7f00 0001  E..4.=@.@.Bt....
        0x0010:  7f00 0001 8296 19fd 3b27 d3a3 2a59 5fee  ........;'..*Y_.
        0x0020:  8010 0156 fe28 0000 0101 080a 00ec d005  ...V.(..........
        0x0030:  00ec d005                                ....

在这里, 10.0.0.1 是交换机地址, 10.0.0.2 是控制器地址. 其中前三条报文显然就是 tcp 三次握手, 后四条就是两者相互发送的 OF_HELLO 消息, 上面两个括号里的注释是我加的. 在 OpenFlow 协议中, OF_HELLO 消息是一条只有 header 没有 payload 的消息, 所以 OF_HELLO 消息的长度就是 OpenFlow 消息头的长度: 8 字节, 其中第一个字节是 Ryu 与 Indigo 协商好的 OpenFlow 版本号, 这里是 0x04, 代表 OpenFlow v1.3 (注意不是 v1.4), 接下来的一个字节 0x00 表示消息类型是 OF_HELLO, 后四个字节是消息的唯一标识码我们可以不用理会, 中间的两个字节表示整个 OpenFlow 消息的长度, 有意思的地方就在这里.

Ryu 和 Indigo 相互发给对方的 OF_HELLO 消息中, 头部的长度字段字节序不一致, OpenFlow 协议头部的长度字段是两字节, OF_HELLO 消息长度为 8, 即 0x0008, 前面说过在 OpenFlow 协议中要求, 所有的 OpenFlow 消息, 都要用大端序发送. 所以这两个字节用网络序也就是大端序表示应该为 0x0008, 即 MSB 在低字节. 显然, 在这一点上 Ryu 的报文是正确的, indigo 的报文是错误的.

那么 EAGAIN 错误是怎么出现的呢? 既然 indigo 发出消息的长度字段的端序有问题, 可想而知其对于收到的消息的长度字段的端序理解也很可能有问题. 下面这段是 indigo 读取消息的代码逻辑, 为了清晰起见我做了一些删减:

/* read header */
if (READING_HEADER(cxn)) {
    INDIGO_ASSERT(cxn->bytes_needed + cxn->read_bytes ==
                  OF_MESSAGE_HEADER_LENGTH);
    if ((rv = read_from_cxn(cxn)) < 0) {
        return rv;
    }

    msg = (of_message_t)(cxn->read_buffer);
    msg_bytes = of_message_length_get(msg);
    if (msg_bytes < OF_MESSAGE_HEADER_LENGTH) {
        ++ind_cxn_internal_errors;
        return INDIGO_ERROR_PROTOCOL;
    }
    cxn->bytes_needed = msg_bytes - OF_MESSAGE_HEADER_LENGTH;
}

if (cxn->bytes_needed == 0) {
    return INDIGO_ERROR_NONE;
}

/* read the rest */
if ((rv = read_from_cxn(cxn)) < 0) {
    return rv;
}

稍微解释一下这段代码, 在 indigo 的代码中, 与 ryu 通信的 socket 是非阻塞的, 用 poll 系统调用来判断是否可读, indigo 在读 socket 时还有一个字段叫做 bytes_needed 用来表示我想要从 socket 中读多少个字节, read_from_cxn(cxn) 方法会按照 bytes_needed 的值明确读取这些字节数. 当内核第一次通知 indigo 这个 socket 可读时, bytes_needed 字段总是 8, 因为 OpenFlow 的消息头最小就是 8, 小于 8 就是非法的. 读取了 8 字节的头之后, of_message_length_get(msg) 会解析头部中的两字节的长度字段获知整条 OpenFlow 消息的长度, 用它减去头部的长度来获知我还需要读多少字节.

可想而知, 当 indigo 收到 ryu 的 OF_HELLO 时, 它也把 0x0008 处理成了 0x0800, indigo 以为 ryu 给它发送了 8 x 256 = 2048 个字节, 但显然实际上 ryu 只给它发送了 8 字节. 在 indigo 认为后面应该还有 2040 个字节, 于是继续调用 read_from_cxn(cxn), 但实际上 socket 上没有字节可读了, 于是引发 EAGAIN 错误.

问题的解决

综上来看, 问题出在 indigo 对长度字段的解析上, 也就是上面代码段里的 of_message_length_get() 方法中, 我层层看下去发现 of_message_length_get() 方法的调用链是这样的:

of_message_length_get() >> buf_u16_get() >> U16_NTOH()

这几个方法的含义一看名字就知道, 最后的 U16_NTOH() 是个条件编译的宏, 定义如下:

#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ 
#define U16_NTOH(val) (val) 
#else 
#define U16_NTOH(val) (((val) >> 8 |(((val) & 0xff) << 8)) 
#endif 

所以现在一切都清晰了, indigo 没有使用 POSIX 标准的系统调用 htons()/ntohs(), 而是自己定义了一套主机序到网络序的转换方法, 可能是 indigo 想能够编译在更多的平台上而不只是 POSIX 兼容的系统. 只不过这样一来, 我们就需要在编译 indigo 的时候根据我们自己的系统架构来定义 __BYTE_ORDER__ 宏. 在我上面的实验环境中, 交换机系统架构是小端序的, 所以收到网络上的消息时, 对于二字节的长度字段应该做颠倒高低字节的处理, 然而 __BYTE_ORDER____ORDER_BIG_ENDIAN__ 这两个宏我都没定义, 所以在预处理阶段 U16_NTOH(val) 宏就直接被定义成了 (val).

那么解决办法就是, 视系统架构而定, 在编译 indigo 的 CPPFLAGS (或者你可能用的是 CFLAGS) 加上几个宏定义:

小端序系统:

CPPFLAGS += -D__ORDER_BIG_ENDIAN__=0 -D__ORDER_LITTLE_ENDIAN__=1 -D__BYTE_ORDER__=__ORDER_LITTLE_ENDIAN__

大端序系统:

CPPFLAGS += -D__ORDER_BIG_ENDIAN__=0 -D__ORDER_LITTLE_ENDIAN__=1 -D__BYTE_ORDER__=__ORDER_BIG_ENDIAN__

(End)

OpenFlow 协议匹配结构进化与 OXM TLV 解读

在 OpenFlow 流表定义中, 报文匹配表项是个重要的行为, 我们下发的每一个流表表项都可以包含一到多个匹配项, 报文进来时会与这些匹配项比较, 如果匹配成功的话, 表项中动作也相应的被附加到报文上. 典型的匹配项有入端口, 源 MAC 地址, 目的 MAC 地址, VLAN Id, 源 IP 地址, 目的 IP 地址, MPLS 标签等等. 不难想象, 网络中的报文种类繁杂, 想要能够匹配每一种报文, 光匹配项就能列一个长长的列表出来, 并且随着各种网络通信协议的不断演进以及越来越复杂的网络业务, 这些匹配项将来还会可能还会增加. 所以在 OpenFlow 协议中, 选择一种合适的数据结构来描述这些匹配项就显得重要起来.

早期的匹配结构

在 OpenFlow 协议早期的版本中, 使用一种固定的数据结构来表述所有的匹配项, 这个数据结构长度固定并且所有的匹配项都包含在里面:

enum ofp_match_type {
    OFPMT_STANDARD,             /* The match fields defined in the ofp_match
                                    structure apply */
};

/* Fields to match against flows */
struct ofp_match {
    uint16_t type;              /* One of OFPMT_* */
    uint16_t length;            /* Length of ofp_match */
    uint32_t in_port;           /* Input switch port. */
    uint32_t wildcards;         /* Wildcard fields. */
    uint8_t dl_src[OFP_ETH_ALEN]; /* Ethernet source address. */
    uint8_t dl_src_mask[OFP_ETH_ALEN]; /* Ethernet source address mask. */
    uint8_t dl_dst[OFP_ETH_ALEN]; /* Ethernet destination address. */
    uint8_t dl_dst_mask[OFP_ETH_ALEN]; /* Ethernet destination address mask. */
    uint16_t dl_vlan;           /* Input VLAN id. */
    uint8_t dl_vlan_pcp;        /* Input VLAN priority. */
    uint8_t pad1[1];            /* Align to 32-bits */
    uint16_t dl_type;           /* Ethernet frame type */
    uint8_t nw_tos;             /* IP ToS */
    uint8_t nw_proto;           /* IP protocol or lower 8 bits of ARP code */
    uint32_t nw_src;            /* IP src address */
    uint32_t nw_src_mask;       /* IP src address mask */
    uint32_t nw_dst;            /* IP dest address */
    uint32_t nw_dst_mask;       /* IP dest address mask */
    uint16_t tp_src;            /* TCP/UDP/SCTP source port */
    uint16_t tp_dst;            /* TCP/UDP/SCTP dest port */
    uint32_t mpls_label;        /* MPLS label */
    uint8_t mpls_tc;            /* MPLS TC */
    uint8_t pad2[3];            /* Align to 64 bit */
    uint64_t metadata;          /* Metadata between tables */
    uint64_t metadata_mask;     /* Mask for metadata */
}

这个是 OpenFlow v1.1 中的匹配结构体的定义, 其中 type 字段的取值只有一个就是 OFPMT_STANDARD. length 代表整个匹配结构体的长度, 它的值可想而知也是固定的, 就是整个结构体的长度 88 字节. wildcards 字段是一个位标志符, 在一次流表下发中, ofp_match 中的匹配字段并不都会被用到, 协议规定哪些匹配字段被用到, 其 wildcards 中的对应位就被清 0, 否则就置 1. 其余的字段就是各个匹配项了, 其含义想必一目了然, 不需多做解释.

通过以上的定义不难看出, 早期的 OpenFlow 协议对于匹配项处理的不是太好, 一个是匹配结构体固定, 所有的匹配项都包含在一起, 下流表时即时不需要这个匹配项, 也要一并下发下来, 加大了网络开销; 另外最重要的是这个定义毫无扩展性, 想要增加新的匹配项就等于再更新一版新的 OpenFlow 协议.

在后续的 OpenFlow 协议中, 采用了另一种新的定义解决了这些问题.

新的匹配结构

从 OpenFlow 1.2 开始, 一种新的匹配结构被定义出来, 这种结构被称作 OpenFlow Extensible Match, 简称 OXM, 它采用 Type-length-value 结构, 所以也被称作 OXM TLV.

enum ofp_match_type {
    OFPMT_STANDARD = 0,       /* Deprecated. */
    OFPMT_OXM      = 1,       /* OpenFlow Extensible Match */
};

/* Fields to match against flows */
struct ofp_match {
    uint16_t type;             /* One of OFPMT_* */
    uint16_t length;           /* Length of ofp_match (excluding padding) */
    /* Followed by:
     *   - Exactly (length - 4) (possibly 0) bytes containing OXM TLVs, then
     *   - Exactly ((length + 7)/8*8 - length) (between 0 and 7) bytes of
     *     all-zero bytes
     * In summary, ofp_match is padded as needed, to make its overall size
     * a multiple of 8, to preserve alignement in structures using it.
     */
    uint8_t oxm_fields[0];     /* 0 or more OXM match fields */
    uint8_t pad[4];            /* Zero bytes - see above for sizing */
};
OFP_ASSERT(sizeof(struct ofp_match) == 8);

这个结构体定义以及注释都是取自 OpenFlow 1.4 的源码, 从其注释中可以看到 OFPMT_STANDARD 类型的匹配项已经废弃不用了, 所以 ofp_match 结构体的 type 字段今后的取值将总是 OFPMT_OXM. oxm_fields 字段表示的是一组 OXM TLV 的集合, 可能是 0 个, 也可能多个, 从这可以看出整个 ofp_match 结构是一个变长的结构, 在控制器下发消息时, 只需要包含需要的匹配项, 不需要的匹配项无需包含在消息体中, 省去了不必要的开销.

那么 OXM TLV 的格式到底是如何的呢?

OXM TLV

每一个 OXM TLV 都一定包含一个 4 字节的头, 对于 OpenFlow 标准所定义的匹配项, oxm_class 的取值固定为 0x8000. oxm_field 表示具体的匹配项, 比如源 MAC, VLAN ID 等. oxm_length 表示此 OXM TLV 的值的长度, 以字节为单位. M 字段则表示这个 OXM TLV 是否包含掩码, 在 OXM TLV 中, 掩码的长度和值的长度是一样的, 掩码中的某位为 1 表示报文中匹配项对应位必须和值的对应位相同才能匹配, 掩码中的某位为 0 则表示对报文中匹配项对应位的值不做限制. 如果包含了掩码, 那么报文的匹配将会变成先和做掩码按位与操作, 结果再和 OXM TLV 的值进行比较. 如果下发的消息里没有包含掩码, 那就需要报文与 OXM TLV 的值完全匹配才行.

31                         15          8             0
------------------------------------------------------
|       oxm_class         | oxm_field |M| oxm_length |
------------------------------------------------------

OpenFlow 1.4 中定义的标准匹配项有如下这些:

/* OXM Flow match field types for OpenFlow basic class. */
enum oxm_ofb_match_fields {
    OFPXMT_OFB_IN_PORT        = 0,  /* Switch input port. */
    OFPXMT_OFB_IN_PHY_PORT    = 1,  /* Switch physical input port. */
    OFPXMT_OFB_METADATA       = 2,  /* Metadata passed between tables. */
    OFPXMT_OFB_ETH_DST        = 3,  /* Ethernet destination address. */
    OFPXMT_OFB_ETH_SRC        = 4,  /* Ethernet source address. */
    OFPXMT_OFB_ETH_TYPE       = 5,  /* Ethernet frame type. */
    OFPXMT_OFB_VLAN_VID       = 6,  /* VLAN id. */
    OFPXMT_OFB_VLAN_PCP       = 7,  /* VLAN priority. */
    OFPXMT_OFB_IP_DSCP        = 8,  /* IP DSCP (6 bits in ToS field). */
    OFPXMT_OFB_IP_ECN         = 9,  /* IP ECN (2 bits in ToS field). */
    OFPXMT_OFB_IP_PROTO       = 10, /* IP protocol. */
    OFPXMT_OFB_IPV4_SRC       = 11, /* IPv4 source address. */
    OFPXMT_OFB_IPV4_DST       = 12, /* IPv4 destination address. */
    OFPXMT_OFB_TCP_SRC        = 13, /* TCP source port. */
    OFPXMT_OFB_TCP_DST        = 14, /* TCP destination port. */
    OFPXMT_OFB_UDP_SRC        = 15, /* UDP source port. */
    OFPXMT_OFB_UDP_DST        = 16, /* UDP destination port. */
    OFPXMT_OFB_SCTP_SRC       = 17, /* SCTP source port. */
    OFPXMT_OFB_SCTP_DST       = 18, /* SCTP destination port. */
    OFPXMT_OFB_ICMPV4_TYPE    = 19, /* ICMP type. */
    OFPXMT_OFB_ICMPV4_CODE    = 20, /* ICMP code. */
    OFPXMT_OFB_ARP_OP         = 21, /* ARP opcode. */
    OFPXMT_OFB_ARP_SPA        = 22, /* ARP source IPv4 address. */
    OFPXMT_OFB_ARP_TPA        = 23, /* ARP target IPv4 address. */
    OFPXMT_OFB_ARP_SHA        = 24, /* ARP source hardware address. */
    OFPXMT_OFB_ARP_THA        = 25, /* ARP target hardware address. */
    OFPXMT_OFB_IPV6_SRC       = 26, /* IPv6 source address. */
    OFPXMT_OFB_IPV6_DST       = 27, /* IPv6 destination address. */
    OFPXMT_OFB_IPV6_FLABEL    = 28, /* IPv6 Flow Label */
    OFPXMT_OFB_ICMPV6_TYPE    = 29, /* ICMPv6 type. */
    OFPXMT_OFB_ICMPV6_CODE    = 30, /* ICMPv6 code. */
    OFPXMT_OFB_IPV6_ND_TARGET = 31, /* Target address for ND. */
    OFPXMT_OFB_IPV6_ND_SLL    = 32, /* Source link-layer for ND. */
    OFPXMT_OFB_IPV6_ND_TLL    = 33, /* Target link-layer for ND. */
    OFPXMT_OFB_MPLS_LABEL     = 34, /* MPLS label. */
    OFPXMT_OFB_MPLS_TC        = 35, /* MPLS TC. */
    OFPXMT_OFP_MPLS_BOS       = 36, /* MPLS BoS bit. */
    OFPXMT_OFB_PBB_ISID       = 37, /* PBB I-SID. */
    OFPXMT_OFB_TUNNEL_ID      = 38, /* Logical Port Metadata. */
    OFPXMT_OFB_IPV6_EXTHDR    = 39, /* IPv6 Extension Header pseudo-field */
    OFPXMT_OFB_PBB_UCA        = 41, /* PBB UCA header field. */
};

可以看出相比 OpenFlow 1.1, 这里的匹配项多了很多, 新的匹配结构也提供了方便的扩展匹配项的机制. 前面说了对于 OpenFlow 定义的标准的匹配项, 其 oxm_class 字段的值固定为 0x8000, 如果是其它厂商或组织定义的匹配项, 则可以使用 0xFFFF 这个值. OpenFlow 协议还规定了 oxm_class 取值在 [0x8000, 0xFFFF) 范围内的都留给 OpenFlow 标准, 以备将来协议更新之用; 而 [0x0000, 0x7FFF] 范围内的值则留给 ONF 组织.

OXM TLV 中 payload 的长度

对于某个 OXM 类别下的某个确定的 OXM TLV, 显然其值的长度是一定的, 如果这个 OXM TLV 不包含掩码, 那么其值的长度就是 payload 的长度; 如果包含掩码, 那么 payload 的长度就是值长度的两倍.

关于值的长度, 有一点需要注意的是, 虽然很多 OXM TLV 的值的长度都按 bit 计算的, 比如 IP 报文的 DSCP 字段其实是 6 bits 字段, Vlan Id 是个 12 bits 字段, 但是 oxm_length 字段的单位是字节, 就是说即时 OXM TLV 值用不了一个字节, 也会占用一字节的空间.

这里还有一个细微的问题, 协议 Spec 中没有明确说明的, 就是如果某个 OXM TLV 包含掩码, 并且其值的长度小于 4 bits, 那么这个 OXM TLV 的 payload 最终占用的空间是 1 字节还是 2 字节呢? 这个问题微妙的地方在于值的长度小于 4 bits, 通过上面所说的我们知道如果没有掩码 payload 肯定也是占用 1 字节, 但如果有掩码呢? 在值和掩码加起来还是不超过 1 字节的情况下, 协议会为这种情况做一点空间优化, 让值和掩码 “挤” 进 1 个字节里吗?

答案是不会, 这种情况下 payload 还是会占用 2 字节的空间, 这个问题 OpenFlow 的文档里没有明确的回答, 答案我也是从源码里找到的. 在 OpenFlow 中 OXM TLV 的定义是用 OXM_HEADER 以及 OXM_HEADER_W 这两个宏来定义的, 前者定义不包含掩码的 OXM TLV, 后者则定义包含掩码的 OXM TLV, 而这两个宏其实又都引用了 OXM_HEADER__ 这个宏. 可以看到, 定义中并没有对值小于 4 bits 的 OXM TLV 做什么特殊处理, 就算值只有 1 bit, 定义包含掩码时也要对长度乘以 2 变成 2 字节.

/* Components of a OXM TLV header. */
#define OXM_HEADER__(CLASS, FIELD, HASMASK, LENGTH) \
    (((CLASS) << 16) | ((FIELD) << 9) | ((HASMASK) << 8) | (LENGTH))
#define OXM_HEADER(CLASS, FIELD, LENGTH) \
    OXM_HEADER__(CLASS, FIELD, 0, LENGTH)
#define OXM_HEADER_W(CLASS, FIELD, LENGTH) \
    OXM_HEADER__(CLASS, FIELD, 1, (LENGTH) * 2)

扩展 OXM TLV

我们可以通过 Experimenter 特性来扩展 OXM TLV, 前面已经说了当 oxm_class 字段取值 0xFFFF 时, 就代表这个 OXM TLV 是扩展字段. 另外对于扩展 OXM TLV 最重要的一点就是, 紧跟在 4 字节头后面的一定得是一个 32 bit 的 Experimenter ID, 而不是 OXM TLV 的值. Experimenter ID 可以用来唯一标志厂商或组织, 可以向 ONF 申请分配, 也可以是厂商自己已有的 IEEE 分配的 OUI 号码.

Experimenter ID 之后的内容 OpenFlow 协议就不关心了, 完全由厂商自己来解释.

参考

  1. OpenFlow Spec v1.4.0

关于 OpenFlow 协议中 Instruction, Action 概念的解读

(首发于 sdnlab: http://www.sdnlab.com/17952.html)

阅读任何一个协议都要注意的一点是这个协议中所定义的专有术语, 对这些术语的理解不到位的话也会造成对协议的理解偏差. 本文想和大家分享几个可能容易混淆的术语.

在 OpenFlow 协议文档中经常会看到这么几个词语: Instruction, Action, Apply-actions, Action Set, Action List, Clear-actions, … 有点迷惑人, 实际上这里面只有两个实体的概念: Instruction 和 Action. 为了保持后文的易读性, 这两个概念分别用中文 “指令” 和 “动作” 来描述. 下文中的 “指令” 和 “动作” 都特指在 OpenFlow 协议中的含义.

指令这个词, 特指流表表项中的指令, 当某个报文匹配了这个表项之后, 表项中的指令就会被应用于这个报文; 而动作是比指令更细粒度的概念, 但它并不是局限于流表表项的概念, 动作可以独立于指令而存在, 也可以被包含在指令中, 具体说来, 我们在下流表的时候, 可以为某个表项的某种指令指定一些列的动作, 但是动作并不是只有下流表的时候才会被用到.

本文以目前较新的 Openflow 1.4 版本为准, 来分别看一下指令和动作的含义.

指令

每一个流表的表项都包含一系列的指令, 当报文匹配上了这个表项后, 这些指令就会被执行, 这些指令的执行结果有几种: 改变报文, 改变 action set, 改变 pipeline. 这些指令可以按照其执行结果的不同而分类, 不同的流表的表项包含的指令种类也不同, 前面说了指令可以包含动作, 但也并非所有种类的指令都包含动作, 下面我们一起来看一下指令的分类.

指令的分类

OpenFlow 1.4 中规定了 6 种类型的指令, 但并不要求交换机支持所有的类型. 另外要注意的是, 在 OpenFlow 协议文档中指令的类型名字几乎全都以 “actions” 后缀结尾, 我觉得这是非常容易令人混淆的地方, 我们一定要记住指令类型名中的 “actions” 字样和我们上面说的 “动作” 的概念完全没有关系. 然而 OpenFlow 协议文档的这种写法看起来似乎每种指令都包含了一组动作, 而实际上只有几种指令是真正包含动作的, 下面我们来看一下这 6 种指令与动作的关系.

为了避免我们更加混淆, 6 种指令类型的名字我还是保持和 OpenFlow 1.4 的 Spec 一样. 还有一点需要注意的是, 括号里的 “可选”, “必选” 字样指的是交换机是否必须支持这个指令, 而不是说下流表时表项中是否必须包含这个指令.

  • (可选指令) Meter meter_id, 不包含动作, 行为是将报文送往指定的 meter
  • (可选指令) Apply-actions action(s), 这个指令是真正包含动作的指令, 它的行为是立即对报文应用这些指令, 不要改变报文的 action set
  • (可选指令) Clear-actions, 这个指令并不包含任何的动作, 它的行为是立即清除报文的 action set 中所有的动作
  • (必选指令) Write-actions actions(s), 这个指令真正的包含动作, 它的行为是将自己包含的动作合并到报文的 action set 中
  • (可选指令) Write-Metadata metadata / mask, 这个也不包含动作, 用的不多
  • (必选指令) Goto-Table next-table-id, 这个指令也不包含动作, 它表示把报文交给后续的哪张流表处理. OpenFlow 协议要求交换机必须支持这个 action, 但有一个例外是假设你的交换机本身就只支持一张流表, 那可以不支持这个 action.

动作

前面说了动作也有分类, 但是相比指令的分类, 动作的分类就比较好理解了, 我们稍加带过, 然后解释下 Action Set 和 Action List 两个概念.

同样, OpenFlow 协议不要求交换机支持所有的动作种类, 我们只看几个常见的:

  • (可选) Output, 表示将报文从某个特定的端口送出去
  • (必选) Drop, 丢弃报文
  • (必选) Group, 表示将报文交给指定的组
  • (可选) Change-TTL, 改变报文的 TTL 字段 (可以是 IPv4 TTL, MPLS TTL 或者 Ipv6 Hop Limit)

关于 Action Set

Action set 是一个与报文相关联的概念, 只要提起 action set, 它就一定是报文的 action set, 它包含了当报文离开流表时要附加于这个报文上的动作. 我们前面看到了有一种 Apply-actions 指令, 它是在报文匹配了表项的时候将它包含的动作立即应用到报文上, 而 Write-actions 则是将它包含的动作合并到报文的 action set 中, 另外还有 Clear-actions 指令, 是将报文的 action set 清空. 最终报文走完所有流表时, 其 action set 里面有什么动作, 就执行什么动作, 这就是 action set 的作用了.

关于 Action List

Action list 实际上就是一系列动作的有序序列, 一定要注意其有序性. 在上面说到的流表中的 Apply-actions 指令中, 以及 OpenFlow 协议中同样能够包含动作的 Packet-out 命令中, 都要求所包含的动作被有序执行. 所以就出来了这么个 action list 的概念, 这是与 action set 的一点区别. 另一个区别是 action list 并不是和报文相关联的概念, action list 可以直接夹带在 controller 发给 agent 的消息中, 比如 Packet-out 消息; 也可以存在于流表表项的指令中, 比如 Apply-actions 指令.

协议源代码

说实话, 光看协议 Spec 我是没有理清楚这些个指令与动作的关系的, 真正完全理清楚是看了 OpenFlow 源码之后. 在 OpenFlow 源码中, 指令与动作的结构头分别如下:

struct ofp_instruction_header {
    uint16_t type;          /* One of OFPIT_*. */
    uint16_t len;           /* Length of this struct in bytes. */
};
OFP_ASSERT(sizeof(struct ofp_instruction_header) == 4);

struct ofp_action_header {
    uint16_t type;          /* One of OFPAT_*. */
    uint16_t len;           /* Length of action, including this
                               header. This is the length of action,
                               including any padding to make it
                               64-bit aligned. */
};
OFP_ASSERT(sizeof(struct ofp_action_header) == 4);

Meter, Write-metadata, Goto-Table 这三类指令的结构如下, 它们的前两个字段和 struct ofp_instruction_header 是相同的, 另外可以看出, 它们都不包含 struct ofp_action_header 结构体, 所以这三个指令是不包含动作的.

/* Instruction structure for OFPIT_METER */
struct ofp_instruction_meter {
    uint16_t type;            /* OFPIT_METER */
    uint16_t len;             /* Length is 8. */
    uint32_t meter_id;        /* Meter instance. */
};
OFP_ASSERT(sizeof(struct ofp_instruction_meter) == 8);

/* Instruction structure for OFPIT_GOTO_TABLE */
struct ofp_instruction_goto_table {
    uint16_t type;             /* OFPIT_GOTO_TABLE */
    uint16_t len;              /* Length is 8. */
    uint8_t table_id;          /* Set next table in the lookup pipeline */
    uint8_t pad[3];            /* Pad to 64 bits. */
};
OFP_ASSERT(sizeof(struct ofp_instruction_goto_table) == 8);

/* Instruction structure for OFPIT_WRITE_METADATA */
struct ofp_instruction_write_metadata {
    uint16_t type;                    /* OFPIT_WRITE_METADATA */
    uint16_t len;                     /* Length is 24. */
    uint8_t pad[4];                   /* Align to 64-bits */
    uint64_t metadata;                /* Metadata value to write */
    uint64_t metadata_mask;           /* Metadata write bitmask */
};
OFP_ASSERT(sizeof(struct ofp_instruction_write_metadata) == 24);

而 Apply-actions, Clear-actions, Write-actions 三种指令则共用如下的结构体, 可以看到它是包含 struct ofp_action_header 的, 你可能会奇怪 Clear-actions 指令不是也不包含动作吗, 为什么也用了这个结构体, 实际上对于 Clear-actions 指令来说, struct ofp_instruction_actions 结构体的最后一个 actions 字段是大小为 0 的数组.

/* Instruction structure for OFPIT_WRITE/APPLY/CLEAR_ACTIONS */
struct ofp_instruction_actions {
    uint16_t type;           /* One of OFPIT_*_ACTIONS */
    uint16_t len;            /* Length is padded to 64 bits. */
    uint8_t pad[4];          /* Align to 64-bits */
    struct ofp_action_header actions[0];        /* 0 or more actions associated with
                                                   OFPIT_WRITE_ACTIONS and
                                                   OFPIT_APPLY_ACTIONS */
};
OFP_ASSERT(sizeof(struct ofp_instruction_actions) == 8);

另外上面还说到了一个 Packet-out 消息也是包含动作的, 它的定义如下, actions 字段包含了一个动作列表, 也就是 action list.

/* Send packet (controller -> datapath). */
struct ofp_packet_out {
    struct ofp_header header;
    uint32_t buffer_id;              /* ID assigned by datapath (OFP_NO_BUFFER if none). */
    uint32_t in_port;                /* Packet’s input port or OFPP_CONTROLLER. */
    uint16_t actions_len;            /* Size of action array in bytes. */
    uint8_t pad[6];
    struct ofp_action_header actions[0]; /* Action list - 0 or more. */
                                         /* The variable size action list is optionally followed by packet data.
                                          * This data is only present and meaningful if buffer_id == -1. */
    /* uint8_t data[0]; */               /* Packet data. The length is inferred from the length field in the header. */
};
OFP_ASSERT(sizeof(struct ofp_packet_out) == 24);

参考

  1. OpenFlow Spec v1.4.0