一、概述
ICMP(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议簇的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。
ICMP协议是一种面向无连接的协议,用于传输出错报告控制信息。它是一个非常重要的协议,它对于网络安全具有极其重要的意义。ICMP就是一个“错误侦测与回报机制”,其目的就是让我们能够检测网路的连线状况﹐也能确保连线的准确性。当路由器在处理一个数据包的过程中发生了意外,可以通过ICMP向数据包的源端报告有关事件。
其功能主要有,侦测远端主机是否存在,建立及维护路由资料,重导资料传送路径(ICMP重定向),资料流量控制。ICMP在沟通之中,主要是透过不同的类别(Type)与代码(Code) 让机器来识别不同的连线状况。
使用ping命令可以检测指定设备的在线状态(有些主机会屏蔽ping命令),但在写程序的时候一般不是调用cmd命令去直接ping主机,而是通过socket想目标机器发送ICMP请求包,然后等待返回结果。
二、实现
1.定义ICMP数据包结构
ICMP数据包包含在IP数据包结构内,为了在程序中对ICMP数据包做处理,我们使用结构体来描述IP数据包和ICMP数据包:
ICMP首部TYPE和CODE对应表:
TYPE | CODE | Description |
---|---|---|
0 | 0 | Echo Reply——回显应答(Ping应答) |
3 | 0 | Network Unreachable——网络不可达 |
3 | 1 | Host Unreachable——主机不可达 |
3 | 2 | Protocol Unreachable——协议不可达 |
3 | 3 | Port Unreachable——端口不可达 |
3 | 4 | Fragmentation needed but no frag. bit set——需要进行分片但设置不分片比特 |
3 | 5 | Source routing failed——源站选路失败 |
3 | 6 | Destination network unknown——目的网络未知 |
3 | 7 | Destination host unknown——目的主机未知 |
3 | 8 | Source host isolated (obsolete)——源主机被隔离(作废不用) |
3 | 9 | Destination network administratively prohibited——目的网络被强制禁止 |
3 | 10 | Destination host administratively prohibited——目的主机被强制禁止 |
3 | 11 | Network unreachable for TOS——由于服务类型TOS,网络不可达 |
3 | 12 | Host unreachable for TOS——由于服务类型TOS,主机不可达 |
3 | 13 | Communication administratively prohibited by filtering——由于过滤,通信被强制禁止 |
3 | 14 | Host precedence violation——主机越权 |
3 | 15 | Precedence cutoff in effect——优先中止生效 |
4 | 0 | Source quench——源端被关闭(基本流控制) |
5 | 0 | Redirect for network——对网络重定向 |
5 | 1 | Redirect for host——对主机重定向 |
5 | 2 | Redirect for TOS and network——对服务类型和网络重定向 |
5 | 3 | Redirect for TOS and host——对服务类型和主机重定向 |
8 | 0 | Echo request——回显请求(Ping请求) |
9 | 0 | Router advertisement——路由器通告 |
10 | 0 | Route solicitation——路由器请求 |
11 | 0 | TTL equals 0 during transit——传输期间生存时间为0 |
11 | 1 | TTL equals 0 during reassembly——在数据报组装期间生存时间为0 |
12 | 0 | IP header bad (catchall error)——坏的IP首部(包括各种差错) |
12 | 1 | Required options missing——缺少必需的选项 |
13 | 0 | Timestamp request (obsolete)——时间戳请求(作废不用) |
14 | Timestamp reply (obsolete)——时间戳应答(作废不用) | |
15 | 0 | Information request (obsolete)——信息请求(作废不用) |
16 | 0 | Information reply (obsolete)——信息应答(作废不用) |
17 | 0 | Address mask request——地址掩码请求 |
18 | 0 | Address mask reply——地址掩码应答 |
IP结构体:
typedef struct IPHDR{
unsigned int h_len:4; //包头长度
unsigned int version:4;//版本号
unsigned char tos://服务类型
unsigned short total_len;//包总长度
unsigned short ident;//唯一标识符
unsigned short frag_and_flages;//标识
unsigned char ttl;//生存时间
unsigned char proto;//传输协议
unsigned short checksum;//校验和
unsigned int souceIP;//源ip
unsigned int destIP;//目标ip
}IpHeader;
ICMP结构体:
typedef struct ICMPHDR{
BYTE i_type;//类型
BYTE i_code;//编码
USHORT i_cksum;//校验和
USHORT i_id;//编号
USHORT i_seq;//序号
ULONG timestamp;//时间戳
}IcmpHeader;
2.代码实现片段
填充ICMP包
/*
即初始化,先将头部字段进行赋值,再对数据部分进行填充,用'E'来填充,不要问我为什么,不求甚解,哈哈
#define ICMP_MIN 8 // ICMP包的最小长度为8个字节,只包含包头
#define DEF_PACKET_SIZE 32 // 执行ping操作时指定发送数据包的缺省大小
#define MAX_PACKET 1024 // 执行ping操作时指定发送数据包的最大大小
#define ICMP_ECHO 8 // 表示ICMP包为回射请求包
#define ICMP_ECHOREPLY 0 // 表示ICMP包为回射应答包
*/
void fullICMP(char *icmp_data,int datasize){
IcmpHeader *icmpHdr;
char *datapart;
icmpHdr = (IcmpHeader*)icmp_data;
memset(icmpHdr,0,datasize);
icmp_hdr->i_type = ICMP_ECHO;
icmp_hdr->i_id = (SHORT)GetCurrentThread();
icmp_hdr->i_code = 0;
icmp_hdr->i_seq = 0;
icmp_hdr->i_sum = 0;//校验和先设置为0
datapart = icmp_data + sizeof(IcmpHeader);
memset(datapart,'E',datasize - sizeof(IcmpHeader));
}
解析ICMp回应包
int decodeIcmpReply(char *buf,int bytes,DWORD tid){
IpHeader *iphdr; // IP数据包头
IcmpHeader *icmphdr; // ICMP包头
unsigned short iphdrlen; // IP数据包头的长度
iphdr = (IpHeader *)buf; // 从buf中IP数据包头的指针
// 计算IP数据包头的长度
iphdrlen = iphdr->h_len * 4 ; // number of 32-bit words *4 = bytes
// 如果指定的缓冲区长度小于IP包头加上最小的ICMP包长度,则说明它包含的ICMP数据不完整,或者不包含ICMP数据
if (bytes < iphdrlen + ICMP_MIN) {
return -1;
}
// 定位到ICMP包头的起始位置
icmphdr = (IcmpHeader*)(buf + iphdrlen);
// 如果ICMP包的类型不是回应包,则不处理
if (icmphdr->i_type != ICMP_ECHOREPLY) {
return -2;
}
// 发送的ICMP包ID和接收到的ICMP包ID应该对应
if (icmphdr->i_id != (USHORT)tid){ //(USHORT)GetCurrentProcessId()) {
return -3;
}
// 返回发送ICMP包和接收回应包的时间差
int time = GetTickCount() - (icmphdr->timestamp);
if(time >= 0)
return time;
else
return -4; // 时间值不对
}
ping函数实现
int ping(const char *ip, DWORD timeout)
{
WSADATA wsaData; // 初始化Windows Socket的数据
SOCKET sockRaw = NULL; // 用于执行ping操作的套接字
struct sockaddr_in dest,from; // socket通信的地址
struct hostent * hp; // 保存主机信息
int datasize; // 发送数据包的大小
char *dest_ip; // 目的地址
char *icmp_data = NULL; // 用来保存ICMP包的数据
char *recvbuf = NULL; // 用来保存应答数据
USHORT seq_no = 0;
int ret = -1;
// 初始化SOCKET
if (WSAStartup(MAKEWORD(2,1),&wsaData) != 0){
ret = -1000;// WSAStartup 错误
goto FIN;
}
// 创建原始套接字
sockRaw = WSASocket (AF_INET,SOCK_RAW,IPPROTO_ICMP,NULL, 0,WSA_FLAG_OVERLAPPED);
if (sockRaw == INVALID_SOCKET) {
ret = -2;// WSASocket 错误
goto FIN;
}
// 设置套接字的接收超时选项
int bread = setsockopt(sockRaw,SOL_SOCKET,SO_RCVTIMEO,(char*)&timeout,sizeof(timeout));
if(bread == SOCKET_ERROR) {
ret = -3;// setsockopt 错误
goto FIN;
}
// 设置套接字的发送超时选项
bread = setsockopt(sockRaw,SOL_SOCKET,SO_SNDTIMEO,(char*)&timeout,
sizeof(timeout));
if(bread == SOCKET_ERROR) {
ret = -4;// setsockopt 错误
goto FIN;
}
memset(&dest,0,sizeof(dest));
unsigned int addr=0; // 将IP地址转换为网络字节序
hp = gethostbyname(ip); // 获取远程主机的名称
if (!hp){
addr = inet_addr(ip);
}
if ((!hp) && (addr == INADDR_NONE) ) {
ret = -5; // 域名错误
goto FIN;
}
// 配置远程通信地址
if (hp != NULL)
memcpy(&(dest.sin_addr),hp->h_addr,hp->h_length);
else
dest.sin_addr.s_addr = addr;
if (hp)
dest.sin_family = hp->h_addrtype;
else
dest.sin_family = AF_INET;
dest_ip = inet_ntoa(dest.sin_addr);
// 准备要发送的数据
datasize = DEF_PACKET_SIZE;
datasize += sizeof(IcmpHeader);
char icmp_dataStack[MAX_PACKET];
char recvbufStack[MAX_PACKET];
icmp_data = icmp_dataStack;
recvbuf = recvbufStack;
// 未能分配到足够的空间
if (!icmp_data) {
ret = -6; //
goto FIN;
}
memset(icmp_data,0,MAX_PACKET);
// 准备要发送的数据
fill_icmp_data(icmp_data,datasize); // 设置报文头
((IcmpHeader*)icmp_data)->i_cksum = 0;
DWORD startTime = GetTickCount();
((IcmpHeader*)icmp_data)->timestamp = startTime;
((IcmpHeader*)icmp_data)->i_seq = seq_no++;
((IcmpHeader*)icmp_data)->i_cksum = checksum((USHORT*)icmp_data,datasize);
// 发送数据
int bwrote;
bwrote = sendto(sockRaw,icmp_data,datasize,0,(struct sockaddr*)&dest,
sizeof(dest));
if (bwrote == SOCKET_ERROR){
if (WSAGetLastError() != WSAETIMEDOUT)
{
ret = -7; // 发送错误
goto FIN;
}
}
if (bwrote < datasize ) {
ret = -8; // 发送错误
goto FIN;
}
// 使用QueryPerformance函数用于精确判断结果返回时间值
// 原有的其他的Windows函数(GetTickCount等)的方式返回值与Windows Ping应用程序相差太大。
LARGE_INTEGER ticksPerSecond;
LARGE_INTEGER start_tick;
LARGE_INTEGER end_tick;
double elapsed; // 经过的时间
QueryPerformanceFrequency(&ticksPerSecond); // CPU 每秒跑几个tick
QueryPerformanceCounter(&start_tick); // 开始时系统计数器的位置
int fromlen = sizeof(from); // 源地址的大小
while(1)
{
// 接收回应包
bread = recvfrom(sockRaw,recvbuf,MAX_PACKET,0,(struct sockaddr*)&from, &fromlen);
if (bread == SOCKET_ERROR){
if (WSAGetLastError() == WSAETIMEDOUT) {
ret = -1; // 超时
goto FIN;
}
ret = -9; // 接收错误
goto FIN;
}
// 对回应的IP数据包进行解析,定位ICMP数据
int time = decode_resp(recvbuf,bread,&from,GetCurrentThreadId());
if( time >= 0 ) {
//ret = time;
QueryPerformanceCounter(&end_tick); // 获取结束时系统计数器的值
elapsed = ((double)(end_tick.QuadPart - start_tick.QuadPart) / ticksPerSecond.QuadPart); // 计算ping操作的用时
ret = (int)(elapsed*1000);
goto FIN;
} else if(GetTickCount() - startTime >= timeout || GetTickCount() < startTime){
ret = -1; // 超时
goto FIN;
}
}
FIN:
// 释放资源
closesocket(sockRaw);
WSACleanup();
// 返回ping操作用时或者错误编号
return ret;
}
以下是自己写的ping指端网段的代码,欢迎讨论并指出不足之处:
ping命令实现