virtio数据包过滤实现

发布时间 2023-06-03 10:11:57作者: 杏子的成长日记

背景和意义

在dpdk实现过滤包的意义主要有两个方面:

第一是出于性能考虑,如果我们对所接受的数据包的类型有所要求,在dpdk这边直接进行过滤的话,可以大大加快处理的效率,节省资源,提升系统性能与吞吐量。否则,包的接收需要通过dpdk vhost user,到virtio net,最后到达协议栈,经过这一系列的传送过程之后,最后才发现这个包不是我们所想要接收的,这样就造成了资源的消耗与时间的浪费。

第二是出于安全性,可以设置过滤的郭泽,以屏蔽掉恶意有害的数据包,从而对网络流量进行控制防护,保护系统安全。

实现过程

精简:数据包的过滤主要涉及到的是dpdk和virtio控制层面的交互。前端,也就是virtio需要把相应的控制命令(包括,你想要进行数据包过滤这件事,以及你想要留下什么类型的数据包)发送给后端。需要qemu充当中间人。virtio和qemu之间是通过cvq进行传递的,virtio将所需要发送的命令通过scatter list,存放到cvq中,然后用kick函数通知qemu。qemu收到消息以后调用相应的处理函数,保存信息,并将信息发送给dpdk,需要把命令封装成message的形式,主要包括type和poayload。然后发送给dpdk。然后dpdk这边解析收到的message,从而知道后端所想要留下的包的类型,在收包的时候进行判断,如果不是他想要的包,就直接continue,放弃接收。

详细

virtio-net driver作为前端,首先将命令发送给qemu,这里是用cvq command传递的,首先需要添加相应的结构体与命令,以在特征协商的时候让后端vhost知道前后端都支持过滤包的功能。在virtnet_probe,探测函数中,添加发送控制命令的函数,这样,在驱动初始化的时候,如果双方都支持过滤包的这个特性,那么就可以发送命令。命令的发送是基于scatter list和cvq的,其中scatter list包含三个主体:CTRL class主命令,cmd 子命令组成的头部,command的主体out,和一个状态status,这个状态是后端在成功执行命令的时候,会改变这个状态变量,从而让前端知道这个命令成功被接收到了。命令被保存在cvq之后,前端通过kick函数通知qemu,告诉qemu有一个命令发送过来了。

qemu作为中间人,通过virtio_net_handle_ctrl获取命令中的payload,用sg读取命令,然后根据hdr保存的命令的种类,调用不同的命令处理的函数。这里就是头部信息的处理函数,首先将命令的内容保存到相应结构体上面,然后调用set_header的函数,也就是上面的过程qemu和前端virtnet driver的通信已经结束了,接下来就是把信息传送给dpdk。这个set_hdr的函数其实是一个回调函数,会根据你后端到底是vhost-net还是vhost-user,调用不同的处理函数,我们这里的架构vhost是在用户态实现的,就是vhost-user,所以是调用vhost-user set_header的函数。因为qemu和vhost通信是用vhost message进行通信的,所以这里要把命令封装成message。首先qemu和dpdk两边都要定义相同的消息类型msg type。消息的封装主要也就是包括msg hdr和payload 也就是可以接收的包的种类信息。

最后,dpdk根据消息类型,调用响应的处理函数,将msg的payload里的内容进行保存。然后运用到收包函数当中,首先需要对收到的数据包mbuf的类型进行层层解析,这里类似于skb的解析,用不同的指针去指示数据包不同的字段,l2指向mac头部,l3指向TCP头部,l4指向IP头部。最后,根据l4里面proto的内容,可以判断该数据包所用的协议,然后,如果不属于我们想接收的包的类型,就跳过,不进行接收

● virtio-net driver 基于 scatterlist 和 cvq,向 qemu 发送配置命令
● qemu 通过 virtio_net_handle_ctrl 获取命令中的 payload,同时基于此 payload 为 vhost-user(dpdk)发送 vhost protocol msg
● dpdk 通过 vhost_user_msg_handler 来处理 qemu 发送过来的 msg,并使用该 msg 所带的 payload info。

driver

总结

前端:首先添加config结构体以及CTRL/SET 命令,然后在control_buf中添加前面的结构体,在virtnet_info中添加标志位,并在virtnet_probe中,根据协商的协议(virtio_has_feature)更新标志位。协商如果成功,发送命令。命令是由一个scattrer list数组sgs存储的,包括头部(主命令、子命令)、具体信息以及返回状态。然后将其添加到cvq中,然后通知qemu

  • 1.在include/uapi/linux/virtio_net.h 中,首先添加 config 结构体以及相关的 CTRL 和 SET 命令:
#define VIRTIO_ENT_F_HEADER 25

struct virtio_net_header_config{
#define VIRTIO_NET_HEADER_TYPE_TCP4(1<<0)
#define VIRTIO_NET_HEADER_TYPE_TCP6(1<<1)
#define VIRTIO_NET_HEADER_TYPE_UDP4(1<<2)
#define VIRTIO_NET_HEADER_TYPE_UDP6(1<<3)
    __le16 type;
}

