ovs源码分析

源码版本

ovs 内核部分的代码在linux内核的 /net/openswitch目录下,应用层控制面代码在ovs项目中。

  • Linux kernel: version 6.2.0

  • Ovs: v3.4.1

总体架构

整体结构图

ovs的架构如下图所示,主要由内核datapath、vswitchd、ovsdb以及用户空间的ovs-vsctl/ovs-ofctl/ovs-dpctl等组成。

  • vswitchd是一个守护进程,是ovs的管理和控制服务,通过unix socket将配置信息保存到ovsdb,并通过netlink和内核模块交互。

  • ovsdb则是ovs的数据库,保存了ovs配置信息。

  • datapath是负责数据交换的内核模块,比如把接受端口收到的包放到流表中进行匹配,并执行匹配后的动作等。它在初始化和port binding的时候注册钩子函数,把端口的报文处理接管到内核模块。

实机环境

ovs 守护进程

bash 复制代码
root@node1:~# ps -aux|grep vswitchd
root       906  3.5  0.7 531888 74196 ?        S<Lsl 17:40   0:01 ovs-vswitchd unix:/var/run/openvswitch/db.sock -vconsole:emer -vsyslog:err -vfile:info --mlockall --no-chdir --log-file=/var/log/openvswitch/ovs-vswitchd.log --pidfile=/var/run/openvswitch/ovs-vswitchd.pid --detach

ovsdb 守护进程

bash 复制代码
root@node1:~# ps -aux|grep ovsdb
root      5904  0.0  0.0  14900  3400 ?        S<s  17:40   0:00 ovsdb-server: monitoring pid 5905 (healthy)
root      5905  0.4  0.0  16220  8392 ?        S<   17:40   0:00 ovsdb-server /etc/openvswitch/conf.db -vconsole:emer -vsyslog:err -vfile:info --remote=punix:/var/run/openvswitch/db.sock --private-key=db:Open_vSwitch,SSL,private_key --certificate=db:Open_vSwitch,SSL,certificate --bootstrap-ca-cert=db:Open_vSwitch,SSL,ca_cert --no-chdir --log-file=/var/log/openvswitch/ovsdb-server.log --pidfile=/var/run/openvswitch/ovsdb-server.pid --detach --monitor
root      6925  0.0  0.0  14904  3408 ?        Ss   17:40   0:00 ovsdb-server: monitoring pid 120 (healthy)
root      6926  0.1  0.0 163628  8080 ?        Sl   17:40   0:00 ovsdb-server -vconsole:off -vfile:info --log-file=/var/log/ovn/ovsdb-server-nb.log --remote=punix:/var/run/ovn/ovnnb_db.sock --pidfile=/var/run/ovn/ovnnb_db.pid --unixctl=/var/run/ovn/ovnnb_db.ctl --detach --monitor --remote=db:OVN_Northbound,NB_Global,connections --private-key=db:OVN_Northbound,SSL,private_key --certificate=db:OVN_Northbound,SSL,certificate --ca-cert=db:OVN_Northbound,SSL,ca_cert --ssl-protocols=db:OVN_Northbound,SSL,ssl_protocols --ssl-ciphers=db:OVN_Northbound,SSL,ssl_ciphers --remote=ptcp:6641:[::] /etc/ovn/ovnnb_db.db
root      7066  0.0  0.0  14904  3408 ?        Ss   17:40   0:00 ovsdb-server: monitoring pid 143 (healthy)
root      7068  0.1  0.1 165384 10172 ?        Sl   17:40   0:00 ovsdb-server -vconsole:off -vfile:info --log-file=/var/log/ovn/ovsdb-server-sb.log --remote=punix:/var/run/ovn/ovnsb_db.sock --pidfile=/var/run/ovn/ovnsb_db.pid --unixctl=/var/run/ovn/ovnsb_db.ctl --detach --monitor --remote=db:OVN_Southbound,SB_Global,connections --private-key=db:OVN_Southbound,SSL,private_key --certificate=db:OVN_Southbound,SSL,certificate --ca-cert=db:OVN_Southbound,SSL,ca_cert --ssl-protocols=db:OVN_Southbound,SSL,ssl_protocols --ssl-ciphers=db:OVN_Southbound,SSL,ssl_ciphers --remote=ptcp:6642:[::] /etc/ovn/ovnsb_db.db
root     20535  0.0  0.0  12300  2816 pts/0    S+   17:42   0:00 grep --color=auto ovsdb

ovs内核模块

bash 复制代码
root@node1:~# lsmod |grep openvswitch
openvswitch           212992  18
nsh                    12288  1 openvswitch
nf_conncount           24576  1 openvswitch
nf_nat                 61440  4 ip6table_nat,openvswitch,iptable_nat,xt_MASQUERADE
nf_conntrack          208896  8 xt_conntrack,nf_nat,nfnetlink_cttimeout,openvswitch,nf_conntrack_netlink,nf_conncount,xt_MASQUERADE,ip_vs
libcrc32c              12288  5 nf_conntrack,nf_nat,openvswitch,nf_tables,ip_vs

关键概念

Datapath

使用kubesphere 开启ovn 安装的单节点集群。

Datapath(数据通路):在网络交换机和路由器中,数据通路是指转发数据包的路径。在 OVS 中,数据通路是用于处理和转发数据包的核心组件,查询到的网桥 br-int, 是ovs在所在节点创建的虚拟网桥。

VPort

VPort 实现:在 OVS 中,VPort 是通过内核模块和用户态进程协同工作来实现的。内核模块负责处理数据包的转发和接收,而用户态进程则负责控制和管理 VPort。

VPort 标识:每个 VPort 都有一个唯一的标识符,用于在 OVS 数据结构中进行识别和管理。

