用户空间协议栈设计和netmap综合指南,将网络效率提升到新高度

举报
Lion Long 发表于 2023/07/30 11:46:06 2023/07/30
【摘要】 这篇综合指南将深入探讨用户空间协议栈设计和netmap技术,以提高网络效率。我们将详细介绍用户空间协议栈的工作原理、优点和挑战,并提供一系列优化策略。同时,我们将重点介绍netmap技术,它是一个高性能数据包I/O框架,可以极大地提升网络吞吐量和响应速度。通过结合用户空间协议栈设计和netmap技术,读者将了解如何最大限度地提高网络连接的速度和效率。

一、协议概念

1.1、七层网络模型和五层网络模型

image.png

image.png

应用层: 最接近用户的一层,为用户程序提供网络服务。主要协议有HTTP、FTP、TFTP、SMTP、DNS、POP3、DHCP等。
表示层: 数据的表示、安全、压缩。管理数据的解密和加密。
会话层: 负责在网络中的两个节点之间的建立、维持和终止通信。
传输层: 模型中最重要的一层,负责传输协议的流控和差错校验。数据包离开网卡后进入的就是传输层;主要协议有:TCP、UDP等。
网络层: 将网络地址翻译成对应的物理地址。主要协议有:ICMP、IP等。
数据链路层: 建立逻辑连接、进行硬件地址寻址、差错校验等功能,解决两台相连主机之间的通信问题。主要协议有SLIP、以太网协议/MAC帧协议、ARP和RARP等。
物理层: 模型的最低层,建立、维护、断开物理连接,传输比特流。常见的物理媒介有光纤、电缆、中继器等。主要协议有RS232等。

1.2、以太网

以太网不是一种网络,而是一种局域网技术,它既有数据链路层内容,也有一些物理层内容。局域网技术除了以太网外,还有令牌环网、无线LAN/WAN等。
以太网的网线必须是双绞线,以太网中的所有主机共享一个通信通道; 当局域网中一台主机发送数据后,该局域网的所有设备都会收到该数据。因为共用一个通信通道,因此同一时刻只允许一台主机发送数据;如果同一时刻不只有一个主机发送数据,为避免干扰,该主机会执行碰撞避免算法(等待一段时间后再进行数据重发)。
以太网帧格式如下:
image.png

源地址和目的地址是指网卡MAC地址,长度是48 bit(6字节)。帧协议类型字段有三种,分别对应IP协议、ARP协议和RARP协议。帧末尾是CRC校验码。

定义一个以太网头结构体示例代码:

#define ETHER_ADDR_LEN	6
struct etherhdr {
	unsigned char dst_mac[ETHER_ADDR_LEN];
	unsigned char src_mac[ETHER_ADDR_LEN];
	unsigned short protocol;
};

输出它的大小:

sizeof(struct etherhdr) = 14

1.3、IP协议

IP协议全称Internet Protocol,即网际互连协议,存在于网络层,负责数据在网络中传输。
IP协议格式如下:

 0              |1              |2              |3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-------+-------+---------------+-------------------------------+
|version|hdr_len| Type Of Server| total length	          	|
+-------------------------------+-----+-------------------------+
|					ID		 |flag |	framegament offset	|
+---------------+---------------+-------------------------------+
| TTL		   | Protocol      | header CRC					|
+---------------------------------------------------------------+
|						Source IP								|
+---------------------------------------------------------------+
|						Destination IP							|
+---------------------------------------------------------------+
|							Option (if have)					|
+---------------------------------------------------------------+
|							 Data								|
|							 ...								|
+---------------------------------------------------------------+

定义一个IP协议头结构体示例代码:

struct iphdr {
	unsigned char version : 4,
		hdrlen : 4;
	
	unsigned char tos;

	unsigned short totlen;

	unsigned short id;
	unsigned short flag : 3,
		offset : 13;
	unsigned char ttl;

	unsigned char protocol;

	unsigned short check;

	unsigned int sip;
	unsigned int dip;
};

1.4、ARP协议

ARP协议全称Address Resolution Protocol,即地址解析协议,是根据IP地址获取MAC地址的一个TCP/IP协议。

