一、Datapath 模块的行为匹配
在 Open vSwitch 的数据包转发流程中,存在快速路径和慢速路径两种模式,如下图所示:
其中,快速路径直接在 Datapath 模块完成行为匹配,将数据包转发出去。而慢速路径的数据包无法在 Datapath 模块完全处理,需要通过 upcall 调用将数据包交给用户空间的 vswitchd 守护进程,但是最终守护进程会将处理后的数据包和流表发回 Datapath 模块,然后再次进行行为匹配和数据包的转发。
对于快速路径而言,行为匹配发生在 Datapath 模块接收和检查筛选之后,即在数据包处理 ovs_dp_process_packet() 函数中调用:
void ovs_dp_process_packet(struct sk_buff *skb, struct sw_flow_key *key) {
......
error = ovs_execute_actions(dp, skb, sf_acts, key);
......
}
对于慢速路径而言,行为匹配发生在流表下发到 Datapath 模块和数据包通过 reinject 发回之后,即在数据包接收和执行 ovs_packet_cmd_execute() 函数中调用:
static int ovs_packet_cmd_execute(struct sk_buff *skb, struct genl_info *info) {
......
err = ovs_execute_actions(dp, packet, sf_acts, &flow->key);
......
}
相应的调用关系如下图所示:
也就是说,无论是快速路径还是慢速路径,最终都会由 Datapath 模块进行行为匹配,只不过不同的路径需要走不同的流程,会有不同的函数实现。
二、行为匹配 ovs_execute_actions()
函数 ovs_execute_actions() 是 Datapath 模块的核心部分,主要负责行为的匹配和执行,存储在 ovs-main/datapath/actions.c 文件中:
/* Execute a list of actions against 'skb'. */
int ovs_execute_actions(struct datapath *dp, struct sk_buff *skb, const struct sw_flow_actions *acts, struct sw_flow_key *key) {
int err, level;
level = __this_cpu_inc_return(exec_actions_level);
if (unlikely(level > OVS_RECURSION_LIMIT)) {
net_crit_ratelimited("ovs: recursion limit reached on datapath %s, probable configuration error\n", ovs_dp_name(dp));
kfree_skb(skb);
err = -ENETDOWN;
goto out;
}
OVS_CB(skb)->acts_origlen = acts->orig_len;
err = do_execute_actions(dp, skb, key, acts->actions, acts->actions_len);
if (level == 1)
process_deferred_actions(dp);
out:
__this_cpu_dec(exec_actions_level);
return err;
}
函数的第一个输入参数 struct datapath *dp 表示 datapath 对象(代表一个 Open vSwitch 实例在内核空间的实现),第二个输入参数 struct sk_buff *skb 表示要处理的数据包,第三个输入参数 const struct sw_flow_actions *acts 表示要执行的动作,第四个输入参数 struct sw_flow_key *key 表示数据包的流关键字信息。
函数首先维护一个当前 CPU 核心的 exec_actions_level 计数器,并进行一些预处理。然后调用 do_execute_actions(dp, skb, key, acts->actions, acts->actions_len) 函数来执行动作列表,并通过判断 exec_actions_level 计数器的值来限制递归次数。最后如果 exec_actions_level 计数器的值为 1 即表示最外层调用,则调用 process_deferred_actions(dp) 函数来处理延迟的动作。
Tips:这里的 exec_actions_level 计数器主要用于递归限制
Open vSwitch 的 Datapath 模块支持递归执行,以处理嵌套的动作。但为了防止无限递归,代码设置了 OVS_RECURSION_LIMIT 的限制。如果递归深度超过该限制,则会打印警告日志,释放数据包,并返回相应错误码 -ENETDOWN。
Tips:延迟动作处理
数据包的某些行为在匹配完成后,可能需要在后续处理过程中执行而不是立即执行(比如转发行为)。对于 Datapath 模块而言,这些行为会被推迟到最外层调用时统一处理,由 process_deferred_actions() 函数负责处理这些延迟的动作,该函数存储在 ovs-main/datapath/actions.c 文件中:
static void process_deferred_actions(struct datapath *dp) {
struct action_fifo *fifo = this_cpu_ptr(action_fifos);
/* Do not touch the FIFO in case there is no deferred actions. */
if (action_fifo_is_empty(fifo))
return;
/* Finishing executing all deferred actions. */
do {
struct deferred_action *da = action_fifo_get(fifo);
struct sk_buff *skb = da->skb;
struct sw_flow_key *key = &da->pkt_key;
const struct nlattr *actions = da->actions;
int actions_len = da->actions_len;
if (actions)
do_execute_actions(dp, skb, key, actions, actions_len);
else
ovs_dp_process_packet(skb, key);
} while (!action_fifo_is_empty(fifo));
/* Reset FIFO for the next packet. */
action_fifo_init(fifo);
}
三、行为执行 do_execute_actions()
函数 do_execute_actions() 的主要作用是针对不同的数据包类型执行相应的行为,存储在 ovs-main/datapath/actions.c 文件中:
/* Execute a list of actions against 'skb'. */
static int do_execute_actions(struct datapath *dp, struct sk_buff *skb, struct sw_flow_key *key, const struct nlattr *attr, int len) {
const struct nlattr *a;
int rem;
for (a = attr, rem = len; rem > 0; a = nla_next(a, &rem)) {
int err = 0;
switch (nla_type(a)) {
case OVS_ACTION_ATTR_OUTPUT: {
int port = nla_get_u32(a);
struct sk_buff *clone;
/* Every output action needs a separate clone of 'skb', In case the output action is the last action, cloning can be avoided. */
if (nla_is_last(a, rem)) {
do_output(dp, skb, port, key);
/* 'skb' has been used for output. */
return 0;
}
clone = skb_clone(skb, GFP_ATOMIC);
if (clone)
do_output(dp, clone, port, key);
OVS_CB(skb)->cutlen = 0;
break;
}
case OVS_ACTION_ATTR_TRUNC: {
struct ovs_action_trunc *trunc = nla_data(a);
if (skb->len > trunc->max_len)
OVS_CB(skb)->cutlen = skb->len - trunc->max_len;
break;
}
case OVS_ACTION_ATTR_USERSPACE:
output_userspace(dp, skb, key, a, attr, len, OVS_CB(skb)->cutlen);
OVS_CB(skb)->cutlen = 0;
break;
case OVS_ACTION_ATTR_HASH:
execute_hash(skb, key, a);
break;
case OVS_ACTION_ATTR_PUSH_MPLS:
err = push_mpls(skb, key, nla_data(a));
break;
case OVS_ACTION_ATTR_POP_MPLS:
err = pop_mpls(skb, key, nla_get_be16(a));
break;
case OVS_ACTION_ATTR_PUSH_VLAN:
err = push_vlan(skb, key, nla_data(a));
break;
case OVS_ACTION_ATTR_POP_VLAN:
err = pop_vlan(skb, key);
break;
case OVS_ACTION_ATTR_RECIRC: {
bool last = nla_is_last(a, rem);
err = execute_recirc(dp, skb, key, a, last);
if (last) {
/* If this is the last action, the skb has been consumed or freed.
* Return immediately. */
return err;
}
break;
}
case OVS_ACTION_ATTR_SET:
err = execute_set_action(skb, key, nla_data(a));
break;
case OVS_ACTION_ATTR_SET_MASKED:
case OVS_ACTION_ATTR_SET_TO_MASKED:
err = execute_masked_set_action(skb, key, nla_data(a));
break;
case OVS_ACTION_ATTR_SAMPLE: {
bool last = nla_is_last(a, rem);
err = sample(dp, skb, key, a, last);
if (last)
return err;
break;
}
case OVS_ACTION_ATTR_CT:
if (!is_flow_key_valid(key)) {
err = ovs_flow_key_update(skb, key);
if (err)
return err;
}
err = ovs_ct_execute(ovs_dp_get_net(dp), skb, key, nla_data(a));
/* Hide stolen IP fragments from user space. */
if (err)
return err == -EINPROGRESS ? 0 : err;
break;
case OVS_ACTION_ATTR_CT_CLEAR:
err = ovs_ct_clear(skb, key);
break;
case OVS_ACTION_ATTR_PUSH_ETH:
err = push_eth(skb, key, nla_data(a));
break;
case OVS_ACTION_ATTR_POP_ETH:
err = pop_eth(skb, key);
break;
case OVS_ACTION_ATTR_PUSH_NSH: {
u8 buffer[NSH_HDR_MAX_LEN];
struct nshhdr *nh = (struct nshhdr *)buffer;
err = nsh_hdr_from_nlattr(nla_data(a), nh, NSH_HDR_MAX_LEN);
if (unlikely(err))
break;
err = push_nsh(skb, key, nh);
break;
}
case OVS_ACTION_ATTR_POP_NSH:
err = pop_nsh(skb, key);
break;
case OVS_ACTION_ATTR_METER:
if (ovs_meter_execute(dp, skb, key, nla_get_u32(a))) {
consume_skb(skb);
return 0;
}
break;
case OVS_ACTION_ATTR_CLONE: {
bool last = nla_is_last(a, rem);
err = clone(dp, skb, key, a, last);
if (last)
return err;
break;
}
case OVS_ACTION_ATTR_CHECK_PKT_LEN: {
bool last = nla_is_last(a, rem);
err = execute_check_pkt_len(dp, skb, key, a, last);
if (last)
return err;
break;
}
}
if (unlikely(err)) {
kfree_skb(skb);
return err;
}
}
consume_skb(skb);
return 0;
}
函数的第一个输入参数 struct datapath *dp 表示 datapath 对象(代表一个 Open vSwitch 实例在内核空间的实现),第二个输入参数 struct sk_buff *skb 表示要处理的数据包,第三个输入参数 struct sw_flow_key *key 表示数据包的流关键字信息,第四个输入参数 const struct nlattr *attr 表示要执行的操作列表,第五个输入参数 int len 表示操作列表的长度。
函数使用 for 循环遍历操作列表,对于每个操作而言,它都会根据 nla_type(a) 类型执行相应的行为。支持的操作类型包括:
- OVS_ACTION_ATTR_OUTPUT 将数据包发送到指定端口
- OVS_ACTION_ATTR_TRUNC 截断数据包长度
- OVS_ACTION_ATTR_USERSPACE 将数据包发送到用户空间
- OVS_ACTION_ATTR_HASH 计算数据包的哈希值
- OVS_ACTION_ATTR_PUSH_MPLS 为数据包添加 MPLS 头部
- OVS_ACTION_ATTR_POP_MPLS 从数据包中删除 MPLS 头部
- OVS_ACTION_ATTR_PUSH_VLAN 为数据包添加 VLAN 头部
- OVS_ACTION_ATTR_POP_VLAN 从数据包中删除 VLAN 头部
- OVS_ACTION_ATTR_RECIRC 重新循环处理数据包
- OVS_ACTION_ATTR_SET 设置数据包的特定字段
- OVS_ACTION_ATTR_SET_MASKED 使用掩码设置数据包的特定字段
- OVS_ACTION_ATTR_SAMPLE 对数据包进行采样操作
- OVS_ACTION_ATTR_CT 执行连接跟踪操作
- OVS_ACTION_ATTR_CT_CLEAR 清除数据包的连接跟踪状态
- OVS_ACTION_ATTR_PUSH_ETH 为数据包添加以太网头部
- OVS_ACTION_ATTR_POP_ETH 从数据包中删除以太网头部
- OVS_ACTION_ATTR_PUSH_NSH 为数据包添加 NSH (网络服务头) 头部
- OVS_ACTION_ATTR_POP_NSH 从数据包中删除 NSH 头部
- OVS_ACTION_ATTR_METER 执行数据包计量操作
- OVS_ACTION_ATTR_CLONE 克隆数据包
- OVS_ACTION_ATTR_CHECK_PKT_LEN 检查数据包长度
对于每个操作而言,如果执行过程中发生错误,则直接释放数据包并返回错误代码。如果所有操作都执行成功,则最终释放原始数据包。
总结:
在 Open vSwitch 的 Datapath 模块中,主要通过 ovs_execute_actions() 函数进行相应行为的匹配和执行,这是数据包在 Datapath 模块中必须执行的步骤。
由于本人水平有限,以上内容如有不足之处欢迎大家指正(评论区/私信均可)。
参考资料:
Open vSwitch 官网
Open vSwitch 源代码 GitHub
Open vSwitch 数据包接收的实现-CSDN博客
Open vSwitch 的 reinject 数据包发回-CSDN博客
Open vSwitch v2.17.10 LTS 源代码