Fork me on GitHub

从1写TCPIP协议栈5:IP协议的输入处理

引言

  在第四章节实现ARP地址解析协议后,遵循自底向上的开发思路,我们紧接着需要实现ICMP\IGMP协议,实现这些协议的前提就是先实现IPV4协议的封装和解析接口,这也是后面几章节的主要内容。

IPV4协议介绍

  IP协议是TCPIP协议族的核心协议,后续我们开发练习的TCP/UDP/ICMP/IGMP协议都是封装到到IP报文数据场中。而之所以IP协议这莫多的辅助协议,是因为单IP协议只是提供了一种无连接的数据传输服务,他不对数据内容做任何加密和校验,错误机制通常只有丢弃域不丢弃两种极端的错误处理方式,只能借助其他协议辅助并由上层决议。IPV4和IPV6都是采用这种尽力而的服务形式。

接下来我们细说IP数据包:

 

字段解释

  IP数据包中分为两大部分,分别为IP头部和IP数据。我们主要介绍长度为20字节的基本IP头部:

  • IP Version:版本字段,IPV4=4IPV6=6
  • Hdr Len:头部长度,指包含32位字(4字节)的数量,一般都是20字节。
  • Tos :服务类型字段,该字段最初只是用来规定一个服务类型,但是由于长期不使用,现在前6位被称为“区分服务字段”+2位“拥塞通知字段”。其中区分服务与Qos相关,定义了8个服务级别。当Qos选择了某种服务模型后,优先级越高,字段越优先传输。一共有D、T、R三种,分别表示延时、吞吐量、可靠性。当这些值都为1时,分别表示低延时、高吞吐量、高可靠性。
  • Total Length:数据包总长度,最大为2^16=65535字节。此字段和头部长度字段共同告知上层协议IP数据的有效部分。这与隧道封装限制有异曲同工之妙。
  • Fragment ID:标识符字段,每发送一次++,初始值为随机值;当IP进行切片时,此字段会被复制到Fragment Offset中。
  • 标志位R:Reserve的缩写,意为保留,暂不使用。
  • 标志位DF:Don't Fragment的缩写,意为没有,表示是“不能分片”。只有当D=0时才允许分片。
  • 标志位MF:More Fragment的缩写,意为更多分片,MF=1即表示后面“还有分片”的数据报。MF=0表示这已是若干数据报片中的最后一个。
  • Fragment Offset:分段偏移。当DF = 0MF =1时,相对于用户数据字段的起点,该片从何处开始。片偏移以8个字节为偏移单位。也就是说,每个分片的长度一定是8字节(64位)的整数倍。
  • TTL:Time To Live,生存时间,表示跳(路由)的最大次数,避免环网时的无限跳转。RFC1122中建议为64,当值为0时,当前路由器将丢弃并原路返回一个ICMP不可达报文。
  • Protocol:数据类型,表示IP数据场中为何种协议数据。1-ICMP17-UDP6-TCP。此字段的出现为IP包的多路分解提供可能性。
  • Checksum:Intel校验和,仅计算IP头部,这意味着IP不对数据场做检查,也体现出IP数据对数据不保证可靠性。
  • Option:可选字段,选项字段用来支持排错、测量以及安全等措施,内容很丰富。,剩余的长度用0填充直至可选长度位数%32(4字节)=0

Intel校验和

  关于Intel校验和介绍本白会从《TCPIP详解卷1》中摘选部分术语并结合一些具体的例子进行演示。在数学上,16位的十六进制集合V = {0001,....,FFFF}与其反码的运算“+”最终得到的结果称之阿贝儿群,因此IP协议中的Checksum字段与其反码的运算+也满足基本规则:

  • 规则1:与自己反码的相加结果一定是0XFFFF,如此,如果IP的Checksum反码与自身相加不为全1,说明校验出错,IPV4机制将丢弃该IP包。
  • 规则2:Checksum值不可能为0x0000

  可以看出,Checlsum的好处就是校验非常快,而这与CAN的CRC异或校验相比则安全性不足。在这里,给出Checksum计算代码供大家使用:

static uint16_t checksum16(uint16_t *buf,uint16_t len)
{
	uint32_t checksum = 0; //初始化
	uint16_t hight;
        checksum=0;
        hight=0;
	while (len>1)
	{
		checksum += *buf++;
		len-= 2;
	}
    
	if (len > 0)
		checksum += *(uint8_t*)buf++;

	//checksum可能出现溢出,因为uint32
	while ((hight = checksum >>16)!=0)
	{
		checksum = hight + (checksum & 0xFFFF);
	}
	return (uint16_t)~checksum;
}