ARP协议的作用:在同一个局域网中要给对方发消息,就必须得知道对方的MAC地址,而实际大部分情况下只知道对方的IP地址,因此需要通过ARP协议来根据IP地址来获取目标主机的MAC地址。
ARP的数据格式如下:

 0              |1              |2              |3				|4				|5
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7
+-----------------------------------------------------------------------------------------------+
|								 Ethernet Destination IP										|
+-----------------------------------------------------------------------------------------------+
|								 Ethernet Source IP												|
+-------------------------------+-------------------------------+-------------------------------+
|	framegament type			|   harware address tpye		|	Protocol address type		|
+---------------+---------------+-------------------------------+-------------------------------+
| HW_addr_length|Pro_addr_len   | op code						| Source MAC 					|
+---------------------------------------------------------------+-------------------------------|
|								 Source MAC 					| Source IP						|
+-------------------------------+---------------------------------------------------------------|
|		Source IP				|		Destination MAC											| 
+-------------------------------+---------------------------------------------------------------+
|		Destination MAC			| 		Destination IP											|
+-----------------------------------------------------------------------------------------------+
|							 																	|
|										PAD				 										|
|														 										|
+-----------------------------------------------------------------------------------------------+

可以看出,ARP是MAC帧协议的上层协议,前3个字段和最后一个字段对应的就是以太网头部。由于ARP数据包的长度不足46字节,因此ARP数据包在封装成为MAC帧时还需要补上18字节的填充字段。
定义一个arp协议头结构体示例代码:

struct arphdr{
	unsigned short h_type;
	unsigned short h_proto;
	unsigned char	h_addrlen;
	unsigned char	protolen;
	unsigned short	oper;
	unsigned char	smac[ETH_ALEN];
	unsigned int	sip;
	unsigned char	dmac[ETH_ALEN];
	unsigned int	dip;
	// pad
};

1.4.1、ARP攻击原理

arp攻击得到主要目的是使网络无法正常通信。 向局域网中的所有主机发送ARP应答,其中包含网关IP地址和虚假的MAC地址。局域网中的主机收到ARP应答跟新ARP表后,再发送数据时,就会发送到虚假的MAC地址导致通信故障,就无法和网关正常通信,导致无法访问互联网。

1.4.2、ARP欺骗原理

ARP欺骗并不会使网络无法正常通信,而是通过冒充网关或其他主机 使得 到达网关或主机的数据流量通过攻击主机进行转发。

比如冒充网关:ARP欺骗发送arp应答给局域网中其他的主机,其中包含网关的IP地址和进行ARP欺骗的主机MAC地址;并且也发送了ARP应答给网关,其中包含局域网中所有主机的IP地址和进行arp欺骗的主机MAC地址。当局域网中主机和网关收到ARP应答跟新ARP表后,主机和网关之间的流量就需要通过攻击主机进行转发。
冒充主机的过程和冒充网关相同。

1.5、ICMP协议

ICMP全称Internet Control Message Protocol,即互联网控制消息协议,位于 IP 报文的数据段。虽然ICMP是网络层协议,但它不直接传递数据到数据链路层,而是封装成IP数据包再传递到数据链路层,IP数据包中的协议类型字段为1就表示ICMP报文。ICMP协议的类型主要有两类:查询报文和差错报文。
ICMP报文格式如下:

 0              |1              |2              |3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+---------------+---------------+-------------------------------+
| 	type		| code			| 			CRC					|
+-------------------------------+-------------------------------+
|					ID			|			Sequence Number		|
+---------------------------------------------------------------+
|						mask									|
+---------------------------------------------------------------+
标识 含义
type 类型,0 代表应答 ICMP 报文、8 代表请求 ICMP 报文。
code 代码,type= 8 && code= 0 表示回显请求(ping 请求);type= 0 && code= 0 表示回显应答(ping 应答);type = 11 && code = 0 表示超时。
CRC 校验和,包括数据在内的整个 ICMP 数据报的检验和
ID 标识符,将发送进程的 ID 号放置在标识符字段,这样即使在主机上运行了多个 ping 程序,ping 程序也可以识别出返回的信息。
Sequence Number 序列号,从 0 开始,每发送一次心得回显请求就加 1 。
mask 子网掩码

定义一个ICMP协议头结构体示例代码:

// ICMP
struct icmphdr {
	unsigned char type;
	unsigned char code;
	unsigned short check;
	unsigned short identifier;
	unsigned short sep;
	unsigned cahr	data[32];

};

ICMP的应用:
(1)ping命令。向目的服务器发送回显请求,目的服务器发送回显应答;计算发送回显请求数据包的时间与接收到回显应答数据包的时间差,就是数据包一去一回所需要的时间。
(2)traceroute命令。traceroute命令利用 ICMP 差错报文类型,用作追踪路由信息。前提条件是路由器没有禁用 ICMP。

