基于netmap的用户态协议栈(一)

前期准备:
#pragma pack(1) 的意义是设置结构体的边界对齐为1个字节,也就是所有数据在内存中是连续存储的。这样能节约内存资源,但是会在效率上有所影响。虽说在效率上有一定的影响,不过,如果编写的是基于协议,如串口通讯的程序,那么必须严格按照一定的规则进行接收数据包。那么,只要#pragma pack(1),让数据在内存中是连续的,才好处理的。
柔性数组:详解


用户态协议栈的意义

在内核实现协议栈往往存在两次拷贝过程,一是网卡中的数据通过sk_buff拷贝到内核,之后再从内核拷贝到进程用户空间。

于是我们考虑如何加快这个过程,减少拷贝次数。一种思路:通过DMA直接将数据从网卡拷贝到内存中,于是应用程序可以直接通过mmap从内存中取得数据。由于DMA的工作是不需要CPU干预的,所以对于CPU来说相当于没有做任何的拷贝操作,即所谓零拷贝。

那么应用进程如何通过这种方式从网卡直接取得数据?常用几种方法:

  • 使用raw socket
  • netmap框架
  • dpdk框架

那还为啥还要使用内核协议栈

1,内核态协议栈可以为多个网络应用服务,用户态协议栈就不行。比如DPDK,它会将网卡从内核unbind,然后自己独占。
2,内核态协议栈提供了丰富的协议支持/调试手段,稳定性也经过时间的检验。反观用户态协议栈处于起步阶段,对网络协议的支持有限,大都是适配应用场景的二次开发。
如果是小型设备,通用需求,请使用内核协议栈
如果是大型设备/数据中心,需求固定,请使用DPDK等用户态协议栈,结合网络虚拟化技术,会获得最适合的性能。

netmap的原理分析

Netmap是基于零拷贝思想的高速网络I/O架构,它能够在千兆或万兆网卡上达到网卡的线速收发包速率。并且能够有效地节省cpu等计算机资源。
其基本原理可参考netmap原理分析

常用接口说明

主要头文件:netmap.h 和 netmap_user.h,位于源码包的./netmap/sys/net/目录下。
netmap.h 被 netmap_user.h调用,里面定义了一些宏和几个主要的结构体。一般来说,如果仅仅只是想要收发数据,在上手时我们知道下面几个接口就可以了。

  • nm_open
struct nm_desc *nm_open(const char *ifname, const struct nmreq *req,
						uint64_t new_flags, const struct nm_desc *arg)

nm_open针对ifname指示的网卡接口启用netmap并返回针对该接口的描述符结构体。一般直接这样调用既可:

struct nm_desc *nmr = nm_open("netmap:eth1", NULL, 0, NULL);

struct nm_desc中包含一个fd指向/dev/netmap,可用于poll、epoll等系统调用。

  • nm_nextpkt
    nm_nextpkt()用于接收网卡上收到的数据包。它会将内部所有接收环检查一遍,如果有需要接收的数据包,则返回这个数据包。一次只能返回一个以太网数据包。因为接收到的数据包没有经过协议栈处理,因此需要在用户程序中自己解析。
    读一个数据包时一般这样调用,stream即为数据在缓冲区中的首地址,struct nm_pkthdr为返回的数据包头部信息,不需要管头部的话直接从stream去取数据就行
struct nm_pkthdr nmhead = {0};
char* stream = nm_nextpkt(nmr, &nmhead);
  • nm_inject
    nm_inject()是用于往共享内存中写入待发送的数据包,数据再被从共享内存拷贝到网卡,进而发送出去。它检查所有的发送环,找到一个可以发送的槽后将数据写入。一次只能发送一个包,包的长度由参数指定。一般的调用方式:
nm_inject(nmr, &datapack, packlen);

安装netmap

呵呵,我安装了半天呜呜呜。
这里推荐一个比较好的教程,照着按就行
netmap安装教程一

协议栈的数据结构定义

下图是实现一个UDP协议时的网络协议栈结构及其数据封装的简化形式,我们的用户态协议栈需要实现链路层、网络层以及传输层。按照图中数据包结构,我们需要依次提取链路层、网络层、传输的首部,并最终得到用户数据。
在这里插入图片描述

链路层

在这里插入图片描述

#pragma pack(1)
#define ETH_ADDR_LENGTH		6
#define PROTO_IP			0x0800
#define PROTO_ARP			0x0806
#define PROTO_UDP			17
#define PROTO_ICMP			1
/*以太网*/
struct ethhdr {
	unsigned char h_dst[ETH_ADDR_LENGTH];	//目的地址mac地址
	unsigned char h_src[ETH_ADDR_LENGTH];	//源地址
	unsigned short h_proto;	//类型
	
}; // 14