TOS:DS字段与ECN

  沿用上文的解释,TOS为服务类型字段,该字段最初只是用来规定一个服务类型,但是由于长期不使用,现在前6位被称为“区分服务字段”+2位“拥塞通知字段”。在IPV4中此字段处于第二和第三字段。这些字段用于支持RFC2474\RFC2175\RFC3260上的不同类型服务

  ECN(Explicit Congestion Notification)拥塞通知字段,在RFC 3168 (2001) 描述,显式拥塞通知作用于路由器。当一台持续拥塞并且具备拥塞感知能力的路由器在转发分组时会设置这两位。ECN机制的总体思路为:当某一路由器监测拥塞后,设置ECN字段,后续的路由器在接收到此分组时将会减缓此分组的发送速度,即延长此分组的保留时间。

  DS(Difference Service)服务区分字段,在RFC0791中第一次描述其原始结构:

  

Bit0-2=服务优先级,Bit3=D表示延时,Bit4=T表示吞吐量,Bit5=R表示可靠性,后两位不使用(现为ECN)。优先级的取值范围从常规到到网络控制依次递增,数量为2^3=8个服务级别,这种服务基于多级优先与抢占方案。注意ECN和DS没有任何关系,DS是针对Qos服务质量提出的,路由器会针对DTR的等级做不同概率的丢弃和不同优先级的处理。

IPV4数据包输入处理

数据定义

IP的数据定义我们按部就班就行:

#pragma pack(1)
typedef struct _ip_packet
{
	uint8_t hdr_len : 4;                // 首部长, 低四位
	uint8_t version : 4;                // 版本号,高四位
	uint8_t  tos;						//服务字段
	uint16_t totalLen;					//数据包总长度
	uint16_t ID;						//数据包标识
	uint16_t  flag_fragment;			//切片标志+切片偏移
	uint8_t  ttl;						//生存时间
	uint8_t protocol;					//上层协议类型
	uint16_t hdr_checksum;				//校验和
	uint8_t srcip[SIZE_IPV4_ADDR];		//源IP
	uint8_t destip[SIZE_IPV4_ADDR];		//目标IP
}_ip_packet;
#pragma pack()

数据校验

校验一般就是对长度、协议类型、生存时间、校验和进行检查,继续按部就班:

void _IP_input(_eth_packet* packet)
{
	uint32_t  hdr_size, total_size;

	uint16_t pre_checksum;
	printf("%x %x %x %x\n", *packet->dataptr, *(packet->dataptr+1), *(packet->dataptr + 2), *(packet->dataptr + 3));
	_ip_packet* iphdr = (_ip_packet*)packet->dataptr;//指向数据区域
	//printf("%x %x %x %x\n", *packet->dataptr, *((_ip_packet*)packet->dataptr + 1), *((_ip_packet*)packet->dataptr + 2), *((_ip_packet*)packet->dataptr + 3));
	if (iphdr->version!= 4)
	{
		printf("IP协议号 = %x 检查出错!\n", iphdr->version);
		hdr_size = iphdr->hdr_len * 4;
		return;
	}

	hdr_size = iphdr->hdr_len * 4;
	total_size = swap_order16(iphdr->totalLen);
	if ((hdr_size < sizeof(_ip_packet)) || (total_size < hdr_size))
	{
		printf("hdr_size = %d 检查出错!\n", hdr_size);
		printf("total_size = %d 检查出错!\n", total_size);
		printf("_ip_packet_size = %d 检查出错!\n", sizeof(_ip_packet));
		return;
	}
	//校验和计算
	pre_checksum = iphdr->hdr_checksum;
	iphdr->hdr_checksum = 0;
	if (pre_checksum != checksum16((uint16_t*)iphdr, hdr_size, 0, 1))
	{
		printf("checksum 校验和出错!\n");
		return;
	}

	//ip包发送方处理
	if(!IP_is_equal_buf(&_ip_cfg, iphdr->destip))
	{
		printf("接收到不属于自己的IP数据包~ \n");
		return;
	}
	switch (iphdr->protocol)
	{
	case ICMP:
		break;
	case TCPIP:
		break;
	case UDP:
		break;
	}
	printf("_IP_input 通过~ \n");
}