#define VIRTIO_NET_CTRL_HEADER 6//主命令
#define VIRTIO_NET_CTRL_HEADER_SET 0//子命令
  • 2.在drivers/net/virtio_net.c中,在control_buf中添加控制结构体
struct control_buf{
	...
	struct virtio_net_header_config hdr;
}

在virnet_info中添加标志位

bool header;
  • 3.在 drivers/net/virtio_net.c 中,在 virtnet_probe 中根据协商的协议来更新 virtnet_info 中的相关 field:
if(virtio_has_feature(vdev,VIRTIO_NET_F_HEADER))
	vi->header=true;
else
	vi->header=false;
  • 4.根据是否协商成功之后,且保证 cvq 可用之后,便可以基于 cvq 进行 config command send:
if(vi->header){
    type|=(VIRTIO_NET_HEADER_TYPE_TCP4|VIRTIO_NET_HEADER_TCP6|VIRTIO_NET_HEADER_TYPE_UDP4|VIRTIO_NET_HEADER_UDP6);
    if(virtnet_set_header(dev,type)<0) vi->header=false;
}

发送的核心函数:

主要基于 scatterlist 和 cvq,其中,sg 包含三个主体:由 CTRL class 和 command cmd 组成的 hdr,command 的主体 out,状态 status:

static int virtnet_set_header(struct net_device *dev,u16 type){
    struct virtnet_info *vi=netdev_priv(dev);
    struct virtio_device *vdev=vi->dev;
    struct scatter list sg;
    if(!vi->has_cvq) return -EINVAL;
    vi->ctrl->hdr.type=cpu_to_virtio16(vi->vdev,type);
    sg_init_one(&sg,&vi->ctrl->hdr,sizeof(vi->ctrl->hdr));
    if(!virtnet_send_command(vi,VIRTIO_NET_CTRL_HEADER,VIRTIO_NET_CTRL_HEADER_SET,&sg)){
        dev(&vdev->dev,"Failed to set header type")
    }
}

qemu

总结:qemu相当于一个中间人,对接kernel driver和dpdk

接收kernel driver基于cvq发送过来的command,command的内容接收和处理以后,又会对接dpdpk,主要基于vhost protocol message

  • 首先需要在qemu控制virtio_net的头文件中添加相同的对于特性、config结构体和CTRL/SET命令的定义。然后在在virtio_net_handler_ctrl中,会从共享的cvq中pop出前面driver填充的内容,其中包括hdr,out和status。通过sg来读取主命令和子命令,然后判断主命令的类型根据不同的主命令,调用不同的处理函数,这里就是virtio_net_handle_hdr。处理函数会首先保存到config结构体当中。

  • 下一步就是把命令封装成一个message,与dpdk通信。这里就是交给vhost_set_header。这是一个回调函数,调用vhost_user_set_header,来进行message结构体的封装。主要包括msg type 和 msg payload(这里就是hdr type)