VPort 的使用场景

  • 数据包处理:当数据包到达一个 VPort 时,内核模块会将数据包从物理设备或其他 VPort 接收,并根据流表规则进行处理。处理可能包括数据包转发、过滤、NAT 等操作。

  • 与虚拟机连接:VPort 通常用于连接虚拟机或容器。通过 VPort,虚拟机可以与 OVS 进行通信,实现虚拟网络的构建和管理。

  • 虚拟交换机间通信:除了连接虚拟机外,VPort 也可以用于连接不同的虚拟交换机,实现虚拟网络之间的通信。

  • 管理和配置:用户态进程负责管理和配置 VPort,包括创建、删除、绑定到特定虚拟机等操作。

总的来说,VPort 在 OVS 中扮演了连接虚拟网络设备的重要角色,通过 VPort,OVS 能够实现虚拟网络的构建、管理和数据包处理。

  • 其中internal 接口类型是一个特定的端口类型,通常会被标记为 type: internal。它的流量通常不会直接暴露到物理网络中。

  • 这种类型的接口通常不具备物理网络接口的一些功能,例如无需设置 MAC 地址或与物理设备的接口绑定。

端口特性

下面是查询到的ovs具体场景中的网卡

bash 复制代码
root@node1:~# ovs-vsctl show
7fb85a99-5ae5-4321-938f-fc647f81c52d
    Bridge br-int                    //网桥
        fail_mode: secure
        datapath_type: system
        Port br-int
            Interface br-int         //代表网桥本身的端口,其他端口的流量会通过流入该端口进入ovs
                type: internal       
        Port mirror0
            Interface mirror0       // 主要用于调试数据的流量镜像端口(ovn创建,非ovs核心功能端口)
                type: internal
        Port "9c9076b62a19_h"        // 连接业务容器的端口
            Interface "9c9076b62a19_h"    
        Port c42b8d13d6ea_h
            Interface c42b8d13d6ea_h // 连接业务容器的端口
        Port "8f5f5cc5dc66_h"
            Interface "8f5f5cc5dc66_h"
        Port ovn0                   // ovn创建的作为网关的端口(ovn创建,非ovs核心功能端口)
            Interface ovn0
                type: internal
       //...
    ovs_version: "2.17.3"

对于OVS来讲,几种网卡类型的特性如下

  1. netdev: 通用网卡设备 eth0 veth

接收: 一个nedev在L2收到报文后会直接通过ovs接收函数处理,不会再走传统内核协议栈.

发送: ovs中的一条流指定从该netdev发出的时候就通过该网卡设备发送

  1. internal: 一种虚拟网卡设备

接收: 当从系统发出的报文路由查找通过该设备发送的时候,就进入ovs接收处理函数

发送: ovs中的一条流指定从该internal设备发出的时候,该报文被重新注入内核协议栈

  1. gre device: gre设备. 不管用户态创建多少个gre tunnel, 在内核态有且只有一个gre设备

接收: 当系统收到gre报文后,传递给L4层解析gre header, 然后传递给ovs接收处理函数

发送: ovs中的一条流制定从该gre设备发送, 报文会根据流表规则加上gre头以及外层包裹ip,查找路由发送

br-int端口

  1. 基本功能
  • 作为虚拟设备的接口 Interface br-int 是与网桥 br-int 关联的虚拟网络设备,代表网桥自身。通过它,主机可以与网桥通信。

  • 配置和控制 网桥 作为网桥的内部接口,它用于接收管理流量(如配置命令)或处理流量转发规则。


  1. 工作原理
  • 桥接点 br-int 作为集成网桥,通过 Interface br-int 与主机的网络栈交互。主机发送到 br-int 的数据包,实际上会通过 Interface br-int 进入 OVS 数据路径。

  • 辅助实现内部通信 br-int 网桥的所有端口(如连接容器的端口)之间的数据流量最终会通过 br-int 的转发逻辑实现。


  1. 使用场景
  • OpenStack 和 OVN 在云平台(如 OpenStack)和虚拟网络(如 OVN)中,br-int 是虚拟机和容器网络的核心桥梁。通过 Interface br-int,虚拟机、容器、以及外部网关可以完成通信。

  • 调试和监控 系统管理员可以使用工具(如 tcpdump)在 Interface br-int 上抓包,检查流量流动或排查问题。


  1. 示例作用

假设一个虚拟机通过一个端口(如 tap0)连接到 br-int 网桥。流量路径如下:

  1. 数据从虚拟机发出,经 tap0 端口进入 br-int

  2. br-int 根据流表规则处理数据流,可能转发到其他端口。

  3. 如果数据需要发到主机,流量会通过 Interface br-int 进入主机网络栈。


总结

Interface br-int 的作用可以概括为:

  1. 作为网桥 br-int 的内部接口,与主机网络栈交互。

  2. 提供一个管理和调试的入口。

  3. 在虚拟化环境中支持虚拟机、容器与主机之间的通信。

它是网桥 br-int 功能实现不可或缺的一部分。

流表

查看该网桥流表信息, 并选取几个流表项进行分析

Flow Table(流表):流表是 OVS 数据通路中的一种数据结构,用于存储和匹配数据包流的规则。当数据包到达 OVS 时,将根据流表中的规则进行匹配,并决定如何处理数据包。