IP首部

在这里插入图片描述

/*ip头*/
struct iphdr {

	unsigned char hdrlen:4,
				  version:4; // 0x45/*版本号和首部长度*/首部长度是低四位,版本长度是高四位
	unsigned char tos;	//服务类型
	unsigned short totlen;//总长度
	unsigned short id;//16位标识  取余更新
	unsigned short flag_offset; //标志
	unsigned char ttl; //time to live
	// 0x1234// htons
	unsigned char type;//8位协议
	unsigned short check;//检验值
	unsigned int sip;//源ip
	unsigned int dip;//目的ip
}; // 20

UDP首部

在这里插入图片描述

struct udphdr {
	unsigned short sport;//源端口
	unsigned short dport;//目的端口
	unsigned short length;//长度
	unsigned short check;//校验值
}; // 8

用户态协议栈实现(一)

协议栈实现的第一部分,简单地实现UDP接收、ARP响应和ICMP响应。

实现一个简单的UDP协议数据接收

UDP数据包封装

首先定义UDP的整个数据包。这里涉及到了柔性数组(零长度数组)用以定义用户数据包的起始地址而不占用实际的结构体空间。

/*用来封装成数据包*/
struct udppkt {
	struct ethhdr eh; // 14	以太网
	struct iphdr ip;  // 20 ip头
	struct udphdr udp; // 8	
	unsigned char data[0];//用户数据(不能定义固定长度)
	/*柔性数组?*/
}; // sizeof(struct udppkt) == 

代码展示

int main() {

	struct nm_pkthdr h;
	/*数据包*/
	//struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
	struct nm_desc *nmr = nm_open("netmap:ens33", NULL, 0, NULL);
	if (nmr == NULL) return -1;

	/*检测fd是否可读*/
	struct pollfd pfd = {0};
	pfd.fd = nmr->fd;
	pfd.events = POLLIN;

	while (1) {

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

		if (pfd.revents & POLLIN) {

			/*用来接收网卡上到来的数据包的函数*/
			unsigned char *stream = nm_nextpkt(nmr, &h);

			struct ethhdr *eh = (struct ethhdr *)stream;
			if (ntohs(eh->h_proto) ==  PROTO_IP) {

				struct udppkt *udp = (struct udppkt *)stream;

				if (udp->ip.type == PROTO_UDP) { //

					int udplength = ntohs(udp->udp.length);

					udp->data[udplength-8] = '\0';

					printf("udp --> %s\n", udp->data);

				} else if (udp->ip.type == PROTO_ICMP) {

					

				}
				

			} 
			/*此为arp响应部分可忽略*/
			else if (ntohs(eh->h_proto) ==  PROTO_ARP) {

				struct arppkt *arp = (struct arppkt *)stream;

				struct arppkt arp_rt;

				if (arp->arp.dip == inet_addr("192.168.112.119")) { //

					echo_arp_pkt(arp, &arp_rt, "00:0c:29:ae:a9:6e");
					/*nm_inject()是用来往共享内存中写入待发送的数据包数据的。数据包经共享内存拷贝到
网卡,然后发送出去。所以 nm_inject()是用来发包的。*/
					nm_inject(nmr, &arp_rt, sizeof(arp_rt));
					printf("arp ret\n");
				}
			}
		}
	}
}

实现ARP协议

ARP全称“地址解析协议”,它为IP地址到对应的MAC地址之间提供动态映射。因为一台主机将以太网数据帧发送到同一局域网内的另一台主机时,是根据6字节的MAC地址来确定目的接口的。对应的还有RARP(逆地址解析协议),即从MAC地址找到IP地址。

设备驱动程序从不检查IP数据包中的目的IP地址。

当一台主机要向另一个IP地址发送数据时,发现自己的ARP表中并没有记录该IP对应的MAC地址,于是就广播这个ARP请求,然后后等待对方的响应。请求的大致意思就是:“如果你是这个IP地址的持有者,请告诉我你的MAC地址”。

ARP协议的报文格式

在这里插入图片描述

  • op操作字段:指出4种操作类型:ARP请求(为1)、ARP应答(为2)、RARP请求(为3)、RARP应答(为4)

一个ARP请求的工作流程就是:

请求端发送一个ARP请求报文,op为1,发送端以太网地址和IP地址就是它自己的地址,目的IP地址为接收端的地址,目的以太网地址为空;
接收端收到ARP请求后,将两个发送端地址分别填到目的地址中,然后自己的以太网地址和IP地址填入两个发送端地址字段,记得将op改为2;同时别忘了把以太网首部的地址也做对应修改;最后将报文发送出去。

结构定义

/*qrp头*/
struct arphdr {

	unsigned short h_type;//硬件类型
	unsigned short h_proto;
	unsigned char h_addrlen;//协议长度
	unsigned char h_protolen;
	unsigned short oper;//操作返送请求和响应
	unsigned char smac[ETH_ADDR_LENGTH];//mac地址
	unsigned int sip;//ip地址
	unsigned char dmac[ETH_ADDR_LENGTH];
	unsigned int dip;
};
/*arp包以太网和arp*/
struct arppkt {
	struct ethhdr eh;
	struct arphdr arp;
};

代码展示

int str2mac(unsigned 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;
}


//arp响应
/*echo_arp_pkt(arp, &arp_rt, "00:0c:29:ae:a9:6e");*/
/*把源改成目的,把目的改成源*/
void echo_arp_pkt(struct arppkt *arp, struct arppkt *arp_rt, char *mac) {

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

	memcpy(arp_rt->eh.h_dst, arp->eh.h_src, ETH_ADDR_LENGTH);
	str2mac(arp_rt->eh.h_src, mac);
	arp_rt->eh.h_proto = arp->eh.h_proto;

	arp_rt->arp.h_addrlen = 6;
	arp_rt->arp.h_protolen = 4;
	arp_rt->arp.oper = htons(2);//操作数改下
	
	str2mac(arp_rt->arp.smac, mac);
	arp_rt->arp.sip = arp->arp.dip;
	
	memcpy(arp_rt->arp.dmac, arp->arp.smac, ETH_ADDR_LENGTH);
	arp_rt->arp.dip = arp->arp.sip;

}

//
int main() {

	struct nm_pkthdr h;
	/*数据包*/
	//struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
	struct nm_desc *nmr = nm_open("netmap:ens33", NULL, 0, NULL);
	if (nmr == NULL) return -1;

	/*检测fd是否可读*/
	struct pollfd pfd = {0};
	pfd.fd = nmr->fd;
	pfd.events = POLLIN;

	while (1) {

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

		if (pfd.revents & POLLIN) {

			/*用来接收网卡上到来的数据包的函数*/
			unsigned char *stream = nm_nextpkt(nmr, &h);

			struct ethhdr *eh = (struct ethhdr *)stream;
			if (ntohs(eh->h_proto) ==  PROTO_IP) {

				struct udppkt *udp = (struct udppkt *)stream;

				if (udp->ip.type == PROTO_UDP) { //

					int udplength = ntohs(udp->udp.length);

					udp->data[udplength-8] = '\0';

					printf("udp --> %s\n", udp->data);

				} else if (udp->ip.type == PROTO_ICMP) {

					

				}
				

			} else if (ntohs(eh->h_proto) ==  PROTO_ARP) {

				struct arppkt *arp = (struct arppkt *)stream;

				struct arppkt arp_rt;

				if (arp->arp.dip == inet_addr("192.168.112.119")) { //

					echo_arp_pkt(arp, &arp_rt, "00:0c:29:ae:a9:6e");
					/*nm_inject()是用来往共享内存中写入待发送的数据包数据的。数据包经共享内存拷贝到
网卡,然后发送出去。所以 nm_inject()是用来发包的。*/
					nm_inject(nmr, &arp_rt, sizeof(arp_rt));

					printf("arp ret\n");
				
				}

			}

		}

	}
	
	

}

arp常见问题:arp欺骗(arp广播发送)
举例:

1.主机A要和主机C通信,主机A发出ARP包询问谁是192.168.1.3?请回复192.168.1.1。
2.这时主机B在疯狂的向主机A回复,我是192.168.1.3,我的地址是0A-11-22-33-44-02。
3.由于ARP协议不会验证回复者的身份,造成主机A错误的将192.168.1.3的MAC映射为0A-11-22-33-44-02。

本次arp响应中怎么能成arp欺骗呢?
将代码中判读响应主机ip匹配去掉,是此代码变成上述的主机B

if (arp->arp.dip == inet_addr("192.168.112.119")) //此判断去掉

ARP欺骗详细讲解:arp欺骗原理及实现

ICMP后续补充

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程

  • 22
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 22
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值