1.6、MTU概念

MTU,全称Maximum Transmission Unit,即最大传输单元。说明一次数据帧可以发送或接收的最大数据量;以字节为单位,一般是是1500,不同网络类型的MTU不同。

(1)如果一次发送要发送的数据超过MTU,需要在IP层对数据进行分片。数据分片和组装在IP层,因为不同网络的MTU不同,不仅源主机可能需要对数据进行分片,数据传输过程中的路由器也可能对数据分片。

(2)以太网规定数据的最小长度为46字节,如果发送数据小于46字节,需要填充,比如ARP数据包就需要填充才能发送。

(3)对于UDP,是定长的8字节报头,如果IP报头没有携带可选项字段,那么UDP一次携带的数据最大为1500-20-8=1472字节,如果超出这个大小就需要在IP层进行分片。分片带来的后果是增加UDP的丢包率。

(4)分片也会增加TCP的丢包率,不过TCP有重传机制;因此需要尽可能避免分片,降低TCP重传次数。

1.7、MSS概念

MSS,全称Maximum Segment Size,即最大报文段大小。表示TCP传往另一端的最大块数据的长度。
当一个连接建立时,连接的双方都要提供各自的MSS。通过协商确定MSS的值(双方MSS的最小值)以避免TCP分片。如果没有分段发生, MSS越大越好。

1.8、TTL概念

TTL,全称Time To Live,即存活时间;指一个数据包可传递的最长距离(跃点数)。
当一个数据包经过一个路由器时,TTL减一;当TTL=0时路由器就会取消数据包的转发。
我们知道网络是有 环 存在的,设计TTL的目的是防止数据包因为不正确的路由表等原因造成无线循环而无法送达导致耗尽网络资源。

二、数据传输框图

网络上所有的数据传输都要经过网卡,网卡将模拟信号转换为数字信号,也就是将物理层信号转换为数据链路层信号。
image.png

注意:
(1)send()返回成功不代表发送成功,send()只是包数据拷贝到写缓冲区,真正发送数据由协议栈完成。如果客户端宕机而服务端一直执行send(),那么在一段时间后send()会返回-1;因为写缓冲区中的数据没有发送出去导致写缓冲区爆满。
(2)协议栈就是数据根据七层网络模型,自顶向下一层一层的协议头包住数据;接收端也是根据七层网络模型,自底向上一层层的解析协议。
(3)驱动如何把数据传递到协议栈?
在Linux kernel有一个sk_buffer结构,sk_buffer将驱动获取的数据通过sk_buffer传递到协议栈中。关于整个过程的执行流程可以参考这篇文章:链接

三、校验和 checksum的计算方法

(1) 先将需要计算checksum数据中的checksum字段设为0;
(2) 将checksum的数据按2 byte(16 bit)划分,如果最后有单个byte的数据,则在其后面补1 byte的0构成2 byte;
(3) 将所有的2 byte(16 bit)z值累加,得到一个4 byte(32 bit)的值;
(4)将得到的4 byte(32 bit)的值的高16bit与低16bit相加得到一个新的4 byte(32 bit)值;若新值大于0xFFFF,再将新值的高16bit与低16bit相加。
(5)将上一步计算所得的值按位取反,即得到checksum值,保存到checksum字段即可。
示例代码:

unsigned short in_cksum(unsigned short *addr,int len)
{
	register int nleft = len;
	register unsigned short *w = addr;
	register int sum = 0;//32bit
	unsigned short answer = 0;//16bit

	while (nleft > 1)
	{
		sum += *w++;//16bit为一组累加
		nleft -= 2;
	}

	if (nleft == 1)//存在单个byte情况
	{
		*(u_char*)(&answer) = *(u_char*)w;
		sum += answer;
	}
	sum = (sum >> 16) + (sum & 0xffff);// 高16bit与低16bit相加
	sum += (sum >> 16);//防止值大于0xffff

	//结果
	answer = ~sum;

	return (answer);
}

四、协议栈设计–netmap

要实现一个协议栈,那么就需要获得原始的协议数据。
image.png

4.1、获取原始协议数据的方法

(1)raw socket,即原始套接字,可以接收本机网卡的数据帧或数据包。有四种方式创建这类socket。