bash 复制代码
root@node1:~# ovs-ofctl dump-flows br-int
// 数据包的输入端口是 "8f5f5cc5dc66_h",这看起来是连接到业务容器的一个端口, 该流将提交到8号流表继续处理
 cookie=0x90ed7d93, duration=135.488s, table=0, n_packets=7447, n_bytes=15326624, priority=100,in_port=ovn0 actions=load:0x2->NXM_NX_REG13[],load:0x1->NXM_NX_REG11[],load:0x7->NXM_NX_REG12[],load:0x2->OXM_OF_METADATA[],load:0x2->NXM_NX_REG14[],resubmit(,8)
 // 11 号流表 匹配 IPv6 ICMP 类型 136(邻居请求)的数据包,元数据字段为 0x1,网络 TTL 为 255,ICMP 类型为 136,ICMP 代码为 0
 // 该类流将丢弃
 cookie=0x92283387, duration=41.987s, table=11, n_packets=0, n_bytes=0, idle_age=41, priority=85,icmp6,metadata=0x1,nw_ttl=255,icmp_type=136,icmp_code=0 actions=drop
 // 该流表 匹配 SCTP 协议的数据包(sctp, reg0=0x4/0x4, metadata=0x2) 执行连接跟踪(ct),提交到流表 16
 // 使用寄存器 NXM_NX_REG13 为连接跟踪区(zone),执行 NAT。
 cookie=0xf7ce3e3a, duration=41.991s, table=15, n_packets=0, n_bytes=0, idle_age=41, priority=120,sctp,reg0=0x4/0x4,metadata=0x2 actions=move:NXM_OF_IP_DST[]->NXM_NX_XXREG0[64..95],move:OXM_OF_SCTP_DST[]->NXM_NX_XXREG0[32..47],ct(table=16,zone=NXM_NX_REG13[0..15],nat)
 // 这个流表项用于处理 TCP 数据包,执行连接跟踪和 NAT 转换。
 cookie=0xf0ac6d85, duration=41.995s, table=70, n_packets=0, n_bytes=0, idle_age=41, priority=100,tcp,reg1=0xae90ee0,reg2=0x19f3/0xffff actions=ct(commit,zone=NXM_NX_REG12[0..15],nat(src=10.233.14.224))

数据流向

一般的数据包在linux网络协议栈中的流向为黑色箭头流向:从网卡上接受到数据包后层层往上分析,最后离开内核态,把数据传送到用户态。当然也有些数据包只是在内核网络协议栈中操作,然后再从某个网卡发出去。

但当其中有openVswitch时,数据包的流向就不一样了。首先是创建一个网桥:ovs-vsctl add-br br0;然后是绑定某个网卡:绑定网卡:ovs-vsctl add-port br0 eth0;这里默认为绑定了eth0网卡。数据包的流向是从网卡eth0上然后到openVswitch的端口vport上进入openVswitch中,然后根据key值进行流表的匹配。

如果匹配成功,则根据流表中对应的action找到其对应的操作方法,完成相应的动作(这个动作有可能是把一个请求变成应答,也有可能是直接丢弃,也可以自己设计自己的action);

如果匹配不成功,则执行默认的动作,有可能是放回内核网络协议栈中去处理(在创建网桥时就会相应的创建一个端口连接内核协议栈的)。

源码分析

数据结构

  • 网桥模块datapath:
cpp 复制代码
/**
 * struct datapath - datapath for flow-based packet switching
 * @rcu: RCU callback head for deferred destruction.
 * @list_node: Element in global 'dps' list.
 * @table: flow table.
 * @ports: Hash table for ports.  %OVSP_LOCAL port always exists.  Protected by
 * ovs_mutex and RCU.
 * @stats_percpu: Per-CPU datapath statistics.
 * @net: Reference to net namespace.
 * @max_headroom: the maximum headroom of all vports in this datapath; it will
 * be used by all the internal vports in this dp.
 * @upcall_portids: RCU protected 'struct dp_nlsk_pids'.
 *
 * Context: See the comment on locking at the top of datapath.c for additional
 * locking information.
 */
 
struct datapath {
    struct rcu_head rcu;
    struct list_head list_node;  //将网桥连接为链组织

    /* Flow table. */
    struct flow_table table;    // 网桥版定的流表

    /* Switch ports. */
    struct hlist_head *ports;    //网桥绑定的端口

    /* Stats. */
    struct dp_stats_percpu __percpu *stats_percpu;

    /* Network namespace ref. */
    possible_net_t net;

    u32 user_features;

    u32 max_headroom;

    /* Switch meters. */
    struct dp_meter_table meter_tbl;

    struct dp_nlsk_pids __rcu *upcall_portids;
};
  • vport端口:
cpp 复制代码
/**
 * struct vport - one port within a datapath
 * @dev: Pointer to net_device.
 * @dev_tracker: refcount tracker for @dev reference
 * @dp: Datapath to which this port belongs.
 * @upcall_portids: RCU protected 'struct vport_portids'.
 * @port_no: Index into @dp's @ports array.
 * @hash_node: Element in @dev_table hash table in vport.c.
 * @dp_hash_node: Element in @datapath->ports hash table in datapath.c.
 * @ops: Class structure.
 * @upcall_stats: Upcall stats of every ports.
 * @detach_list: list used for detaching vport in net-exit call.
 * @rcu: RCU callback head for deferred destruction.
 */
struct vport {
    struct net_device *dev;        //指向网络设备结构体的指针,表示与此虚拟端口相关联的网络设备
    netdevice_tracker dev_tracker;
    struct datapath *dp;                //端口关联的网桥
    struct vport_portids __rcu *upcall_portids;
    u16 port_no;                        //端口号,唯一标识

    struct hlist_node hash_node;
    struct hlist_node dp_hash_node;
    const struct vport_ops *ops;       //指向虚拟端口操作函数集
    struct vport_upcall_stats_percpu __percpu *upcall_stats;

    struct list_head detach_list;
    struct rcu_head rcu;
};
  • 流表结构flow_table
cpp 复制代码
// 表通用结构,该表的buckets 是alloc的定长数组,数组中每个格子存放的是 对应数据的双向链表
// table_instance.buckects = malloc(sizeof(hlist_head) * n_buckets)
struct table_instance {
    struct hlist_head *buckets;    // 链表数组
    unsigned int n_buckets;        // buckets 数组长度
    struct rcu_head rcu;
    int node_ver;
    u32 hash_seed;
};
// 流表管理
struct flow_table {
    // 流表的主哈希表,存储流表项(flow entries),
    // 这些流表项是通过五元组匹配字段(如源IP、目的IP、端口等)组织的。
    struct table_instance __rcu *ti;
    //指向以 UFID(Unique Flow Identifier) 为索引的辅助哈希表。
    // 用于快速查找流表项,通过 UFID(一种全局唯一标识符)来定位特定流表项。
    struct table_instance __rcu *ufid_ti;
    // 流表的掩码缓存(mask cache)。提高流表查找效率,通过缓存最近使用的掩码
    //(mask)来减少掩码匹配的开销。掩码通常用于实现子网匹配或特定字段的模糊匹配。
    struct mask_cache __rcu *mask_cache;
    // 流表的掩码数组。
    // 存储所有有效的掩码,供流表匹配时使用。掩码是流表项中用于选择性匹配某些字段的模板。
    struct mask_array __rcu *mask_array;
    // 记录上次重哈希(rehash)的时间戳。
    // 在负载因子达到一定阈值时,触发对流表哈希的重构,以保持高效的查找性能。
    unsigned long last_rehash;
    // 当前流表中存储的流表项数量。
    // 提供流表的大小信息,用于控制哈希表的扩展或缩小。
    unsigned int count;
    // 以 UFID 为索引的流表项数量。
    // 记录辅助哈希表中的流表项数量,用于管理和调试。
    unsigned int ufid_count;
};