这里可以注意到没有对校验和进行字节序的处理,因为intel校验和顾名思义就是累加校验,和字节序没有关系。除此之外,校验码在传输的时候并不会因字节序的不同改变值,因为网络字节序都是大端字节序,所以不管是服务器还是主机当从网络接收/发送数据包时必须是大端字节序!另外需要提醒的是TTL生存时间在IP报文中已经失去了他的作用,因为现有的路由器持有一个报文的时间不会超过1s,本白看过某主机厂网关的以太网转发定义都是要求内部跳转<0.1s以内。关于TTL的后续变动在下文IPV6中将会提及。

IPV4数据包输出处理

遵循分层封装得原则,IPV4得以太网数据包到达链路层是需要补充MAC/IP/上层协议信息,此时因为主机的IP和MAC都已知,目标的mac可以根据IP在ARP表项中查询得到,所以我们输出时入参只需:

_type_drv_err _IP_output(_type_eth_ptl ptl/*上层协议类型*/, _ip_addr* ipaddr/*目的IP*/, _eth_packet* packet/*以太网包*/)
{
	_ip_header* iphdr;
	static ID_Counter;
	_eth_packet_add_header(packet,sizeof(_ip_header));
	iphdr = (_ip_header*)packet->dataptr;
	iphdr->version = 4;//IPV4
	iphdr->hdr_len = 20/4; //headr size
	iphdr->tos = 0; //暂时不关注
	iphdr->totalLen = swap_order16(packet->size);
	iphdr->ID = swap_order16(ID_Counter); ID_Counter++;
	iphdr->flag_fragment = 0;
	iphdr->ttl = 64; //最大255
	iphdr->protocol = IP_PROTOCOL;//暂时不定义  1-icmp  17-udp  6-tcp
	memcpy(iphdr->srcip, &_ip_cfg.array, SIZE_IPV4_ADDR);
	memcpy(iphdr->destip, &_vrip_cfg.array,SIZE_IPV4_ADDR);
	iphdr->hdr_checksum = 0;
	iphdr->hdr_checksum = checksum16((uint16_t*)iphdr,sizeof(_ip_header),0,1);
	
	return _ethernet_out(&_vrip_cfg,packet);//_ethernet_out中会添加目标MAC,源MAC等信息到包头,大家明白这个流程就足够了
}


//_ethernet_out中函数的关键API
static _type_drv_err _ethernet_send(_type_eth_ptl protocol_type/*协议类型*/, const* mac_addr/*目标mac地址*/, _eth_packet* packet/*发送的包*/)
{
	_ethII_packet* eth_header;//mac地址是llc层的包中数据
	_eth_packet_add_header(packet, sizeof(_ethII_packet));
	//printf("packet point size = %d",sizeof(packet));
	eth_header = (_ethII_packet*)packet->dataptr;
	memcpy(eth_header->dest_MAC,mac_addr, SIZE_ETHII_MAC);//目标主机的mac地址
	memcpy(eth_header->source_MAC, _mac_cfg, SIZE_ETHII_MAC);//源网卡的mac地址
	eth_header->Type = swap_order16(protocol_type);

	return _eth_drive_send(packet);//通过驱动发送包
}

到此为止,关于IP包的接收发送已经完成,后续章节将会利用此接口分别进行ICMP/UDP/TCP的报文发送啦。

关于IP协议的简单思考

  IP协议的最基本知识起始就这莫多,关于IPV6\路由\切片\Qos服务质量等概念大家可以进一步深化学习。对于入门者来说,我们知道IPV4各字段的基本概念就好啦,有兴趣的可以去看看《TCPIP详解卷1》中对于隧道封装、路由、快速启动等概念的解释,这可能会帮助你对于IP协议的理解更上一层楼。

  就本白目前学习了解到的知识,IP协议的最大特点首先是数据传输是否可达的尽力而为,这种尽力而为只是体现在数据尽可能到达,而为了提高可达性,分别有TLL、DS、分片、IPV6路由(RH2)等字段信息共同维护,但是对于IP数据的内容不做任何保护和检查。那如何确保数据一定可达并且尽可能的准确呢?IP协议的辅助机制ICMP(故障探测与配置)和TCP(可靠性)将会对该部分做出补充。

posted @ 2023-02-25 21:38  张一默  阅读(97)  评论(0编辑  收藏  举报