目标 实现
发送接收IP数据包 socket(PF_INET,SOCK_RAW,IPPROTO_TCP | IPPROTO_UDP | IPPROTO_ICMP) ;
发送接收以太网数据帧 socket(PF_PACKET,SOCK_RAW,htons(ETH_P_IP | ETH_P_ARP | ETH_P_ALL));
发送接收以太网数据帧(不包括以太网头部) socket(PF_PACKET,SOCK_DGRAM,htons(ETH_P_IP | ETH_P_ARP | ETH_P_ALL));

(2)旁路。netmap、dpdk等
(3)hook。bpf、ebpf等

4.2、零长数组

零长数组,顾名思义,就是长度为零的数组。一般在GUN C中使用,其他编译器使用可能会报错或警告。
零长度数组的一个特点是它不占用内存存储空间。如下示例:

#include <stdio.h>

char test[0];

int main()
{
	printf("size = %ld\n",sizeof(test));
	return 0;
}
// 输出 为 0

在结构体中使用,它同样也不占内存:

#include <stdio.h>

struct test{
    int len;
    int ch[0];
};

int main(void)
{
      printf("size of = %ld\n",sizeof(struct test));
      return 0;
}

零长数组的使用:内存已经分配,但数据长度不确定,需要计算出数据长度的,就可以使用零长数组。零长数组在内存池中使用比较多。
使用示例:

#include <stdio.h>

struct test{
    int len;
    char ch[0];
};
int main(void)
{
    struct test *buf;
    buf = (struct test *)malloc(sizeof(struct test)+ 16);
 	memset(buf,0,sizeof(struct test)+ 16);
 	
    strcpy(buf->ch, "hello world\n");
    puts(buf->ch);
 
    free(buf);  
    return 0;
}

4.3、修改ens33为eth0

(1)打开/etc/default/grub

sudo nano /etc/default/grub

(2)找到GRUB_CMDLINE_LINUX=" "改为GRUB_CMDLINE_LINUX=“net.ifnames=0 biosdevname=0”
(3)写入配置

sudo grub-mkconfig -o /boot/grub/grub.cfg

(4)重启系统

reboot

4.4、netmap下载安装

以ubuntu为例。
(1)切换到根目录:

cd /

(2)切换到root权限:

sudo su

(3)在根目录clone netmap:

git clone https://github.com/luigirizzo/netmap.git
正克隆到 'netmap'...
remote: Enumerating objects: 28670, done.
remote: Counting objects: 100% (978/978), done.
remote: Compressing objects: 100% (397/397), done.
remote: Total 28670 (delta 603), reused 867 (delta 533), pack-reused 27692
接收对象中: 100% (28670/28670), 10.13 MiB | 2.72 MiB/s, 完成.
处理 delta 中: 100% (18306/18306), 完成.

(4)安装编译环境:

apt-get install build-essential

(5)进入netmap/LINUX 目录

cd /netmap/LINUX/

(6)执行配置:

./configure

此过程会下载一些东西,然后提示耐心等待一段时间,过程有点久,请耐心等待,如下。

image.png

(7)编译和安装:

make && make install

此过程也需要耐心等待一段时间,过程有点久。

......

##install -D -m 644 ice.7.gz //usr/share/man/man7/ice.7.gz
/sbin/depmod -e -F /boot/System.map-4.15.0-142-generic  -a 4.15.0-142-generic
Updating initramfs...
update-initramfs -u
update-initramfs: Generating /boot/initrd.img-4.15.0-142-generic
make[1]: Leaving directory '/netmap/LINUX/ice-1.7.16/src'
make -C ixgbe install INSTALL_MOD_PATH= CFLAGS_EXTRA="-Wno-unused-but-set-variable -Wno-attributes -Wno-maybe-uninitialized -Wno-unused-variable -Wno-unused-label -I/netmap/LINUX -I/netmap/LINUX -I/netmap/LINUX/../sys -I/netmap/LINUX/../sys/dev -DCONFIG_NETMAP -Wno-unused-but-set-variable -g -DCONFIG_NETMAP_NULL -DCONFIG_NETMAP_PTNETMAP -DCONFIG_NETMAP_GENERIC -DCONFIG_NETMAP_MONITOR -DCONFIG_NETMAP_PIPE -DCONFIG_NETMAP_VALE" NETMAP_DRIVER_SUFFIX= KSRC=/lib/modules/4.15.0-142-generic/build KBUILD_EXTRA_SYMBOLS=/netmap/LINUX/Module.symvers
make[1]: Entering directory '/netmap/LINUX/ixgbe-5.3.8/src'
make[2]: Entering directory '/usr/src/linux-headers-4.15.0-142-generic'
  Building modules, stage 2.
  MODPOST 1 modules