哈希表 ( ti ) 和辅助哈希表 ( ufid_ti ) 的协作

哈希表 ( ti )

以流的匹配字段(如源IP、目的IP、端口等五元组)为键存储流表项。

主要用于数据平面快速匹配数据包。

辅助 哈希表 ( ufid_ti )

UFID(Unique Flow Identifier) 作为键存储流表项。

主要用于控制平面通过 UFID 操作流表(例如添加、删除、更新流表项)。

UFID 是一个全局唯一的标识符,解决了复杂匹配条件下的直接查找问题。

配合方式

数据平面:通过 ti 使用匹配字段快速定位流表项。

控制平面:通过 ufid_ti 使用 UFID 高效地对流表项进行管理。

这些哈希表之间的数据同步通过流表项的生命周期管理机制完成。

掩码数组(mask_array)

mask_array 是一个包含流表项掩码(mask)的数组。掩码在流表匹配中用于定义如何比较流表项的特定字段。mask_array 中的掩码有助于优化流表的匹配过程,使得 OVS 可以更高效地处理网络流量。掩码通常用于匹配IP地址、端口号等字段中的某些部分(例如子网匹配)。

cpp 复制代码
struct sw_flow {
    struct rcu_head rcu; // rcu保护机制
    struct {
        // node[0] 是哈希链表节点,表示流表项在 ti 哈希表中的位置
        // node[1] 是哈希链表节点,表示流表项在 ufid_ti 哈希表中的位置
        struct hlist_node node[2]; 
        
        u32 hash;                 // hash值
    } flow_table, ufid_table;
    int stats_last_writer;      /* CPU id of the last writer on
                     * 'stats[0]'.
                     */
    struct sw_flow_key key;    // 流表中的key值
    struct sw_flow_id id;    // 存储该流表项的唯一标识符 UFID
    struct cpumask *cpu_used_mask;
    struct sw_flow_mask *mask;    // 要匹配的mask结构体
    struct sw_flow_actions __rcu *sf_acts;    // 相应的action动作
    struct sw_flow_stats __rcu *stats[]; /* One for each CPU.  First one
                       * is allocated at flow creation time,
                       * the rest are allocated on demand
                       * while holding the 'stats[0].lock'.
                       */
};

struct sw_flow_key_range {
    unsigned short int start;    // key值匹配数据开始部分
    unsigned short int end;    // key值匹配数据结束部分
};

// 流掩码
struct sw_flow_mask {
    int ref_count;
    struct rcu_head rcu;
    struct sw_flow_key_range range; // 对key赋予范围,构成掩码
    struct sw_flow_key key; // 要和数据包操作的key,将要被用来匹配的key值
};

#define MAX_UFID_LENGTH 16 /* 128 bits */

// UFID, hash计算出的字符串
struct sw_flow_id {
    u32 ufid_len;
    union {
        u32 ufid[MAX_UFID_LENGTH / 4];
        struct sw_flow_key *unmasked_key;
    };
};

添加 网桥

  • 键入命令ovs-vsctl add-br testBR

  • 内核中的 openvswitch.ko 收到一个添加网桥的命令时候------即收到 OVS_DATAPATH_FAMILY通道的 OVS_DP_CMD_NEW命令。该命令绑定的回调函数为 ovs_dp_cmd_new

OVS_DP_CMD_NEW 注册 回调函数 ovs_dp_cmd_new

