OVS中Action源码分析&自定义Action

前言

在生产或是科研中,OpenFlow定义的Action有时候并不能完全满足需求,那么如何向OVS中添加一个自定义的action,本文对此做详细分析。

我们知道,无论是控制器还是交换机,OVS对于OpenFlow内容的解析和支持是基于OpenFlow标准的。也就是说每一个消息体、流表中的匹配域或是action等,都是按照OpenFlow协议定义进行实现。那么对于自定义action,也应该遵循这一规则,即需要在OVS中,完成新action的定义、解析、执行等一系列源码修改。

说明:OpenFlow 1.3是科研和生产环境中最常用的版本,因此就以OpenFlow1.3为例,在Open vSwitch v1.3.1上分析OVS如何运行一个action,并同时说明如何自定义Action。

一 新action的定义

1 include/OpenFlow文件夹定义了OpenFlow各个版本的协议内容(nicira-ext.h中定义nicira的action等内容,我们对他们也统称为openflow action),包括所支持的消息体,匹配域,openflow action等。首先,我们需要在这里定义新的action枚举类型和其结构体。

action的枚举类型。在OpenFlow-1.3.h中,`ofp13_action_type`中定义OF v1.3所支持的action名字,以枚举方式表示:

enum ofp13_action_type {
OFPAT13_OUTPUT = 0,   /* Output to switch port. */
 	OFPAT13_COPY_TTL_OUT = 11, /* Copy TTL "outwards" -- from next-to-outermost
to outermost */
 	OFPAT13_COPY_TTL_IN  = 12,  /* Copy TTL "inwards" -- from outermost to
next-to-outermost */
 	OFPAT13_SET_MPLS_TTL = 15,  /* MPLS TTL */
 	OFPAT13_DEC_MPLS_TTL = 16,  /* Decrement MPLS TTL */
 	OFPAT13_PUSH_VLAN = 17,  /* Push a new VLAN tag */
 	OFPAT13_POP_VLAN = 18,  /* Pop the outer VLAN tag */
 	OFPAT13_PUSH_MPLS = 19, /* Push a new MPLS Label Stack Entry */
 	OFPAT13_POP_MPLS = 20, /* Pop the outer MPLS Label Stack Entry */
 	OFPAT13_SET_QUEUE = 21, /* Set queue id when outputting to a port */
 	OFPAT13_GROUP = 22, /* Apply group. */
 	OFPAT13_SET_NW_TTL = 23,  /* IP TTL. */
 	OFPAT13_DEC_NW_TTL = 24,  /* Decrement IP TTL. */
 	OFPAT13_SET_FIELD = 25,  /* Set a header field using OXM TLV format. */
 	OFPAT13_PUSH_PBB = 26, /* Push a new PBB service tag (I-TAG) */
 	OFPAT13_POP_PBB = 27,  /* Pop the outer PBB service tag (I-TAG) */
 	/*add my new action  type*/
 	OFPAT13_SELF_LEARNING = 28  /*self learning mac. */};

类型号1-10在前面版本文件中定义。在`ofp13_action_typ`最后直接新添加新action即可。注意顺序号很重要,后面依赖顺序号进行action解析。

2 定义完新action的枚举类型,则需要定义新action对应的结构体,说明action所包含的字段属性。of 1.3中定义的action结构体在前面版本定义好了,如v1.2版本中增加了set_field,则对应结构体如下:

/* Action structure for OFPAT12_SET_FIELD. */
struct ofp12_action_set_field {
 	ovs_be16 type;      /* OFPAT12_SET_FIELD. */
 	ovs_be16 len;       /* Length is padded to 64 bits. */
 	ovs_be32 dst;       /* OXM TLV header */
 	/* Followed by:
 	* - Exactly ((oxm_len + 4) + 7)/8*8 - (oxm_len + 4) (between 0 and 7)
 	*   bytes of all-zero bytes
 	*/};
OFP_ASSERT(sizeof(struct ofp12_action_set_field) == 8);

type和len是必须的成员属性,如果action结构体中无其他字段信息,则只需要type和len即可(这个例子中还含有设置目的地址的值dst这个字段),如归不足64bit的整数倍,用Pad补全。