make[2]: Leaving directory '/usr/src/linux-headers-4.15.0-142-generic'
Copying manpages...
Installing modules...
make[2]: Entering directory '/usr/src/linux-headers-4.15.0-142-generic'
  INSTALL /netmap/LINUX/ixgbe-5.3.8/src/ixgbe.ko
At main.c:160:
- SSL error:02001002:system library:fopen:No such file or directory: bss_file.c:175
- SSL error:2006D080:BIO routines:BIO_new_file:no such file: bss_file.c:178
sign-file: certs/signing_key.pem: No such file or directory
  DEPMOD  4.15.0-142-generic
make[2]: Leaving directory '/usr/src/linux-headers-4.15.0-142-generic'
Running depmod...
make[1]: Leaving directory '/netmap/LINUX/ixgbe-5.3.8/src'
make -C igb install INSTALL_MOD_PATH= CFLAGS_EXTRA="-DDISABLE_PACKET_SPLIT -fno-pie -I/netmap/LINUX -I/netmap/LINUX -I/netmap/LINUX/../sys -I/netmap/LINUX/../sys/dev -DCONFIG_NETMAP -Wno-unused-but-set-variable -g -DCONFIG_NETMAP_NULL -DCONFIG_NETMAP_PTNETMAP -DCONFIG_NETMAP_GENERIC -DCONFIG_NETMAP_MONITOR -DCONFIG_NETMAP_PIPE -DCONFIG_NETMAP_VALE" NETMAP_DRIVER_SUFFIX= KSRC=/lib/modules/4.15.0-142-generic/build KBUILD_EXTRA_SYMBOLS=/netmap/LINUX/Module.symvers
make[1]: Entering directory '/netmap/LINUX/igb-5.3.5.20/src'
make[2]: Entering directory '/usr/src/linux-headers-4.15.0-142-generic'
  Building modules, stage 2.
  MODPOST 1 modules
make[2]: Leaving directory '/usr/src/linux-headers-4.15.0-142-generic'
Copying manpages...
Installing modules...
make[2]: Entering directory '/usr/src/linux-headers-4.15.0-142-generic'
  INSTALL /netmap/LINUX/igb-5.3.5.20/src/igb.ko
At main.c:160:
- SSL error:02001002:system library:fopen:No such file or directory: bss_file.c:175
- SSL error:2006D080:BIO routines:BIO_new_file:no such file: bss_file.c:178
sign-file: certs/signing_key.pem: No such file or directory
  DEPMOD  4.15.0-142-generic
make[2]: Leaving directory '/usr/src/linux-headers-4.15.0-142-generic'
Running depmod...
make[1]: Leaving directory '/netmap/LINUX/igb-5.3.5.20/src'
make -C virtio_net.c install INSTALL_MOD_PATH= EXTRA_CFLAGS="-I/netmap/LINUX -I/netmap/LINUX -I/netmap/LINUX/../sys -I/netmap/LINUX/../sys/dev -DCONFIG_NETMAP -Wno-unused-but-set-variable -g -DCONFIG_NETMAP_NULL -DCONFIG_NETMAP_PTNETMAP -DCONFIG_NETMAP_GENERIC -DCONFIG_NETMAP_MONITOR -DCONFIG_NETMAP_PIPE -DCONFIG_NETMAP_VALE" NETMAP_DRIVER_SUFFIX= KSRC=/lib/modules/4.15.0-142-generic/build
make[1]: Entering directory '/netmap/LINUX/virtio_net.c'
make -C "/lib/modules/4.15.0-142-generic/build" M=/netmap/LINUX/virtio_net.c modules_install
make[2]: Entering directory '/usr/src/linux-headers-4.15.0-142-generic'
  INSTALL /netmap/LINUX/virtio_net.c/virtio_net.ko
At main.c:160:
- SSL error:02001002:system library:fopen:No such file or directory: bss_file.c:175
- SSL error:2006D080:BIO routines:BIO_new_file:no such file: bss_file.c:178
sign-file: certs/signing_key.pem: No such file or directory
  DEPMOD  4.15.0-142-generic