cpp 复制代码
static const struct genl_small_ops dp_datapath_genl_ops[] = {
    { .cmd = OVS_DP_CMD_NEW,
      .validate = GENL_DONT_VALIDATE_STRICT | GENL_DONT_VALIDATE_DUMP,
      .flags = GENL_UNS_ADMIN_PERM, /* Requires CAP_NET_ADMIN privilege. */
      .doit = ovs_dp_cmd_new
    }, 
  • ovs_dp_cmd_new 函数除了初始化 dp 结构
cpp 复制代码
static int ovs_dp_cmd_new(struct sk_buff *skb, struct genl_info *info)
{
    //...
    // 申请dp 即datapath 结构 
    dp = kzalloc(sizeof(*dp), GFP_KERNEL);
    if (dp == NULL)
        goto err_destroy_reply;

    ovs_dp_set_net(dp, sock_net(skb->sk));

    /* Allocate table. */
    // 初始化流表
    err = ovs_flow_tbl_init(&dp->table);
    if (err)
        goto err_destroy_dp;
    // 初始化状态
    err = ovs_dp_stats_init(dp);
    if (err)
        goto err_destroy_table;
    // 初始化datapath的 vport 表
    err = ovs_dp_vport_init(dp);
    if (err)
        goto err_destroy_stats;

    err = ovs_meters_init(dp);
    if (err)
        goto err_destroy_ports;
    // 初始化端口,对网桥br-int ,将初始化化内部端口 br-int internal
    /* Set up our datapath device. */
    parms.name = nla_data(a[OVS_DP_ATTR_NAME]);
    parms.type = OVS_VPORT_TYPE_INTERNAL;
    parms.options = NULL;
    parms.dp = dp;
    parms.port_no = OVSP_LOCAL;
    parms.upcall_portids = a[OVS_DP_ATTR_UPCALL_PID];
    parms.desired_ifindex = a[OVS_DP_ATTR_IFINDEX]
        ? nla_get_s32(a[OVS_DP_ATTR_IFINDEX]) : 0;

    /* So far only local changes have been made, now need the lock. */
    ovs_lock();

    err = ovs_dp_change(dp, a);
    if (err)
        goto err_unlock_and_destroy_meters;
    // 网桥自身vport的生成
    vport = new_vport(&parms);
    if (IS_ERR(vport)) {
        //...
    }
  • 调用 new_vport 函数来生成新的 vport

可以看到vport创建乎是连接入 hash表中的链表进行管理的

cpp 复制代码
static struct vport *new_vport(const struct vport_parms *parms)
{
    struct vport *vport;
    //创建vport
    vport = ovs_vport_add(parms);
    // 放入hash表管理
    if (!IS_ERR(vport)) {
        struct datapath *dp = parms->dp;
        struct hlist_head *head = vport_hash_bucket(dp, vport->port_no);

        hlist_add_head_rcu(&vport->dp_hash_node, head);
    }
    return vport;
}
  • new_vport 函数调用 ovs_vport_add()来尝试生成一个新的 vport
cpp 复制代码
struct vport *ovs_vport_add(const struct vport_parms *parms)
{
   //... 
    ops = ovs_vport_lookup(parms);
    if (ops) {
        struct hlist_head *bucket;

        if (!try_module_get(ops->owner))
            return ERR_PTR(-EAFNOSUPPORT);
        // 创建vport, 这里将调用注册的函数方法
        vport = ops->create(parms);
        if (IS_ERR(vport)) {
            module_put(ops->owner);
            return vport;
        }
        //...
    }
    // ...
}
  • ovs_vport_add()函数会检查 vport 类型(通过 vport_ops_list[]数组),并调用相关的 create()函数来生成 vport 结构

在 net/openvswitch/ 目录下可找到多个 net/openvswitch/vport-xxx.c 的文件,对应各个类型端口的注册回调

net/openvswitch/vport-geneve.c

cpp 复制代码
static struct vport_ops ovs_geneve_vport_ops = { 
        .type           = OVS_VPORT_TYPE_GENEVE,
        .create         = geneve_create,                                                                                                                                                                    
        .destroy        = ovs_netdev_tunnel_destroy,
        .get_options    = geneve_get_options,
        .send           = dev_queue_xmit,
};  

net/openvswitch/vport-gre.c

cpp 复制代码
static struct vport_ops ovs_gre_vport_ops = { 
        .type           = OVS_VPORT_TYPE_GRE,
        .create         = gre_create,                                                                                                                                                                       
        .send           = dev_queue_xmit,
        .destroy        = ovs_netdev_tunnel_destroy,
};  

net/openvswitch/vport-internal_dev.c

此处可以看到internal_vport 的特殊性, 其包send函数是 internal 的接收函数 internal_dev_recv,即通过internal 端口发送的包都将进入internal 的接收函数,从而进入ovs继续处理分发。

cpp 复制代码
static struct vport_ops ovs_internal_vport_ops = { 
        .type           = OVS_VPORT_TYPE_INTERNAL,
        .create         = internal_dev_create,
        .destroy        = internal_dev_destroy,
        .send           = internal_dev_recv,                                                                                                                                                                
}; 

net/openvswitch/vport-netdev.c

cpp 复制代码
static struct vport_ops ovs_netdev_vport_ops = { 
        .type           = OVS_VPORT_TYPE_NETDEV,
        .create         = netdev_create,                                                                                                                                                                    
        .destroy        = netdev_destroy,
        .send           = dev_queue_xmit,
};     

net/openvswitch/vport-vxlan.c

cpp 复制代码
static struct vport_ops ovs_vxlan_netdev_vport_ops = {
        .type                   = OVS_VPORT_TYPE_VXLAN,
        .create                 = vxlan_create,                                                                                                                                                             
        .destroy                = ovs_netdev_tunnel_destroy,
        .get_options            = vxlan_get_options,
        .send                   = dev_queue_xmit,
};
  • 当dp是网络设备时(vport_netdev.c),最终由 ovs_vport_add()函数调用的是 netdev_create()【在 vport_ops_listovs_netdev_ops 中】

  • netdev_create()函数最关键的一步是注册了收到网包时的回调函数

cpp 复制代码
static struct vport *netdev_create(const struct vport_parms *parms)
{
    struct vport *vport;

    vport = ovs_vport_alloc(0, &ovs_netdev_vport_ops, parms);
    if (IS_ERR(vport))
        return vport;
    // 在该函数中注册收包函数
    return ovs_netdev_link(vport, parms->name);
}

struct vport *ovs_netdev_link(struct vport *vport, const char *name)
{
    //...
    // 注册 netdev_frame_hook, 即收报处理函数
    err = netdev_rx_handler_register(vport->dev, netdev_frame_hook,
                     vport);
   //...
}
        
  • register_netdevice(vport->dev)操作是将 netdev_vport->dev 收到网包时的相关数据由 netdev_frame_hook()函数来处理,都是些辅助处理,依次调用各处理函数,在 netdev_port_receive()【这里会进行数据包的拷贝,避免损坏】进入 ovs_vport_receive()回到 vport.c,从 ovs_dp_process_packet()回到 datapath.c,进行统一处理
cpp 复制代码
/* Called with rcu_read_lock and bottom-halves disabled. */
static rx_handler_result_t netdev_frame_hook(struct sk_buff **pskb)
{
   //...
    netdev_port_receive(skb);
    return RX_HANDLER_CONSUMED;
}

/* Must be called with rcu_read_lock. */
static void netdev_port_receive(struct sk_buff *skb)
{
    // ...
    ovs_vport_receive(vport, skb, skb_tunnel_info(skb));
    return;
error:
    kfree_skb(skb);
}

int ovs_vport_receive(struct vport *vport, struct sk_buff *skb,
              const struct ip_tunnel_info *tun_info)
{    
    // 提取数据包特征用于流表匹配
    //...

    /* Extract flow from 'skb' into 'key'. */
    error = ovs_flow_key_extract(tun_info, skb, &key);
    if (unlikely(error)) {
        kfree_skb(skb);
        return error;
    }
    // 处理数据包
    ovs_dp_process_packet(skb, &key);
    return 0;
}
  • 收包流程:netdev_frame_hook()->netdev_port_receive->ovs_vport_receive->ovs_dp_process_received_packet()

数据包处理

  • ovs_vport_receive()调用ovs_flow_extract基于skb生成key值,并检查是否有错,然后调用ovs_dp_process_packet。交付给datapath处理
cpp 复制代码
int ovs_flow_key_extract(const struct ip_tunnel_info *tun_info,
             struct sk_buff *skb, struct sw_flow_key *key)
{
#if IS_ENABLED(CONFIG_NET_TC_SKB_EXT)
    struct tc_skb_ext *tc_ext;
#endif
    bool post_ct = false, post_ct_snat = false, post_ct_dnat = false;
    int res, err;
    u16 zone = 0;
    // 是否是隧道封装发送过来的包进行区别处理
    /* Extract metadata from packet. */
    if (tun_info) {
        // 获取隧道协议
        key->tun_proto = ip_tunnel_info_af(tun_info);
        //提取隧道信息
        memcpy(&key->tun_key, &tun_info->key, sizeof(key->tun_key));

        if (tun_info->options_len) {
            BUILD_BUG_ON((1 << (sizeof(tun_info->options_len) *
                           8)) - 1
                    > sizeof(key->tun_opts));

            ip_tunnel_info_opts_get(TUN_METADATA_OPTS(key, tun_info->options_len),
                        tun_info);
            key->tun_opts_len = tun_info->options_len;
        } else {
            key->tun_opts_len = 0;
        }
    } else  {
        // 关键字中隧道信息放置为空信息
        key->tun_proto = 0;
        key->tun_opts_len = 0;
        memset(&key->tun_key, 0, sizeof(key->tun_key));
    }

   // ...
    // 进一步提取更通用的信息,如以太网头信息,Ip头信息
    err = key_extract(skb, key);
    if (!err) {
        ovs_ct_fill_key(skb, key, post_ct);   /* Must be after key_extract(). */
        if (post_ct) {
            if (!skb_get_nfct(skb)) {
                key->ct_zone = zone;
            } else {
                if (!post_ct_dnat)
                    key->ct_state &= ~OVS_CS_F_DST_NAT;
                if (!post_ct_snat)
                    key->ct_state &= ~OVS_CS_F_SRC_NAT;
            }
        }
    }
    return err;
}
  • ovs_flow_tbl_lookup_stats。基于前面生成的key值进行流表查找,返回匹配的流表项,结构为sw_flow

  • 若不存在匹配,则调用ovs_dp_upcall上传至userspace进行匹配。 (包括包和key都要上传)

  • 若存在匹配,则直接调用ovs_execute_actions执行对应的action,比如添加vlan头,转发到某个port等。

cpp 复制代码
/* Must be called with rcu_read_lock. */
void ovs_dp_process_packet(struct sk_buff *skb, struct sw_flow_key *key)
{
    //...
    // 流表匹配
    flow = ovs_flow_tbl_lookup_stats(&dp->table, key, skb_get_hash(skb),
                     &n_mask_hit, &n_cache_hit);
    if (unlikely(!flow)) {
        // 未匹配上流表项,调用upcall 
        struct dp_upcall_info upcall;

        memset(&upcall, 0, sizeof(upcall));
        upcall.cmd = OVS_PACKET_CMD_MISS;
        
        if (dp->user_features & OVS_DP_F_DISPATCH_UPCALL_PER_CPU)
            upcall.portid =
                ovs_dp_get_upcall_portid(dp, smp_processor_id());
        else
            upcall.portid = ovs_vport_find_upcall_portid(p, skb);

        upcall.mru = OVS_CB(skb)->mru;
        error = ovs_dp_upcall(dp, skb, key, &upcall, 0);
        //...
    }
    // 流找到匹配项,按照关联的action处理
    error = ovs_execute_actions(dp, skb, sf_acts, key);
    //...
}
  • 流表查询调用路径为

ovs_flow_tbl_lookup_stats-->尝试查询缓存-->flow_lookup-->masked_flow_lookup

cpp 复制代码
/* Flow lookup does full lookup on flow table. It starts with
 * mask from index passed in *index.
 * This function MUST be called with BH disabled due to the use
 * of CPU specific variables.
 */
static struct sw_flow *flow_lookup(struct flow_table *tbl,
                   struct table_instance *ti,
                   struct mask_array *ma,
                   const struct sw_flow_key *key,
                   u32 *n_mask_hit,
                   u32 *n_cache_hit,
                   u32 *index)
{
    struct mask_array_stats *stats = this_cpu_ptr(ma->masks_usage_stats);
    struct sw_flow *flow;
    struct sw_flow_mask *mask;
    int i;

    if (likely(*index < ma->max)) {
        mask = rcu_dereference_ovsl(ma->masks[*index]);
        if (mask) {
            flow = masked_flow_lookup(ti, key, mask, n_mask_hit);
            if (flow) {
                u64_stats_update_begin(&stats->syncp);
                stats->usage_cntrs[*index]++;
                u64_stats_update_end(&stats->syncp);
                (*n_cache_hit)++;
                return flow;
            }
        }
    }

    for (i = 0; i < ma->max; i++)  {

        if (i == *index)
            continue;

        mask = rcu_dereference_ovsl(ma->masks[i]);
        if (unlikely(!mask))
            break;
        // 遍历所有mask,在下面函数中使用遍历到的mask与key做掩码操作后
        // 进行流表查询
        flow = masked_flow_lookup(ti, key, mask, n_mask_hit);
        if (flow) { /* Found */
            *index = i;
            u64_stats_update_begin(&stats->syncp);
            stats->usage_cntrs[*index]++;
            u64_stats_update_end(&stats->syncp);
            return flow;
        }
    }

    return NULL;
}
cpp 复制代码
static struct sw_flow *masked_flow_lookup(struct table_instance *ti,
                      const struct sw_flow_key *unmasked,
                      const struct sw_flow_mask *mask,
                      u32 *n_mask_hit)
{
    struct sw_flow *flow;
    struct hlist_head *head;
    u32 hash;
    struct sw_flow_key masked_key;
    
    // 对flow_key进行掩码操作
    ovs_flow_mask_key(&masked_key, unmasked, false, mask);
    // 对掩盖结果进行hash计算
    hash = flow_hash(&masked_key, &mask->range);
    // 根据hash索引到对应的流表桶
    head = find_bucket(ti, hash);
    (*n_mask_hit)++;
    // 遍历查找到的流表桶进行逐一精确匹配
    hlist_for_each_entry_rcu(flow, head, flow_table.node[ti->node_ver],
                 lockdep_ovsl_is_held()) {
        if (flow->mask == mask && flow->flow_table.hash == hash &&
            flow_cmp_masked_key(flow, &masked_key, &mask->range))
            // 匹配到时返回flow
            return flow;
    }
    return NULL;
}
  • 动作执行

在流表中匹配到流表项后,将使用找到的sw_flow 索引该流绑定的处理方法进行处理

调用路径为

ovs_execute_actions-->do_execute_actions

cpp 复制代码
/* 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;
    // attr ,len 参数为该流关联的action数组,遍历执行action
    for (a = attr, rem = len; rem > 0;
         a = nla_next(a, &rem)) {
        int err = 0;

        if (trace_ovs_do_execute_action_enabled())
            trace_ovs_do_execute_action(dp, skb, key, a, rem);

        /* Actions that rightfully have to consume the skb should do it
         * and return directly.
         */
         // 根据attr 的类型区别处理
        switch (nla_type(a)) {
        case OVS_ACTION_ATTR_OUTPUT: {
          //...
        }

        case OVS_ACTION_ATTR_TRUNC: {
           //...
        }

        case OVS_ACTION_ATTR_USERSPACE:
           //...
        case OVS_ACTION_ATTR_HASH:
            execute_hash(skb, key, a);
            break;

        case OVS_ACTION_ATTR_PUSH_MPLS: {
            struct ovs_action_push_mpls *mpls = nla_data(a);

            err = push_mpls(skb, key, mpls->mpls_lse,
                    mpls->mpls_ethertype, skb->mac_len);
            break;
        }
           //...
        }
        }

        if (unlikely(err)) {
            ovs_kfree_skb_reason(skb, OVS_DROP_ACTION_ERROR);
            return err;
        }
    }

    ovs_kfree_skb_reason(skb, OVS_DROP_LAST_ACTION);
    return 0;
}

