Fork me on GitHub

从1写TCPIP协议栈6:ICMP协议及Ping响应实现

引言

  承接第五小节的随笔,ICMP是IP协议的辅助与补充协议,它挂载在IP数据包中进行传输,因此ICMP协议同IP协议一样严格来说介于网络层和传输层之间。ICMP(Intel control message protocol)控制报文协议只负责传递差错信息。细品一下就能明白ICMP也不会为IP协议提供可靠性,只会为IP协议提供错误检查。本篇本白只介绍ICMP-IPV4~

ICMP协议简介

ICMP协议是RFC792提出并逐渐作为标准被大众所接收,ICMP包的结构非常简单~

字段解释

  • Type类型:在ICMPV4中,类型字段一共保留了42个不同的类型的值。但是一般只有8个是常用的,比如本次练习所使用的8代表Echo-Request报文。
  • Code代码:基于Type类型的描述进一步给出报文的具体类别,也就是更加具体的差错信息。
  • Checksum校验:采用Intel校验,作用整个ICMP报头。

Code与Type字段

如上所述,Code和Type共同描述差错信息,类型和代码字段参考链接:ICMP报文分类。

ICMP-IPV4差错报文

ICMP的差错报文根据报文类型共有五种:

报文类型 名称 原因
3 目的不可达 不可达的主机/协议
4 源端抑制 CP拥塞/UDP弃用
5 重定向 发回主机重新发给其他路由器
11 超时 TTL耗尽
12 参数问题 数据包有错误

再提醒一点,学习过CAN的小伙伴了解这样一种规则,CAN通讯时的错误帧爆出针对一些特殊情况做了避免,比如仲裁期间不产生错误帧,对总线上其他节点发送的错误帧不产生错误帧。该设计和ICMP差错报文的设计有一点共通之处,那就是针对自己的差错不会差错报文,这避免了广播风暴,即不会进入差错的差错的差错....这种死循环。当然,还有一些其他情况,总结下来就是:

  • 差错报文。
  • IP分片报文。
  • 目的地址为广播或组播的数据包。
  • 源地址是特殊地址 , 比如0地址,环回地址,广播地址,组播地址。不发送 ICMP 差错报文。

Ping响应实现

实现说明

本次练习我们只实现Ping的响应用于学习了解ICMP的基本机制,为什莫呢?因为Ping的请求中包括了两个特殊字段:

ID和Seq是为了区分不同的Ping进程所添加的辅助识别信息,每一次ping中会发送四次或多次ping报文(Seq++),每一个ping请求和ping响应的ID与Seq一致,但是ID一般来说跟随系统是固定的。本白为了偷懒,本次练习不做ping请求的实现~如果想更详细的了解ICMP相关的内容,推荐《用TCP/IP进行网际互联:原理、协议与结构(第一卷 第四版)》,在第20章讲述了具体的实现。

代码实现

首先定义一个ICMP结构体:

typedef struct _icmp_packet
{
	uint8_t type;//报文类型
	uint8_t code;//差错代码号
	uint16_t checksum;
	uint16_t ID;//ICMP-ID
	uint16_t Seq;//ICMP-Seq
}_icmp_packet;

ICMP既然挂载在IP数据中,那只需要针对IP数据包做一次分类就好:

switch (iphdr->protocol)
	{
	case ICMP:
		_eth_packet_del_header(packet, sizeof(_ip_header));
		_icmp_check_in(&Srcip, packet);
		break;
	case TCP:
		break;
	}

 根据IP协议的协议类型字段进行ICMP的判断,然后提出IP头,剩下的就是ICMP的数据了,根据拿到的数据我们可以提取相关信息存入发送的ICMP包中,重新组IP包发送时可以根据接收到的IP包发送方IP地址在ARP表项中查询对应MAC:

//ICMP响应
int _icmp_make_response(_icmp_packet* icmphdr, _ip_addr* srcip, _eth_packet* packet)
{

	//配置发送区大小,准备发送
	_eth_packet* tx = _eth_packet_tx(packet->size);//此时packet只有icmp报文了
	_icmp_packet* reply_hdr = (_icmp_packet*)tx->dataptr;
	reply_hdr->code = 0;
	reply_hdr->ID = icmphdr->ID;//应该和请求ID一致
	reply_hdr->Seq = icmphdr->Seq;//应该和请求Seq一致
	reply_hdr->checksum = 0;
	reply_hdr->type = 0;
	//做好的icmp数据复制到定义的reply中去
	memcpy((uint8_t*)reply_hdr+sizeof(_icmp_packet), ((uint8_t*)icmphdr) +sizeof(_icmp_packet),
		packet->size- sizeof(_icmp_packet));
	
	reply_hdr->checksum = checksum16((uint16_t*)reply_hdr, tx->size,0,1);
	printf("ICMP : %x %x %x %x %x\n", *packet->dataptr, *(packet->dataptr + 1), *(packet->dataptr + 2), *(packet->dataptr + 3), *(packet->dataptr + 4));
	return _IP_output(1, srcip, tx);
}
//IP包组装发送
_type_drv_err _IP_output(_type_eth_ptl ptl/*上层协议类型*/, _ip_addr* ipaddr/*目的IP*/, _eth_packet* packet/*以太网包*/)
{
	_ip_header* iphdr;
	static uint32_t ID_Counter=0;
	_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 = ptl;//暂时不定义  1-icmp  17-udp  6-tcp
	memcpy(iphdr->srcip, _ip_cfg.array, SIZE_IPV4_ADDR);
	memcpy(iphdr->destip, &ipaddr->array,SIZE_IPV4_ADDR);
	iphdr->hdr_checksum = 0;
	iphdr->hdr_checksum = checksum16((uint16_t*)iphdr,sizeof(_ip_header),0,1);
	return _ethernet_out(ipaddr,packet);
}

static _type_drv_err _ethernet_out(_ip_addr* destip, _eth_packet* packet)
{
	_type_drv_err err;
	uint8_t* macaddr;
	printf("_ethernet_out");
	if ((DRIVE_ERR_OK == _arp_convert(destip, &macaddr)))//ARP表项查询
	{
		return _ethernet_send(TCP_PROTOCOL, macaddr, packet);
	}
	return DRIVE_ERR_IO;
}

ICMP目的不可达报文实现

除了正常的响应报文,跟着大佬本白再实现一个差错报文-目的不可达,目的不可达主要有以下几种错误代码:

我们挑一个端口不可达,端口不可达同理。另外差错报文我们只需要注意其Seq与ID就是Unused-00000000就可以了:

_type_drv_err _icmp_dest_unreach(uint8_t code, _ip_header* ipheader)
{
	_icmp_packet* icmphdr;
	_eth_packet* packet;
	_ip_addr dest_ip;

	uint16_t ip_header_size = ipheader->hdr_len * 4;
	uint16_t ip_data_size = swap_order16(ipheader->totalLen) - ip_header_size;

	packet = _eth_packet_tx(sizeof(_icmp_packet)+ ip_header_size+ ip_data_size+8/*rfc指明最小为头部长度+8字节*/);//icmp包头+ip头+数据部分

	icmphdr = (_icmp_packet*)packet->dataptr;
	icmphdr->code = code;
	icmphdr->ID = 0;//不可达报文为00
	icmphdr->Seq = 0;//不可达报文为00
	icmphdr->checksum = 0;
	icmphdr->type = XICMP_TYPE;
	memcpy((uint8_t*)icmphdr +sizeof(_icmp_packet),ipheader, ip_data_size);

	icmphdr->checksum = checksum16((uint16_t*)icmphdr, packet->size, 0, 1);
	printf("unreach ICMP : %x %x %x %x %x\n", *packet->dataptr, *(packet->dataptr + 1), *(packet->dataptr + 2), *(packet->dataptr + 3), *(packet->dataptr + 4));

	SRC_IP_extract(&dest_ip, ipheader->srcip);
	return _IP_output(1, ipheader->srcip, packet);
}

关于ICMP报文的思考

ICMP报文仍然是挂载在IP数据包中的,因此是IP协议的子协议。ICMP的报文大致分为差错报文和信息报文,差错报文一般要求不会回复差错报文来防止广播泛洪,但我们仍需要IP报文不分片的情况下使用拓展字段让ICMP报文尽可能多的包含差错信息来帮助主机检查错误。即对于IP数据保障,扩展字段在ICMPv4与v6之间的位置相同(填充位实现兼容性)。ICMP提供了差错机制,最核心的差错便是回显与请求。另外,简单看了下ICMPv6,ICMPv6增加了邻居发现功能,也可以借用IGMP进行组播成员的资格管理,这些机制后续等练习做完也会专门写一篇随笔来对这部分做介绍。

希望计划不要延迟吧~

posted @ 2023-03-05 22:11  张一默  阅读(85)  评论(0编辑  收藏  举报