make[2]: Leaving directory '/usr/src/linux-headers-4.15.0-142-generic'
make[1]: Leaving directory '/netmap/LINUX/virtio_net.c'
make -C build-apps/dedup install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/dedup'
install -D dedup //usr/local/bin/dedup
install -D -m 644 /netmap/LINUX/../apps/lb/lb.8 //usr/local/share/man/man8/lb.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/dedup'
make -C build-apps/vale-ctl install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/vale-ctl'
install -D vale-ctl //usr/local/bin/vale-ctl
install -D -m 644 /netmap/LINUX/../apps/vale-ctl/vale-ctl.4 //usr/local/share/man/man4/vale-ctl.4
make[1]: Leaving directory '/netmap/LINUX/build-apps/vale-ctl'
make -C build-apps/nmreplay install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/nmreplay'
install -D nmreplay //usr/local/bin/nmreplay
install -D -m 644 /netmap/LINUX/../apps/nmreplay/nmreplay.8 //usr/local/share/man/man8/nmreplay.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/nmreplay'
make -C build-apps/tlem install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/tlem'
install -D tlem //usr/local/bin/tlem
install -D -m 644 /netmap/LINUX/../apps/tlem/tlem.8 //usr/local/share/man/man8/tlem.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/tlem'
make -C build-apps/lb install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/lb'
install -D lb //usr/local/bin/lb
install -D -m 644 /netmap/LINUX/../apps/lb/lb.8 //usr/local/share/man/man8/lb.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/lb'
make -C build-apps/bridge install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/bridge'
install -D bridge //usr/local/bin/bridge
install -D -m 644 /netmap/LINUX/../apps/bridge/bridge.8 //usr/local/share/man/man8/bridge.8
install -D bridge-b //usr/local/bin/bridge-b
install -D -m 644 /netmap/LINUX/../apps/bridge/bridge.8 //usr/local/share/man/man8/bridge.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/bridge'
make -C build-apps/pkt-gen install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/pkt-gen'
install -D pkt-gen //usr/local/bin/pkt-gen
install -D -m 644 /netmap/LINUX/../apps/pkt-gen/pkt-gen.8 //usr/local/share/man/man8/pkt-gen.8
install -D pkt-gen-b //usr/local/bin/pkt-gen-b
install -D -m 644 /netmap/LINUX/../apps/pkt-gen/pkt-gen.8 //usr/local/share/man/man8/pkt-gen.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/pkt-gen'
install -m 0644 -D /netmap/LINUX/../sys/net/netmap.h //usr/local/include/net/netmap.h
install -m 0644 -D /netmap/LINUX/../sys/net/netmap_user.h //usr/local/include/net/netmap_user.h
install -m 0644 -D /netmap/LINUX/../sys/net/netmap_virt.h //usr/local/include/net/netmap_virt.h
install -m 0644 -D /netmap/LINUX/../sys/net/netmap_legacy.h //usr/local/include/net/netmap_legacy.h
install -m 0644 -D /netmap/LINUX/../libnetmap/libnetmap.h //usr/local/include/libnetmap.h
install -D -m 644 /netmap/LINUX/../share/man/man4/netmap.4 //usr/local/share/man/man4/netmap.4
install -D -m 644 /netmap/LINUX/../share/man/man4/vale.4 //usr/local/share/man/man4/vale.4
install -D -m 644 /netmap/LINUX/../share/man/man4/ptnet.4 //usr/local/share/man/man4/ptnet.4
make -C build-libnetmap install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-libnetmap'
install -D libnetmap.a //usr/local/lib/libnetmap.a
make[1]: Leaving directory '/netmap/LINUX/build-libnetmap'

(8)使用netmap:

insmod netmap.ko

每次使用前都要执行insmod netmap.ko,它在/netmap/LINUX/路径下。
(9)检查netmap是否insmod成功:

ls /dev/netmap -l

出现如下表示成功:

crw------- 1 root root 10, 54 831 12:53 /dev/netmap

(10)编译运行自己的代码

# 头文件 #include<net/netmap_user.h> 在 /netmap/sys/目录下
# 和/usr/local/include/net/目录下
gcc -o testcode testcode.c -I /netmap/sys/

4.5、协议栈实现代码示例

示例简单实现了arp、icmp、udp的协议栈;其他协议的实现类似。

//需要开启netmap的宏
#define NETMAP_WITH_LIBS

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#include <net/netmap_user.h>
#include <sys/poll.h>
#include <arpa/inet.h>

#pragma pack(1)//设置一字节对齐方式

#define ETH_ALEN		6
#define PROTO_IP		0x0800 //	IP 协议
#define PROTO_ARP		0x0806