各个action 说明如下

在 Open vSwitch (OVS) 中,流表的 action 定义了当某个数据包与流表项匹配时,应该执行的动作。OVS 提供了多种不同的 action,用于控制数据包的处理方式。以下是 OVS 流表中常见的 action 类型及其详细说明:

  1. Output Action

output 操作是 OVS 中最常见的动作,表示将匹配的数据包发送到指定的端口。

语法output:port

功能:将数据包发送到指定的端口,可以是物理端口、虚拟端口或特殊端口。

示例:output:2 表示将数据包发送到端口 2。

  1. Drop Action

drop 操作用于丢弃匹配到的流表项的数据包。

功能:数据包被丢弃,不会被转发或处理。这是网络安全或流量控制中的常见动作。

示例:如果某条流表项没有指定 output 操作,就相当于隐式地执行了 drop 操作。

  1. Resubmit Action

resubmit 操作将数据包提交给指定的流表,进行重新匹配。

语法resubmit:[table],port

功能:允许数据包重新提交到指定的流表(或同一流表)的特定表项。可以实现多阶段流表处理。

示例:resubmit:1 表示将数据包提交给流表表 1 进行进一步匹配。

  1. Set Field Action

set_field 操作用于修改数据包的字段,例如修改数据包的 IP 地址、MAC 地址或 VLAN 标签。

