背景和意义
在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)

-
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
-
6.将命令封装成message,并发送给dpdk。在结构体
VhostUserRequest中,添加msg type:
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函数中,添加头部检测的函数

-
先实现
检测包类型的函数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 函数或宏的详细信息是建议的。