#define PROTOCOL_UDP	17
#define PROTO_ICMP		1
#define PROTO_IGMP		2

#define ICMP_TYPE_ANS	0
#define ICMP_TYPE_REQ	8

#define ETHER_ADDR_LEN	6

#define MY_IP			"192.168.7.146"
#define MY_MAC			"00:0c:29:39:a8:c4"
// ether
struct etherhdr {
	unsigned char dst_mac[ETHER_ADDR_LEN];
	unsigned char src_mac[ETHER_ADDR_LEN];
	unsigned short protocol;
};

// IP
struct iphdr {
	unsigned char version : 4,
		hdrlen : 4;
	
	unsigned char tos;

	unsigned short totlen;

	unsigned short id;
	unsigned short flag : 3,
		offset : 13;
	unsigned char ttl;

	unsigned char protocol;

	unsigned short check;

	unsigned int sip;
	unsigned int dip;
};

// UDP
struct udphdr {
	unsigned short sport;
	unsigned short dport;
	unsigned short length;
	unsigned short check;
};

struct udppkt {
	struct etherhdr eth;
	struct iphdr ip;
	struct udphdr udp;

	unsigned char payload[0];// 零长数组

};

// ARP
struct arphdr{
	unsigned short h_type;
	unsigned short h_proto;
	unsigned char	h_addrlen;
	unsigned char	protolen;
	unsigned short	oper;
	unsigned char	smac[ETH_ALEN];
	unsigned int	sip;
	unsigned char	dmac[ETH_ALEN];
	unsigned int	dip;

};

struct arppkt {
	struct etherhdr eth;
	struct arphdr arp;
};

// ICMP
struct icmphdr {
	unsigned char type;
	unsigned char code;
	unsigned short check;
	unsigned short identifier;
	unsigned short sep;
	unsigned char	data[32];

};

struct icmppkt{
	struct etherhdr eth;
	struct iphdr ip;
	struct icmphdr icmp;

};


void echo_udp_pkt(struct udppkt *udp,struct udppkt *udp_rt)
{
	memcpy(udp_rt, udp, sizeof(struct udppkt));
	memcpy(udp_rt->eth.dst_mac, udp->eth.src_mac, ETH_ALEN);
	memcpy(udp_rt->eth.src_mac, udp->eth.dst_mac, ETH_ALEN);

	udp_rt->ip.sip = udp->ip.dip;
	udp_rt->ip.dip = udp->ip.sip;

	udp_rt->udp.sport = udp->udp.dport;
	udp_rt->udp.dport = udp->udp.sport;
}

unsigned short in_cksum(unsigned short *addr,int len)
{
	register int nleft = len;
	register unsigned short *w = addr;
	register int sum = 0;//32bit
	unsigned short answer = 0;//16bit

	while (nleft > 1)
	{
		sum += *w++;//16bit为一组累加
		nleft -= 2;
	}

	if (nleft == 1)//存在单个byte情况
	{
		*(u_char*)(&answer) = *(u_char*)w;
		sum += answer;
	}
	sum = (sum >> 16) + (sum & 0xffff);// 高16bit与低16bit相加
	sum += (sum >> 16);//防止值大于0xffff

	//结果
	answer = ~sum;

	return (answer);
}

void echo_icmp_pkt(struct icmppkt *icmp, struct icmppkt *icmp_rt)
{
	memcpy(icmp_rt, icmp, sizeof(struct icmppkt));

	memcpy(icmp_rt->eth.dst_mac, icmp->eth.src_mac, ETH_ALEN);
	memcpy(icmp_rt->eth.src_mac, icmp->eth.dst_mac, ETH_ALEN);

	icmp_rt->icmp.type = ICMP_TYPE_ANS;
	icmp_rt->icmp.code = 0;
	icmp_rt->icmp.check = 0;

	icmp_rt->ip.sip = icmp->ip.dip;
	icmp_rt->ip.dip = icmp->ip.sip;

	icmp_rt->icmp.check = in_cksum((unsigned short*)&icmp_rt->icmp, sizeof(struct icmphdr));


}


int str2mac(char *mac, char *str) {

	char *p = str;
	unsigned char value = 0x0;
	int i = 0;

	while (p != '\0') {

		if (*p == ':') {
			mac[i++] = value;
			value = 0x0;
		}
		else {

			unsigned char temp = *p;
			if (temp <= '9' && temp >= '0') {
				temp -= '0';
			}
			else if (temp <= 'f' && temp >= 'a') {
				temp -= 'a';
				temp += 10;
			}
			else if (temp <= 'F' && temp >= 'A') {
				temp -= 'A';
				temp += 10;
			}
			else {
				break;
			}
			value <<= 4;
			value |= temp;
		}
		p++;
	}

	mac[i] = value;

	return 0;
}