语法set_field:value->field

功能:修改指定的包头字段。

示例:set_field:10.0.0.1->ip_dst 表示将数据包的目标 IP 地址修改为 10.0.0.1

  1. Push /Pop VLAN Action

这些操作用于插入或移除 VLAN 标签,通常用于处理 VLAN 隔离的网络。

push_vlan:为数据包添加一个 VLAN 标签。

语法push_vlan:ethertype

示例:push_vlan:0x8100 表示给数据包添加一个 VLAN 标签。

pop_vlan:从数据包中移除 VLAN 标签。

语法pop_vlan

示例:pop_vlan 表示从数据包中移除最外层的 VLAN 标签。

  1. Push /Pop MPLS Action

这些操作用于插入或移除 MPLS(多协议标签交换)标签。

push_mpls:为数据包添加一个 MPLS 标签。

语法push_mpls:ethertype

示例:push_mpls:0x8847 表示给数据包添加 MPLS 标签。

pop_mpls:从数据包中移除 MPLS 标签。

语法pop_mpls:ethertype

示例:pop_mpls:0x0800 表示移除 MPLS 标签,并将数据包的以太网类型设置为 0x0800(IP 数据包)。

  1. Group Action

group 操作用于执行复杂的组操作,通常用于负载均衡和多路径转发。

语法group:group_id

功能 :将数据包发送到指定的组(group),每个组可以包含多个输出端口,并支持多种转发模式,如 all(将数据包发送到组中的所有端口)或 select(随机选择一个端口)。

示例:group:1 表示将数据包提交到组 1 进行处理。

  1. Set Queue Action

set_queue 操作用于将数据包发送到特定的队列,通常用于实施 QoS(服务质量)策略。

语法set_queue:queue_id

功能:将数据包标记为特定队列中的成员,不同队列可以有不同的优先级或带宽限制。

示例:set_queue:3 表示将数据包分配到队列 3。

  1. Modifying MAC / IP Address