3 宏定义,在lib/ofp.def中定义,如下:

OFPAT11_ACTION(OFPAT12_SET_FIELD,    ofp12_action_set_field, 1, "set_field")
...
其格式为OFPAT11_ACTION(ENUM, STRUCT, EXTENSIBLE, NAME)

- 第一个参数ENUM即openflow action的名字,对应1中定义action enum(隐含编号值)。ENUM在后面用于构成openflow action的编号code(类型ofputil_action_code),互相一一对应的,格式为OFPUTIL_##ENUM。

- 第二个参数STRUCT即 openflow action的结构体名字,在2中定义。

- 第四个参数NAME,即action字符串名字。其用于ofctl手动添加流表项时,把字符串action放在这里查找,找到对应ENUM,然后通过ENUM转化为相应action code。比如flowmod中action为“set_field”,则找到ENUM为OFPAT12_SET_FIELD,最终得到OOFPUTI_OFPAT12_SET_FIELD。

4 此外,需要在在ofp-action.c中联合体类型ofp_action中添加openflow action(如1.3和nicira)中的openflow action结构体名:

union ofp_action {
ovs_be16 type;
struct ofp_action_header header;
struct ofp_action_vendor_header vendor;
struct ofp10_action_output output10;
struct ofp_action_vlan_vid vlan_vid;
struct ofp_action_vlan_pcp vlan_pcp;
struct ofp_action_nw_addr nw_addr;
struct ofp_action_nw_tos nw_tos;
...
struct nx_action_header nxa_header;
struct nx_action_resubmit resubmit;
struct nx_action_set_tunnel set_tunnel;
struct nx_action_set_tunnel64 set_tunnel64;
...

union的设计其实很巧妙,对于接收到的flowmod中action字段,直接赋给enum ofp_action即可,会根据type(即对应openflow action的enum值)来确定是哪一个action。因此无需赋值时直接赋值给具体action的结构体。

5 以上完成了在OVS中OpenFlow action的定义,但是ovs对action的处理都是基于抽象action(后面如果遇到ofpact则理解为ovs抽象action),因此OVS需要对这些openflow action进行进一步抽象转化,称其为抽象action(一般带有OFPACT_字样)。

若定义OVS中定义抽象action,首先需要在lib/ofp-actions.h中进行宏定义,如下:

#define OFPACTS                                     /* Output. */ \
DEFINE_OFPACT(OUTPUT,  ofpact_output,  ofpact) \
DEFINE_OFPACT(GROUP,  ofpact_group, ofpact)    \
DEFINE_OFPACT(CONTROLLER, ofpact_controller,  ofpact)    \
DEFINE_OFPACT(ENQUEUE,ofpact_enqueue,  ofpact)    \
DEFINE_OFPACT(OUTPUT_REG, ofpact_output_reg, ofpact)    \
DEFINE_OFPACT(BUNDLE, ofpact_bundle,  slaves) \
          \
/* Header changes. */           \
DEFINE_OFPACT(SET_FIELD,  ofpact_set_field, ofpact)    \
DEFINE_OFPACT(SET_VLAN_VID, ofpact_vlan_vid, ofpact)    \
...

宏定义格式为DEFINE_OFPACT(ENUM, STRUCT, MEMBER)。

- 第一个变量enum为抽象action(ofpact)名字,ENUM在后面会用于构造抽象action类型type,即OFPACT_##ENUM。

- 第二个变量为ofpact的结构体名。

这个宏定义了OVS所有抽象action(ofpact),其与openflow action有一对一或一对多映射关系(如多个版本openflow action映射为一个ofpact),这利于ovs做统一处理。

6 既然定义了抽象action(ofpact),则需要有相应的结构体,lib/ofp-actions.h中定义如下:

/* OFPACT_SET_FIELD.*
 * Used for OFPAT12_SET_FIELD. */
struct ofpact_set_field {
 	struct ofpact ofpact;
 	const struct mf_field *field;
 	bool flow_has_vlan;   /* VLAN present at action validation time. */
 	union mf_value value;
};

第一个属性结构体ofpact是必须的,是作为ofpact的header的,指定了此抽象action类型相关内容,后面是action的data。因此,有必要看一下ofpact结构体定义:

struct ofpact {
 	enum ofpact_type type;      /* OFPACT_*.  其实就是OFPACT_##ENUM*/
 	enum ofputil_action_code compat; /* Original type when added, if any. */
 	uint16_t len;               /* Length of the action, in bytes, including                            * struct ofpact,  excluding padding. */
};

- type:即抽象action(ofpact)的类型(上面提到,格式为OFPACT_##ENUM),如OFPACT_SET_FIELD;

- ofputil_action_code: 可以理解为openflow action编号(包含openflow自身和nicira的action)。因为之前提到,可能一个ofpact结构体对应多个openflow action(一对多),如NXAST_SET_TUNNEL 或者NXAST_SET_TUNNEL64 action(在openflow/nicira-ext.h中定义,我们也称为openflow action)都映射为OFPACT_SET_TUNNEL。这种一对多映射,因此就需要ofputil_action_code来具体记录指定是曾经是哪种openflow action,则可以看出ofputil_action_code就是对应基本(原始)的openflow action类型。ofputil_action_code命名规则其实在前面lib/ofp.def添加代码中提到,格式为OFPUTIL_##ENUM,如OFPUTIL_SET_FIELD.

当添加新action时,则按照以上提到的关键部分,“照猫画虎”式的添加即可,比较简单,不再赘述。

二 action的解析

通过以上操作,完成了自定义action的定义,那么当OVS接收到一个flow-mod消息,如何对消息体中action进行解析、flowmod添加,这需要接下来的工作。

这里有一个诀窍,我们只需要跟踪flowmod消息的解析过程,即可以发现在哪里需要添加相应的action解析代码。从ofproto.c中入口:

handle_openflow() -> handle_openflow_() -> handle_flow_mod()

在handle_flow_mod()函数关系到flowmod消息如何解析出匹配域和action的ofpacts,共同组成流表结构fm,然后并对action的ofpacts进行检验与流表项的添加。

1、解析action

进入handle_flow_mod()函数。在ofputil_decode_flow_mod()中,首先用b指向flowmod数据开始部分,数据长度为flowmod内容长度,然后通过b解析匹配域和action。重点看action,因此进入函数ofpacts_pull_openflow_instructions()后,会解析得到不同instruction,对于我们常用的OVSINST_OFPIT11_APPLY_ACTIONS所含的action,需要用函数

get_actions_from_instruction解析出openflow action,将内容赋给联合体类型ofp_action(一中我们已经定义)的actions指针,然后通过ofpacts_from_openflow函数从actions指针把action内容赋给内存空间ofbuf的ofpacts。

在函数ofpacts_from_openflow中,用函数指针变量ofpact_from_openflow指向相应版本的action解析函数,这里依然以of1.3为例,ofpact_from_openflow(a, version, out)即为ofpact_from_openflow11(a, version, out)。

以上过程函数关系表示如下:

handle_openflow() -> handle_openflow_() -> handle_flow_mod()->ofputil_decode_flow_mod()->ofpacts_pull_openflow_instructions()->ofpacts_from_openflow()->ofpact_from_openflow11()

说了这么多,终于到真正添加关于新action代码的地方了。

通过decode_openflow11_action函数解析出openflow action的code,code即OFPUTIL_##ENUM (openflow action的类型编号,之前提到,并且在一中已铺垫好相应代码,这里无需改动)。下来,需要在switch添加新的case OFPUTIL_##ENUM,然后调用ofpact_put##ENUM函数来生成抽象action(ofpact)。如下:

case OFPUTIL_OFPAT11_DEC_MPLS_TTL:
    	ofpact_put_DEC_MPLS_TTL(out);
    	break;

OFPUTIL_##ENUM是openflow action的code,ofpact_put##ENUM则生成抽象action。ofpact_put##ENUM的ENUM是抽象action(ofpact)名字,在一中的ofp-actions.h中定义。对于简单action(即不像output等action有参数)的添加,只需要向例子中一样即可,带参数则可以同时对ofpact参数做初始化赋值,可以参考其他case的写法。

2、检测

完成action的解析,现在返回到ofpacts_pull_openflow_instructions()函数中。函数结束处 调用函数ofpacts_verify()->ovs_instruction_type_from_ofpact_type() (ofp-actions.c中),这对ovs 抽象action类型进行检验。对于新添加的action,则在switch添加一个新action的case即可,格式为OFPACT_##ENUM,如下:

...
case OFPACT_SET_MPLS_TC:
case OFPACT_SET_MPLS_TTL:
case OFPACT_DEC_MPLS_TTL:
...

通过以上步骤,已经完成从flowmod消息中解析出action内容,并完成抽象action的转化,存储在buffer类型的ofpacts中。

完成添加后返回到ofputil_decode_flow_mod函数,函数最后用fm->ofpacts = ofpbuf_data(ofpacts),完成流表项fm的属性--结构体ofpacts指向这个buffer ofpacts.

紧接着最后,需要用ofpacts_check_consistency进行进一步的ofpact的一致性检测,因此需要进入函数ofpacts_check()-> ofpact_check__()(ofp-actions.c)完成添加。如检测output动作的出端口的合理性,如端口号大于最大限制,则返回错误号。又如OFPACT_DEC_TTL,会检测是否网络层协议为ip,若不是则无法执行这个action,返回协议不一致的错误号。对于添加一个简单的新action,无需检测,直接添加case OFPACT_##ENUM: return即可,如下:

case OFPACT_SET_TUNNEL:
return;

完成以上操作,返回到最外层函数handle_flow_mod。以上都是在ofputil_decode_flow_mod函数中进行,下来继续是ofpact检测,主要是ofpact长度检测, 里面牵扯到的新action的检测代码,在上面已经完成,这里不需要添加。

3、流表项操作(包含action)

以上得到流表项结构fm,需要对fm进行操作,比如添加、删除流表项等,这里还是对新action有关内容着重描述。进入函数handle_flow_mod__()(ofproto.c中),以添加流表项为例,则进入add_flow()函数,先由fm构造rule,并且构造rule->actions,幸运的是,这里面不需要对新的action进行代码修改或是添加。

到这里为止,通过以上OVS代码的添加和修改,OVS则可以正确解析控制器下发的新的action(比如含有新action的flow-mod消息),OVS已经可以正确对action进行解析,并且可以正确插入到流表中。但是action的执行和打印查看需要在完成后面工作。

三 action的执行

当数据包匹配到这条流表项,如何让action正确执行呢?

这里action的执行分两种情况,一种是内核层action的执行和用户层action的执行。但不是所有action会最终下放到内核层执行,只有像output等几个少数的action可以走快速通道直接处理数据包,其他action的执行则要上交用户层来执行。对于可以在内核层执行的action,需要netlink通道action数据传输,并在内核层添加相应action的执行函数。这里只简单说明如何在用户层执行新添加的action。

当数据包在用户层执行action时,最终会调用函数do_xlate_actions()(ofproto-dpif-xlate.c):

static void do_xlate_actions(const struct ofpact *ofpacts, size_t ofpacts_len,struct xlate_ctx *ctx)
{
 	struct flow_wildcards *wc = &ctx->xout->wc;
 	struct flow *flow = &ctx->xin->flow;
 	const struct ofpact *a;
 	OFPACT_FOR_EACH (a, ofpacts, ofpacts_len) {
    	struct ofpact_controller *controller;
    	const struct ofpact_metadata *metadata;
    	const struct ofpact_set_field *set_field;
    	const struct mf_field *mf;
    	if (ctx->exit) {
     	   break;
    	}
    	switch (a->type) {
    	case OFPACT_OUTPUT:
xlate_output_action(ctx, ofpact_get_OUTPUT(a)->port,
ofpact_get_OUTPUT(a)->max_len, true);
       	break;
...

如何新的action需要对数据包操作,则有必要分析清楚代码执行过程。函数开始通过struct flow *flow = &ctx->xin->flow提取出要执行action的flow。ctx(xlate_ctx)是一个重要的结构体,其属性xin是结构体xlate_in,xin含有要执行action的数据包,数据包所有包头字段组成为flow,action的一些包头修改操作都将在flow上进行。提取flow之后,通过一个循环提取出每一个ofpact(ovs的抽象action,之前有提到),然后进行switch判断进行相应动作的操作执行。如对ipv4头部修改的action:

case OFPACT_SET_IPV4_SRC:
if (flow->dl_type == htons(ETH_TYPE_IP)) {
memset(&wc->masks.nw_src, 0xff, sizeof wc->masks.nw_src);
   flow->nw_src = ofpact_get_SET_IPV4_SRC(a)->ipv4;
        }
        break;

注意对于新增加action,case必须是OFPACT_##ENUM格式,然后进行相应操作即可,比如检测包头字段,判断是arp请求,则进行arp包的回复等,这里需要根据自己action的需求进行操作即可。

action的执行只需要在这里添加代码,当数据包在用户层匹配到后会正确执行,并向内核层添加流表项,对于以上新的数据包匹配到这个action仍然会上交用户层处理。

四 查询、封装

到这里,一个新的action就可以完完整整的正确执行了。但是有一点,我们并不能ovs-ofctl dump-flows等命令正确查看一个流表项,新的action会显示乱码,也不能通过控制器访问交换机流表,原因在于对于存在流表项的action,需要有ovs解析一个新action的逆过程,把其从ovs的抽象action(ofpact)转化为openflow action,打印还需要action字符串名字的解析过程。因此,有这么几个地方需要源码添加修改,主要集中在lib/ofp-action.c和lib\ofp-parse.c两个文件:

1 lib/ofp-action.c中

函数 ofpact_to_openflow11 和上面提到的函数ofpact_from_openflow11(把openflow action解析为ofpact)是互逆的过程。对于新添加action,代码添加比较简单,仿照下面即可:

case OFPACT_DEC_MPLS_TTL:
    ofputil_put_OFPAT11_DEC_MPLS_TTL(out);
    break;

2 lib/ofp-action.c中

当ofctl打印流表项时,则需要将action解析为相应字符串。这在函数ofpact_format()中进行添加。对于新的action,添加如下,替换case类型、替换“pop_queue”为相应action名字字符串即可:

case OFPACT_POP_QUEUE:
    	ds_put_cstr(s, "pop_queue");
    	break;

3 在 lib/ofp-parse.c中

当用ofctl添加带有新action的流表项时候,则需要把action字符串名字解析为相应action类型编号(code),并同时解析action带有的参数。这些在函数str_to_ofpact__中完成:

static char * WARN_UNUSED_RESULT str_to_ofpact__(char *pos, char *act, char *arg, struct ofpbuf *ofpacts, int n_actions, enum ofputil_protocol *usable_protocols)
{
 	int code = ofputil_action_code_from_name(act);
 	if (code >= 0) {
 	   return parse_named_action(code, arg, ofpacts, usable_protocols);
 	} else if (!strcasecmp(act, "drop")) {
...

举例,如“output=2”,act则为action字符串“output”,;则通过ofputil_action_code_from_name解析“output”为OFPUTIL_OFPAT10_OUTPUT,这里面用到了ofp-util.def中宏定义,用最后一个参数进行映射解析到action code,一中已经对新action进行了添加,这里不需要修改。arg则是2,则需要联合刚才的code一起转化为ovs的抽象action(ofpact),则需要在在parse_named_action函数中需要添加相应代码,如下:

case OFPUTIL_OFPAT11_SET_NW_DST:
   		error = str_to_ip(arg, &ofpact_put_SET_IPV4_DST(ofpacts)->ipv4);
    	break;

上面代码是output,set_nw_dst这种有参数类型,如果action无参数,则只需要将error置为NULL,然后生成相应ofpact即可,如下:

case OFPUTIL_OFPAT13_SELF_LEARNING:
    	error = NULL;
    	ofpact_put_##ENUM(ofpacts);
    	break;

4 此外,当instruction为OFPIT_WRITE_ACTIONS类型时候,则会把action写入set集合中,最后按照一定顺序逐一执行。如果想让新的action可以用在这种write类型的instruction中(一般我们都封装在apply的instruction中),则还需要在ofp-action.c中修改如下代码:

/* True if an action is allowed in the action set.
 		* False otherwise. */
static bool
ofpact_is_allowed_in_actions_set(const struct ofpact *a)
{
 	switch (a->type) {
 	case OFPACT_DEC_MPLS_TTL:
 	case OFPACT_DEC_TTL:
...
return true;

为了新action允许写入set集合中,则可仿照上面,在return ture前添加新action的case即可。为了控制新action在set中的执行顺序,需要修改下面代码:

void ofpacts_execute_action_set(struct ofpbuf *action_list,
const struct ofpbuf *action_set)
{
 	/* The OpenFlow spec "Action Set" section specifies this order. */
 	ofpacts_copy_last(action_list, action_set, OFPACT_STRIP_VLAN);
 	ofpacts_copy_last(action_list, action_set, OFPACT_POP_MPLS);
 	ofpacts_copy_last(action_list, action_set, OFPACT_PUSH_MPLS);
 	ofpacts_copy_last(action_list, action_set, OFPACT_PUSH_VLAN);
 	ofpacts_copy_last(action_list, action_set, OFPACT_DEC_TTL);
 	ofpacts_copy_last(action_list, action_set, OFPACT_DEC_MPLS_TTL);
 	ofpacts_copy_all(action_list, action_set, ofpact_is_set_action);
 	ofpacts_copy_last(action_list, action_set, OFPACT_SET_QUEUE);
...

如上代码,把action set中所有action按照右上向下的顺序放入action_list中,待逐一执行action_list中的action。因此,新添加的action,也需要采用如下的格式插入相应位置,即可控制action在set中执行的顺序。:

ofpacts_copy_last(action_list, action_set, OFPACT_##ENUM);

原文发布于微信公众号 - SDNLAB(SDNLAB)

原文发表时间:2015-11-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java帮帮-微信公众号-技术文章全总结

Mybatis_day01

Mybatis_day01 前言 Jdbc演变到mybatis jdbc jdbc编程 publicstaticvoid main(String[] args)...

43270
来自专栏听雨堂

Python防止sql注入

看了网上文章,说的都挺好的,给cursor.execute传递格式串和参数,就能防止注入,但是我写了代码,却死活跑不通,怀疑自己用了一个假的python 最后,...

38070
来自专栏行者悟空

Hadoop之MapReduce原理及运行机制

21040
来自专栏技术记录

JAVA-FTP批量大文件传输

FTP的具体使用      FTP是一种网络协议,用于进行不同服务器主机之间的文件传输,或者简单地说两台不同IP的机器之间的文件传输。在java中我们什么时候需...

82460
来自专栏恰童鞋骚年

Entity Framework 基础知识走马观花

  (1)通过选择以XML方式打开edmx文件,我们可以可以清楚地看到,edmx模型文件本质就是一个XML文件;

11820
来自专栏瓜大三哥

HLS综合策略

Loop:rolled00 Array: BRAM Struct:被分解为成员变量 操作符:硬件核 优化策略 The Initial Optimization...

32370
来自专栏MasiMaro 的技术博文

PE文件详解(六)

这篇文章转载自小甲鱼的PE文件详解系列原文传送门 之前简单提了一下节表和数据目录表,那么他们有什么区别? 其实这些东西都是人为规定的,一个数据在文件中或...

24520
来自专栏哲学驱动设计

优化OEA中的聚合SQL

    之前写过几篇关于聚合对象SQL的文章,讲的是如果设计框架,使用一句SQL语句来加载整个聚合对象树中的所有数据。相关内容,参见:《性能优化总结(二):聚合...

24870
来自专栏MasiMaro 的技术博文

Windows平台下的内存泄漏检测

在C/C++中内存泄漏是一个不可避免的问题,很多新手甚至有许多老手也会犯这样的错误,下面说明一下在windows平台下如何检测内存泄漏。 在windows平...

24820
来自专栏浪淘沙

Hive学习

         Hive是基于Hadoop的一个数据仓库工具(离线),可以将结构化的数据文件映射为一张数据库表,并提供类SQL查询功能。

33820

扫码关注云+社区

领取腾讯云代金券