void echo_arp_pkt(struct arppkt *arp, struct arppkt *arp_rt, char *hmac) {

	memcpy(arp_rt, arp, sizeof(struct arppkt));

	memcpy(arp_rt->eth.dst_mac, arp->eth.src_mac, ETH_ALEN);
	str2mac(arp_rt->eth.src_mac, hmac);
	arp_rt->eth.protocol = arp->eth.protocol;

	arp_rt->arp.h_addrlen = 6;
	arp_rt->arp.protolen = 4;
	arp_rt->arp.oper = htons(2);

	str2mac(arp_rt->arp.smac, hmac);
	arp_rt->arp.sip = arp->arp.dip;

	memcpy(arp_rt->arp.dmac, arp->arp.smac, ETH_ALEN);
	arp_rt->arp.dip = arp->arp.sip;

}

// netmap

int main()
{
	printf("length = %ld\n", sizeof(struct etherhdr));

	struct pollfd pfd = { 0 };// poll
	struct nm_pkthdr h;
	struct etherhdr *eh;
	// 打开/dev/netmap,映射网卡数据到内存空间
	struct nm_desc *nmr = nm_open("netmap:eth0", NULL,0,NULL);

	if (nmr == NULL)
	{
		printf("netmap open fail!\n");
		return -1;
	}

	
	pfd.fd = nmr->fd;// 指向/dev/netmap
	pfd.events = POLLIN;//监听读事件

	while (1)
	{
		int ret = poll(&pfd, 1, -1);
		if (ret < 0)
			continue;

		if (pfd.events & POLLIN)//操作内存
		{
			unsigned char *stream = nm_nextpkt(nmr, &h);//从环形队列中取出一个数据包
			eh = (struct etherhdr *)stream;
			//将网络数据转换为本地字节序
			if (ntohs(eh->protocol) == PROTO_IP)
			{
				struct udppkt *pkt = (struct udppkt *)stream;
				if (pkt->ip.protocol == PROTOCOL_UDP)
				{
					struct in_addr addr;
					addr.s_addr = pkt->ip.sip;
					// udp包length字段表示的是整个UDP包的总长度(包含udp的头长度)
					int length = ntohs(pkt->udp.length);
					printf("%s:%d:length:%d,ip length:%d\n", 
						inet_ntoa(addr),
						pkt->udp.sport,
						length,
						ntohs(pkt->ip.totlen));
					pkt->payload[length - 8] = '\0';
					printf("pkt: %s\n", pkt->payload);

					struct udppkt udp_rt;
					echo_udp_pkt(pkt, &udp_rt);
					nm_inject(nmr, &udp_rt, sizeof(struct udppkt));

				}
				else if (pkt->ip.protocol == PROTO_ICMP)
				{
					struct icmppkt *icmp = (struct icmppkt*)stream;
					printf("icmp------> %d,%x\n",
						icmp->icmp.type,icmp->icmp.check);
					if (icmp->icmp.type == ICMP_TYPE_REQ)//0 代表应答 ICMP 报文、8 代表请求 ICMP 报文。
					{
						struct icmppkt icmp_rt = { 0 };
						echo_icmp_pkt(icmp, &icmp_rt);

						nm_inject(nmr, &icmp_rt, sizeof(struct icmppkt));

					}
				}
				else if (pkt->ip.protocol == PROTO_IGMP)
				{
					printf("PROTO_IGMP packet\n");
				}
				else
				{
					printf("other ip packet\n");
				}
			}
			else if (ntohs(eh->protocol) == PROTO_ARP)
			{
				struct arppkt *arp = (struct arppkt*)stream;
				struct arppkt arp_rt;
				if (arp->arp.dip == inet_addr(MY_IP))
				{
					echo_arp_pkt(arp, &arp_rt, MY_MAC);
					nm_inject(nmr, &arp_rt, sizeof(struct arppkt));
				}
			}
		}
	}

	
	return 0;
}

总结

要实现一个协议栈,需要清楚七层网络模型,熟悉协议标准;获得协议的原始数据包后需要一层层的拨开解析;发送数据之前需要将协议一层层的往下包装。
image.png
image.png

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。