除了 set_field 操作,OVS 还提供一些特定操作用于修改 MAC 和 IP 地址。

mod_dl_src:修改数据包的源 MAC 地址。

语法mod_dl_src:mac_address

示例:mod_dl_src:00:11:22:33:44:55 表示将源 MAC 地址修改为 00:11:22:33:44:55

mod_dl_dst:修改数据包的目标 MAC 地址。

语法mod_dl_dst:mac_address

示例:mod_dl_dst:66:77:88:99:AA:BB 表示将目标 MAC 地址修改为 66:77:88:99:AA:BB

mod_nw_src:修改数据包的源 IP 地址。

语法mod_nw_src:ip_address

示例:mod_nw_src:192.168.1.1 表示将源 IP 地址修改为 192.168.1.1

mod_nw_dst:修改数据包的目标 IP 地址。

语法mod_nw_dst:ip_address

示例:mod_nw_dst:192.168.1.2 表示将目标 IP 地址修改为 192.168.1.2

  1. Controller Action

controller 操作用于将数据包发送到控制器处理,常用于 OpenFlow 中。

语法controller

功能:将数据包发送到控制器(如 OpenFlow 控制器),由控制器决定如何处理数据包。

示例:controller 表示将数据包发送给控制器处理。

  1. Normal Action

normal 操作用于将数据包交给 OVS 内置的常规交换机行为处理,即模拟传统的 L2 交换机转发数据包。

语法normal

功能:按照传统交换机的学习规则,查找 MAC 表并转发数据包。适合用在简化的网络配置中,作为一种默认的 L2 处理方式。

示例:normal 表示数据包按照常规 L2 交换机转发规则处理。

  1. Flood Action

flood 操作用于将数据包泛洪到除输入端口外的所有端口。

语法flood

功能:将数据包泛洪到网桥中除了输入端口之外的所有端口。常用于未知 MAC 地址的场景,类似传统 L2 交换机的泛洪行为。

示例:flood 表示泛洪数据包。

  1. Mirror Action

mirror 操作将数据包复制并发送到一个或多个指定的端口,通常用于流量监控或分析。

功能:将数据包副本发送到指定的监控端口,可以同时发送多个副本到不同的端口。

  1. Continue Action

continue 操作用于继续执行后续的流表操作,而不会停止当前的流表处理。

功能:允许多个流表操作连续进行,而不在遇到匹配时停止处理。

upcall 消息处理

  1. ovs_dp_upcall()首先调用 err=queue_userspace_packet()将信息排队发到用户空间去

  2. dp_ifindex=get_dpifindex(dp)获取网卡设备索引号

  3. 调整 VLANMAC 地址头指针

  4. 网络链路属性,如果不需要填充则调用此函数

  5. len=upcall_msg_size(),获得 upcall 发送消息的大小

  6. user_skb=genlmsg_new_unicast,创建一个新的 netlink 消息

  7. upcall=genlmsg_put()增加一个新的 netlink 消息到 skb

  8. err=genlmsg_unicast(),发送消息到用户空间去处理

数据包未匹配到flow时将调用ovs_dp_upcall将数据发送到用户空间

cpp 复制代码
int ovs_dp_upcall(struct datapath *dp, struct sk_buff *skb,
          const struct sw_flow_key *key,
          const struct dp_upcall_info *upcall_info,
          uint32_t cutlen)
{
    struct dp_stats_percpu *stats;
    int err;

    if (trace_ovs_dp_upcall_enabled())
        trace_ovs_dp_upcall(dp, skb, key, upcall_info);
    // 检测端口id,排除portid为0的包,因为该函数为数据包上报用户空间
    if (upcall_info->portid == 0) {
        err = -ENOTCONN;
        goto err;
    }

    if (!skb_is_gso(skb))
        // 入队上报用户空间
        err = queue_userspace_packet(dp, skb, key, upcall_info, cutlen);
    else
        err = queue_gso_packets(dp, skb, key, upcall_info, cutlen);

    ovs_vport_update_upcall_stats(skb, upcall_info, !err);
    if (err)
        goto err;

    return 0;

err:
    stats = this_cpu_ptr(dp->stats_percpu);

    u64_stats_update_begin(&stats->syncp);
    stats->n_lost++;
    u64_stats_update_end(&stats->syncp);

    return err;
}

参考文档

https://www.kancloud.cn/digest/openvswitch/117251

https://feisky.gitbooks.io/sdn/content/ovs/internal.html

https://github.com/dcui/sdn-handbook-1/blob/master/ovs/ovn.md

https://www.jianshu.com/p/bf112793d658

https://chentingz.github.io/2019/12/30/「OpenFlow」协议入门/

https://blog.csdn.net/u014044624/article/details/103911224

https://www.cnblogs.com/oxspirt/p/16435926.html

https://blog.51cto.com/u_15127581/4599785

相关推荐
余~~185381628001 天前
矩阵碰一碰发视频的源码技术,支持OME
microsoft
※※冰馨※※2 天前
【C#】无法安装程序包“DotSpatial.Symbology 4.0.656”
windows·microsoft·c#
电手2 天前
微软宣布 Windows 11 将不再免费升级:升级需趁早
windows·microsoft
猿大师办公助手2 天前
Weboffice在线Word权限控制:限制编辑,只读、修订、禁止复制等
vue.js·chrome·microsoft·word
labview_自动化3 天前
C#功能测试
windows·microsoft·c#
三月七(爱看动漫的程序员)3 天前
工具包组件和内置Agent组件
开发语言·前端·javascript·microsoft·ecmascript·facebook·新浪微博
觅远3 天前
python+Ollama库实现简单的AI模型问答交互
python·microsoft·交互
鲤籽鲲3 天前
C# ConcurrentBag 使用详解
windows·microsoft·c#·多线程
努力的小好4 天前
【Python 打造高效文件分类工具】
开发语言·python·microsoft
来自于狂人4 天前
解决 `pip install open-webui` 时的编译错误:Microsoft Visual C++ 14.0 或更高版本缺失
c++·microsoft·pip