DPDK中ACL(Access Control List)的使用

29 May 2016 | DPDK, ACL | | ˚C

什么是ACL

DPDK中的ACL(Access Control List)模块提供了数据包分类功能,根据文档Packet Classification and Access Control中所述:

The ACL library is used to perform an N-tuple search over a set of rules with multiple categories and find the best match (highest priority) for each category.

大体来说,就是事先定义一些基于数据包N元组的规则,每一个规则都有一个优先级,并且归属到一个或多个不同类别;然后对于输入的数据包进行规则检查,如果该数据包满足一个类别里的某些规则,返回结果是其中优先级最高的那个,如果类别中的规则都不满足就返回0.显然,设定了多少个类别就会有多少个匹配结果。

举例来说,编写基于ipv4 的5元组{proto, ip_src, ip_dst, port_src, port_dst}的规则:

Example 1:

rule 1: 返回值:1 类别:0,1 优先级:1 ip_dst:192.168.0.0/16 port_src:8000-9000
rule 2: 返回值:2 类别:0   优先级:2 ip_dst:192.168.1.0/24
rule 3: 返回值:3 类别:1   优先级:3 ip_src:10.1.1.1/32    port_dst:1000-2000

假如一个数据包的ip_src为10.1.1.1, ip_dst为192.168.1.15, port_src为8080, port_dst为1500, 在类别0中, 同时满足rule1和rule2, 由于rule2的优先级更高,因此返回2; 在类别1中, 满足rule3, 因此返回3.

通过上述例子可以看出,使用ACL的关键在于:

规则定义

ACL的规则是基于N元组的,对N元组内的每一个字段来设置具体的规则内容.其字段定义如下:

rte_acl.h:

struct rte_acl_field_def {
	uint8_t  type;        /**< type - RTE_ACL_FIELD_TYPE_*. */
	uint8_t	 size;        /**< size of field 1,2,4, or 8. */
	uint8_t	 field_index; /**< index of field inside the rule. */
	uint8_t  input_index; /**< 0-N input index. */
	uint32_t offset;      /**< offset to start of field. */
};

各字段的意义:

需要注意的是,基于优化考虑,DPDK在内部实现时对输入的N元组数据进行4字节分组(group),具体来说,第0个字段要求必须是1字节(size为1),属于第0组(input_field为0),后面每4字节算作一组,所以除了第0个字段以外,其余字段的size必须要能凑成4字节的分组。例如,字段大小分别设置为(1, 2, 1, 2, 2)就是错误的,因为2+1=3, 2+1+2=5, 字段3跨越了group1和group2。

了解了ACL字段之后, 我们就可以对具体的N元组定义规则了。以IPv4的5元组为例:

 struct ipv4_5tuple {
    uint8_t proto;
    uint32_t ip_src;
    uint32_t ip_dst;
    uint16_t port_src;
    uint16_t port_dst;
};
struct rte_acl_field_def ipv4_defs[5] = {
    /* first input field - always one byte long. */
    {
        .type = RTE_ACL_FIELD_TYPE_BITMASK,
        .size = sizeof (uint8_t),
        .field_index = 0,
        .input_index = 0,
        .offset = offsetof (struct ipv4_5tuple, proto),
    },
    /* next input field (IPv4 source address) - 4 consecutive bytes. */
    {
        .type = RTE_ACL_FIELD_TYPE_MASK,
        .size = sizeof (uint32_t),
        .field_index = 1,
        .input_index = 1,
       .offset = offsetof (struct ipv4_5tuple, ip_src),
    },
    /* next input field (IPv4 destination address) - 4 consecutive bytes. */
    {
        .type = RTE_ACL_FIELD_TYPE_MASK,
        .size = sizeof (uint32_t),
        .field_index = 2,
        .input_index = 2,
       .offset = offsetof (struct ipv4_5tuple, ip_dst),
    },
    /*
     * Next 2 fields (src & dst ports) form 4 consecutive bytes.
     * They share the same input index.
     */
    {
        .type = RTE_ACL_FIELD_TYPE_RANGE,
        .size = sizeof (uint16_t),
        .field_index = 3,
        .input_index = 3,
        .offset = offsetof (struct ipv4_5tuple, port_src),
    },
    {
        .type = RTE_ACL_FIELD_TYPE_RANGE,
        .size = sizeof (uint16_t),
        .field_index = 4,
        .input_index = 3,
        .offset = offsetof (struct ipv4_5tuple, port_dst),
    },
};

最后,使用宏RTE_ACL_RULE_DEF定义rule结构体:

RTE_ACL_RULE_DEF(acl_ipv4_rule, RTE_DIM(ipv4_defs));

该宏的定义如下:

rte_acl.h:

#define	RTE_ACL_RULE_DEF(name, fld_num)	struct name {\
	struct rte_acl_rule_data data;               \
	struct rte_acl_field field[fld_num];         \
}

这样,我们的ipv4规则就被定义为struct acl_ipv4_rule

规则编写

有了规则定义,就可以按照需求编写具体的规则。在实际应用中,分类规则一般是由配置文件等方式按需配置,这里用最简单的硬编码来编写Example 1中的规则:

struct acl_ipv4_rule acl_rules[] = {
    {
        .data = {.userdata = 1, .category_mask = 3, .priority = 1},
        .field[2] = {.value.u32 = IPv4(192,168,0,0),. mask_range.u32 = 16,},
        .field[3] = {.value.u16 = 8000, .mask_range.u16 = 9000,},
        .field[4] = {.value.u16 = 0, .mask_range.u16 = 0xffff,},
    },
    {
        .data = {.userdata = 2, .category_mask = 1, .priority = 2},
        .field[2] = {.value.u32 = IPv4(192,168,1,0),. mask_range.u32 = 24,},
        .field[3] = {.value.u16 = 0, .mask_range.u16 = 0xffff,},
        .field[4] = {.value.u16 = 0, .mask_range.u16 = 0xffff,},
    },
    {
        .data = {.userdata = 3, .category_mask = 2, .priority = 3},
        .field[1] = {.value.u32 = IPv4(10,1,1,1),. mask_range.u32 = 32,},
        .field[3] = {.value.u16 = 0, .mask_range.u16 = 0xffff,},
        .field[4] = {.value.u16 = 1000, .mask_range.u16 = 2000,},
    },

};

下面解释一下编写方法。这里的struct acl_ipv4_rule就是刚刚通过RTE_ACL_RULE_DEF定义的规则类型,其中包含两个结构体字段datafield

data字段的结构体定义为:

struct rte_acl_rule_data {
	uint32_t category_mask; /**< Mask of categories for that rule. */
	int32_t  priority;      /**< Priority for that rule. */
	uint32_t userdata;      /**< Associated with the rule user data. */
};

其中category_mask是类别掩码,32位中每一位代表一个类别,该规则属于哪个类别就将哪一位置1;priority是优先级,值越大表明优先级越高;userdata是用户自定义的一个数值(不能是0),当某个规则匹配成功时,就会返回该规则的userdata

field字段的结构体定义为:

struct rte_acl_field {
	union rte_acl_field_types value;
	/**< a 1,2,4, or 8 byte value of the field. */
	union rte_acl_field_types mask_range;
	/**<
	 * depending on field type:
	 * mask -> 1.2.3.4/32 value=0x1020304, mask_range=32,
	 * range -> 0 : 65535 value=0, mask_range=65535,
	 * bitmask -> 0x06/0xff value=6, mask_range=0xff.
	 */
};

其中联合体union rte_acl_field_types定义为:

union rte_acl_field_types {
	uint8_t  u8;
	uint16_t u16;
	uint32_t u32;
	uint64_t u64;
};

对应了1,2,4,8四种字段长度。field字段要根据先前定义的字段类型设置其值,具体来说:

执行匹配

要执行匹配,首先需要创建规则上下文,相关代码如下:

struct rte_acl_ctx * acx;
int ret;
struct rte_acl_param prm = {
    .name = "ACL_example",
    .socket_id = SOCKET_ID_ANY,
    .rule_size = RTE_ACL_RULE_SZ(RTE_DIM(ipv4_defs)),
    .max_rule_num = 8,
};
if ((acx = rte_acl_create(&prm)) == NULL) {
    /* handle context create failure. */
}

接下来需要在刚刚创建的空上下文中添加规则:

ret = rte_acl_add_rules(acx, acl_rules, RTE_DIM(acl_rules));
if (ret != 0) {
   /* handle error at adding ACL rules. */
}

这样,就把Example 1中的规则加入到上下文中。

然后需要构建运行时结构:

struct rte_acl_config cfg;
cfg.num_categories = 2;
cfg.num_fields = RTE_DIM(ipv4_defs);
cfg.max_size = 0x800000;
memcpy(cfg.defs, ipv4_defs, sizeof (ipv4_defs));
ret = rte_acl_build(acx, &cfg);
if (ret != 0 && ret != -ERANGE) {
     /* handle error at build runtime structures for ACL context. */
} else if (ret == -ERANGE) {
   cfg.max_size = 0;
   ret = rte_acl_build(acx, &cfg);
}

这里的cfg是构建运行时结构的配置,其定义如下:

struct rte_acl_config {
	uint32_t num_categories; /**< Number of categories to build with. */
	uint32_t num_fields;     /**< Number of field definitions. */
	struct rte_acl_field_def defs[RTE_ACL_MAX_FIELDS];
	/**< array of field definitions. */
	size_t max_size;
	/**< max memory limit for internal run-time structures. */
};

其他字段都很好理解,主要注意其中的max_size字段,该字段用来规定ACL运行时的内存上限,但是如果内存过小会增加分类时间,因此这是一个折中的设置选项。如果设置为0则会尽量减少内存占用,但不会保证使用限度。

至此执行规则的上下文创建完毕。对于一组输入数据,只要通过函数rte_acl_classify即可执行匹配,该函数原型如下:

int rte_acl_classify(const struct rte_acl_ctx *ctx,
		 const uint8_t **data,
		 uint32_t *results, uint32_t num,
		 uint32_t categories);

其中ctx即构建好的上下文了;data可以理解为一个指针数组:const uint8_t *data[INPUT_COUNT],其每一个元素指针指向一条 待匹配二进制数据。注意要进行匹配的二进制数据必须是按照网络字节序存储的;num表示data中的数据数量;categories是类别的数量;results是用来保存结果的数组,由于data中每一条数据都有categories个匹配结果,因此results的大小至少为categories * num


Older · View Archive (22)

关于init_MUTEX()的消失的探索

Newer

OpenSUSE Leap在线更新到Tumbleweed小记