一、概述
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命令實現