基于netmap的用户态协议栈实现
一 引入
先提几个问题
为什么要做一个用户态的协议栈,好处在哪?
1.用户态实现协议栈可以避免用户态和内核态之间的频繁切换,从而提高网络处理性能。传统的内核态协议栈需要将网络数据包从用户态传递到内核态进行处理,然后再返回给用户态,这个过程涉及到多次上下文切换和数据拷贝,会引入较大的性能开销。而用户态协议栈直接在用户态中进行网络数据包的处理,避免了这些开销,可以更高效地处理网络流量。
2.灵活性和可定制性: 用户态协议栈可以根据具体需求进行定制和优化,灵活适应不同的应用场景。用户态协议栈的实现可以根据应用程序的特点和需求进行优化,例如针对特定的协议进行加速、减少不必要的功能模块、优化内存管理等。这种灵活性和可定制性使得用户态协议栈能够更好地满足特定应用的性能和功能需求。
3.可移植性和跨平台支持:用户态协议栈通常是以库的形式提供,可以在不同的操作系统和平台上使用。相比于依赖于特定操作系统的内核态协议栈,用户态协议栈可以更容易地移植到不同的平台上
那么如何实现用户态的协议栈呢?
这里拿下别人做的网图,讲解一下网络数据包的大致流程
正常的流程是网卡接收到数据后,把数据copy到协议栈(sk_buff),协议栈把sk_buff数据解析完后再把数据放到recv_buff,此时应用程序调用recv把数据从协议栈copy到应用程序;发送数据包,则与之相反,应用程序调用send把数据包copy到send_buff,协议栈从send_buff取数据放到sk_buff,交给网卡发送出去。这个过程有多次拷贝,为避免多次拷贝,使用dma的方式(零拷贝),把网卡的数据直接映射到内存,再由应用程序访问内存。
在这里我们需要做自己的协议栈的话,我们必须要能够拿到网卡的数据。
有这么几种方式
1.直接从网卡拿到原始数据,到用户端处理,这样还是会经过协议栈
2.数据旁路
Netmap安装后相当于多了一个网卡驱动
通过对网卡驱动的扩展,使得网卡可以直接将收到的数据包存储在用户态的内存缓冲区中,而不是传递给操作系统内核的协议栈。这样一来,用户态程序可以直接访问和处理这些数据包。
在启用Netmap后,操作系统的协议栈将不再直接收到网络数据包。Netmap的设计目标之一是绕过操作系统协议栈,将网络数据包直接传递给用户态程序进行处理,以提高性能和降低延迟。
当网卡进入Netmap模式后,数据包将通过Netmap框架直接传递给用户态程序,而不经过操作系统的协议栈。操作系统的协议栈将不会接收、处理或转发这些数据包。
- 利用hook
bpf或者epbf
二 netmap介绍和环境配置
介绍具体可以看下面的文章
netmap介绍
Netmap的原理可以概括为以下几点:
- 网卡驱动的扩展:Netmap通过对网卡驱动的扩展,使得网卡可以直接将收到的数据包存储在用户态的内存缓冲区中,而不是传递给操作系统内核的协议栈。这样一来,用户态程序可以直接访问和处理这些数据包。
- 共享内存环:Netmap引入了一个共享内存环(shared memory
ring),用于在用户态程序和网卡之间传递数据包。网卡将收到的数据包写入共享内存环的发送队列,而用户态程序则从接收队列中读取数据包进行处理。这种基于共享内存的数据包传递方式避免了数据拷贝,提高了性能。 - 零拷贝技术:Netmap利用共享内存环的特性,实现了零拷贝的数据包处理。用户态程序可以直接在共享内存中操作数据包,而无需将数据包从内核态复制到用户态。这样可以减少数据拷贝的开销,提高处理效率。
- 轮询模式:Netmap使用轮询模式来实现高性能的数据包处理。用户态程序可以通过轮询共享内存环中的接收队列,及时处理收到的数据包,而不需要等待中断或轮询操作系统的网络缓冲区。这种主动轮询的方式可以降低处理延迟,并提高吞吐量。
环境配置
这里讲我在插入netmap驱动时,需要修改本地网卡从ens33到ens0,如何修改
首先我的虚拟机系统是ubuntu20.04.6
-
vim /etc/default/grub
-
重建grub配置文件
执行命令:
grub2-mkconfig -o /boot/grub2/grub.cfg
可能会没有grub2-mkconfig命令 那就执行
grub-mkconfig -o /boot/grub/grub.cfg -
网上有些地方要修改/etc/network/interfaces文件,我这里没有修改。(里面只有一个lo地址)
-
重启系统
-
ifconfig,发现已经更改完成
这时会发现虚拟机没网,因为没有IP了,原来ens33的IP是在/etc/network/interfaces里面配的
现在改为ens0后需要这样配(我配置的是静态IP,就是禁用了DHCP)
如果你想要为eth0
网卡配置静态IP地址,可以按照以下步骤进行操作:
- 打开终端,以管理员权限运行以下命令来编辑网络接口的配置文件:
sudo nano /etc/netplan/00-installer-config.yaml
请注意,如果你的系统上的配置文件名称不是00-installer-config.yaml
,请使用相应的文件名进行替换。
- 在打开的文件中,找到
ethernets
部分,如果不存在,请添加以下内容:
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: no
addresses: [192.168.1.100/24]
gateway4: 192.168.1.1
nameservers:
addresses: [8.8.8.8, 8.8.4.4]
在上述配置中,dhcp4: no
指示禁用DHCP,并使用静态IP地址。将192.168.1.100/24
替换为你想要为eth0
网卡配置的静态IP地址和子网掩码。将192.168.1.1
替换为你的网关地址,将8.8.8.8
和8.8.4.4
替换为你的首选和备用DNS服务器地址。
-
保存文件并退出编辑器。
-
运行以下命令来应用配置更改:
sudo netplan apply
配置更改将会生效,并为eth0
网卡配置了指定的静态IP地址、子网掩码、网关和DNS服务器。请确保在修改配置文件之前备份该文件,以防出现意外情况。
最后配上了IP,如果你的虚拟机还是没有网络,可能是VMware的虚拟网络编辑器没有设置好,可以在网上查看如何给虚拟机配上静态IP,VMware这部分是一样的。
3 代码编写
这里只实现了简单的ARP协议,UDP协议,ICMP协议的收发,有需求大家可以自己完善,代码比较简单这里我写了详细的注释。可以自己查看。github自取。github代码
这里我讲下代码的核心逻辑,
利用nm_open函数打开一个网卡,这里是eth0,返回一个句柄
然后就可以用netmap提供的nm_nextpkt函数拿到数据包,然后分协议进行处理就行了,这里用到了linux提供的poll函数进行设计。
每次我们用echo_XXX_pkt 函数对数据包进行回显,再用nm_inject函数把数据包送入网卡。
这里实现ARP协议的目的:
如果只是实现一个UDP协议,会出现数据包发着发着就发不了的问题,原因在于每次主机发送数据包时可能会发送ARP请求虚拟机对应的MAC地址,而我们的虚拟机没有进行回复,这导致,主机的ARP表会失去虚拟机的映射关系,导致不能发送数据包。
ICMP协议,主要是进行Ping功能的测试。
int main() {
struct ethhdr *eh;
struct pollfd pfd = {0};
struct nm_pkthdr h;
unsigned char *stream = NULL;
//打开netmap设备:使用nm_open函数打开netmap设备,这里是"netmap:eth0",表示打开名为eth0的网络接口。
struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
if (nmr == NULL) {
return -1;
}
pfd.fd = nmr->fd;
pfd.events = POLLIN;
while (1) {
//设置轮询事件:使用poll函数设置一个文件描述符的轮询事件,以等待数据包的到达。
int ret = poll(&pfd, 1, -1);
if (ret < 0) continue;
if (pfd.revents & POLLIN) {
//接收数据包:使用nm_nextpkt函数从netmap设备中接收下一个数据包,并将其存储在stream指针中。
stream = nm_nextpkt(nmr, &h);
// 解析以太网头部:将stream指针强制转换为以太网头部结构体ethhdr,以便进一步解析数据包。
eh = (struct ethhdr*)stream;
//通过检查以太网头部中的协议字段,判断数据包的协议类型。
if (ntohs(eh->h_proto) == PROTO_IP) {
struct udppkt *udp = (struct udppkt*)stream;
if (udp->ip.protocol == PROTO_UDP) {
struct in_addr addr;
addr.s_addr = udp->ip.saddr;
int udp_length = ntohs(udp->udp.len);
//于将32位的IPv4地址转换为点分十进制字符串表示的函数
printf("%s:%d:length:%d, ip_len:%d --> ", inet_ntoa(addr), udp->udp.source,
udp_length, ntohs(udp->ip.tot_len));
udp->body[udp_length-8] = '\0';
printf("udp --> %s\n", udp->body);
#if 1
struct udppkt udp_rt;
echo_udp_pkt(udp, &udp_rt);
nm_inject(nmr, &udp_rt, sizeof(struct udppkt));
#endif
} else if (udp->ip.protocol == PROTO_ICMP) { //发送ICMP包
struct icmppkt *icmp = (struct icmppkt*)stream;
printf("icmp ---------- --> %d, %x\n", icmp->icmp.type, icmp->icmp.check);
if (icmp->icmp.type == 0x08) {
struct icmppkt icmp_rt = {0};
echo_icmp_pkt(icmp, &icmp_rt);
//printf("icmp check %x\n", icmp_rt.icmp.check);
nm_inject(nmr, &icmp_rt, sizeof(struct icmppkt));
}
} else if (udp->ip.protocol == PROTO_IGMP) {
} else {
printf("other ip packet");
}
} else if (ntohs(eh->h_proto) == PROTO_ARP) { //如果是arp报文
struct arppkt *arp = (struct arppkt *)stream;
struct arppkt arp_rt;
// 函数将 IPv4 地址字符串转换为 32 位无符号整数时,返回的整数值是以网络字节序(大端字节序)表示的。
if (arp->arp.dip == inet_addr("192.168.82.168")) {
echo_arp_pkt(arp, &arp_rt, "00:0c:29:fb:c6:6b");
nm_inject(nmr, &arp_rt, sizeof(struct arppkt));
}
}
}
}
}
编译代码时可能会出现找不到头文件的情况
采用下面的编译命令,替换为自己的netmap安装路径
gcc -o server icmp_arp_udp_success.c -I /home/XXX/software/netmap-master/sys