![img](file:///E:\QQ\QQ_file\951658480\Image\C2C\Image1\746B845DFB57483DDEE2E515A0E0F1DF.jpg)

  • 1.首先同样在virtio_net头文件中添加相关特性、config结构体和CTRL/SET命令(同上一步的1)

  • 2.设备在初始化时,为VirtIONet分配cvq的时候,会指定回调函数为virtio_net_handler_ctrl,在virtio_net_handler_ctrl中,会从共享的cvq中pop出driver填充的内容,其中包含了hdr、out和status。通过sg来得到hdr中的主命令class和子命令cmd

  • 3.根据不同的命令class,调用不同的处理函数,这里即是调用virtio_net_handle_hdr

  • 4.在virtio_net_handle_hdr中,首先把sg更新到virtio_net_ctrl_hdr结构体当中(header type),然后将要发送的命令交给vhost_set_header,来完成将命令封装成message,并发送给dpdk

  • 5.vhost_set_header其实是一个ops回调,会根据vhost-net或者vhost-user来决定具体调用哪个函数,这里是vhost_user_set_header

    如下图:将回调函数赋值为vhost_user_set_header

    image-20230523204818700

  • 6.将命令封装成message,并发送给dpdk。在结构体 VhostUserRequest 中,添加msg type:

    image.png

static int vhost_user_set_header(struct vhost_dev *dev,uint16_t hdr_type){
    VhostUserMsg msg={
        .hdr.request=VHOST_USER_SET_HEADER,//请求类型:SET_HEADER
        //.hdr.flags=VHOST_USER_VISION,
        .hdr.size=sizeof(hdr_type);
    }
    bool reply_supported=virtio_has_feature(dev->protocal_features,VHOST_USER_PROTOCLO_F_REPLY_ACK);
    msg.playload.u64=hdr_type;//payload:header type
    bool reply_supported=virtio_has_feature(dev->protocal_features,VHOST_USER_PROTOCLO_F_REPLY_ACK);
    msg.hdr.flags=VHOST_USER_VISION,
    if(reply_supported){
        msg.hdr.flags|=VHOST_USER_NEED_REPLY_MASK;
    }
    //发送给dpdk
    if(vhost_user_write(dev,&msg,NULL,0)<0){
        return -1;
    }
    
    if(reply_supported){
        return process_message_reply(dev,&msg);
    }
    return 0;
}

dpdk

  • 首在 drivers/net/virtio/virtio_user/vhost.h 中添加和 qemu 一致的消息类型。在 lib/librte_vhost/vhost.h 中为 virtio_net 结构体添加 hdr_type,这个是和 qemu 中的 VirtIONet 对应的。

  • DPDK接收消息是通过vhost_user_msg_handler中接收消息,主要是调用 vhost_msg_handlers

    将payload中的内容赋值给header type

    vhost_user_header(struct virtio_net **pdev,struct VhostUserMsg *msg,
                     int main_fd __rte_unused){
        struct virtio_net *dev=*pdev;
        if(validate_msg_fds(msg,0)!=0){
            return RTE_VHOST_MSG_RESULT_ERR;
        }
        dev->hdr_type=msg->payload.u64;
        return RTE_VHOST_MSG_RESULT_OK;
    }
    
  • dpdk中的virtio_dev_rx_split函数中,添加头部检测的函数

    image.png

  • 先实现检测包类型的函数

    static void
    parse_ethernet(struct rte_mbuf *m,uint16_t *l4_proto,void **l4_hdr)
    {
        struct rte_ipv4_hdr *ipv4_hdr;
        struct rte_ipv6_hdr *ipv6_hdr;
        void *l3_hdr=NULL;
        struct rte_ther_hdr *eth_hdr;
        struct rte_tcp_hdr *tcp_hdr;
        uint16_t ethertype;
        
        eth_hdr=rte_pktmbuf_mthod(m,struct rte_ether_hdr *);//拆包,获得payload,获得指向数据的指针
        
        m->l2_len=sizeof(struct rte_ether_hdr);
        ethertype=rte_be_to_cpu_16(eth_hdr->ether_type);
        
        if(etehrtype==RTE_ETHER_TYPE_VLAN){
            struct rte_vlan_hdr *vlan_hdr=(struct rte_vlan_hdr *)(eth_hdr+1);
            
            m->l2_len+=sizeof(struct rte_vlan_hdr);
            ethertype=rte_be_to_cpu_16(vlan_hdr->eth_proto);
        }
        
        l3_hdr=(char*)eth_hdr+m->l2_len;
        
        switch(ethertype){
            case RTE_ETHER_TYPE_IPV4:
                ipv4_hdr=l3_hdr;
                *l4_proto=ipv4_hdr->next_proto_id;
                m->l3_len=(ipv4_hdr->version_ihl&oxof)*4;
                *l4_hdr=(char*)l3_hdr+m->l3_len;
                m->ol_flags|=PKT_TX_IPV4;
                break;
            case RTE_ETHER_TYPE_IPV6:
                ipv6_hdr=l3_hdr;
                *l4_proto=ipv6_hdr->proto;
                m->l3_len=sizeof(struct rte_ipv6_hdr);
                *l4_hdr=(char*)l3_hdr+m->l3_len;
                m->ol_flags|=PKT_TX_IPV4;
                break;
            default:
                m->l3_len=0;
                *l4_proto=0;
                *l4_hdr=NULL;
                break;
        }
        if(*l4->proto==IPPROTO_TCP){
            tcp_hdr=(struct rte_tcp_hdr *)*l4_hdr;
            m->l4_len=(tcp_hdr->data_off&0x0f)>>2;
        }else if(*l4->proto==IPPROTO_UDP){
            m->l4_len=sizeof(struct rte_udp_hdr);
        }else{
            m->l4_len=0;
        }
        
    }
    

笔记

virtio_has_feature

virtio_has_feature 是一个用于检查 VirtIO 设备是否支持某个特定特性的函数或宏。VirtIO 是一种虚拟化设备标准,用于在虚拟机和宿主机之间进行高性能的设备通信。

在 VirtIO 驱动程序或代码中,virtio_has_feature 可能是一个宏或函数,用于检查 VirtIO 设备是否支持某个特定的功能或特性。它通常接受两个参数:设备结构体和特性标识符。

例如,在 Linux 内核的 VirtIO 驱动程序中,可以使用 virtio_has_feature 宏来检查设备是否支持指定的特性。示例代码如下:

if (virtio_has_feature(vdev, VIRTIO_F_VERSION_1)) {
    // 设备支持版本 1 特性
    // 执行相应操作
} else {
    // 设备不支持版本 1 特性
    // 执行其他操作
}

上述代码使用 virtio_has_feature 宏来检查设备结构体 vdev 是否支持 VIRTIO_F_VERSION_1 特性。根据返回值,可以根据设备是否支持该特性来执行不同的操作。

请注意,virtio_has_feature 的确切实现可能因使用的 VirtIO 实现和代码环境而异。在具体的代码库或文档中查找有关 virtio_has_feature 函数或宏的详细信息是建议的。