目錄
ICMP協議介紹
Ping程序實現原理
代碼部分
測試結果
ICMP協議介紹
ICMP是internet控制報文協議,它是TCP/IP協議族的一個子協議,用於IP主機、路由器之間傳遞控制消息。ICMP協議是一種面向無連接的協議,用於傳輸出錯報告控制信息。它是一個非常重要的協議,它對於網絡安全具有極其重要的意義。從技術角度說,ICMP就是一個“錯誤偵測與回報機制”,其目的就是讓我們能夠檢測網絡的連接狀況,也能確保連線的準確性,其功能主要有:
· 偵測遠端主機是否存在。
· 建立及維護路由資料。
· 重導資料傳送路徑(ICMP重定向)。
· 資料流量控制。
ICMP 是個非常有用的協議﹐尤其是當我們要對網路連接狀況進行判斷的時候。
它傳遞差錯報文以及其他需要注意的信息,經常供IP層或更高層協議(TCP或UDP)使用。所以它經常被認爲是IP層的一個組成部分。它在IP數據報文中的封裝如下:
ICMP的數據報文格式如下所示。所有報文的前4個字節都是一樣的,其他的因報文類型不同而不一樣。類型字段可以有15個不同的值,用以描述不同的ICMP報文。校驗和字段覆蓋整個ICMP報文,使用了和IP首部檢驗和一樣的算法,詳細請搜索TCP/IP檢驗和算法。
不同類型的報文是由類型字段和代碼字段來共同決定。下表是各種類型的ICMP報文。
根據上表可知,ICMP協議大致分爲兩類,一種是查詢報文,一種是差錯報文。查詢報文是用一對請求和應答定義的,它通常有以下幾種用途:
- ping查詢
- 子網掩碼查詢(用於無盤工作站在初始化自身的時候初始化子網掩碼)
- 時間戳查詢(可以用來同步時間)
而差錯報文通常包含了引起錯誤的IP數據報的第一個分片的IP首部(和選項),加上該分片數據部分的前8個字節。RFC 792規範中定義的這8個字節中包含了該分組運輸層首部的所有分用信息,這樣運輸層協議就可以向正確的進程提交ICMP差錯報文。
當傳送IP數據包發生錯誤時,比如主機不可達,端口不可達等,ICMP協議就會把錯誤信息封包,然後傳送回給主機。給主機一個處理錯誤的機會,這也就是爲什麼說建立在IP層以上的協議是可能做到安全的原因。由上面可知,ICMP數據包由8bit的錯誤類型和8bit的代碼和16bit的校驗和組成,而前 16bit就組成了ICMP所要傳遞的信息。由數據鏈路層所能發送的最大數據幀,即MTU(Maximum Transmission Unit)爲1500,計算易知ICMP協議在實際傳輸中數據包爲:20字節IP首部 + 8字節ICMP首部+ 1472字節(數據大小)。
儘管在大多數情況下,錯誤的包傳送應該給出ICMP報文,但是在特殊情況下,是不產生ICMP錯誤報文的。如下
- ICMP差錯報文不會產生ICMP差錯報文(出IMCP查詢報文)(防止IMCP的無限產生和傳送)
- 目的地址是廣播地址或多播地址的IP數據報。
- 作爲鏈路層廣播的數據報。
- 不是IP分片的第一片。
- 源地址不是單個主機的數據報。這就是說,源地址不能爲零地址、環回地址、廣播地 址或多播地址。
Ping程序實現原理
源主機向網絡上的另一個主機系統發送ICMP報文,如果指定系統得到了報文,它將把報文一模一樣地傳回給發送者,如此便能探測目標主機是否在線。一般使用類型爲0和8的ICMP報文。
在此應該瞭解一下IP頭部生存時間(time to live)TTL字段。程序實現時是向目地主機發送一個ICMP回顯請求消息,初始時TTL爲固定值,這樣當該數據報抵達途中的第一個路由器時,TTL的值就被減1,再TTL的值不爲零時,數據包繼續往下傳送數據包,直至TTL的值爲0返回ICMP超時差錯報文,或者到達目的主機返回一模一樣的報文。
代碼部分
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
//IP報頭
typedef struct
{
unsigned char hdr_len:4; //4位頭部長度
unsigned char version:4; //4位版本號
unsigned char tos; //8位服務類型
unsigned short total_len; //16位總長度
unsigned short identifier; //16位標識符
unsigned short frag_and_flags; //3位標誌加13位片偏移
unsigned char ttl; //8位生存時間
unsigned char protocol; //8位上層協議號
unsigned short checksum; //16位效驗和
unsigned long sourceIP; //32位源IP地址
unsigned long destIP; //32位目的IP地址
}IP_HEADER;
//ICMP報頭
typedef struct
{
BYTE type; //8位類型字段
BYTE code; //8位代碼字段
USHORT cksum; //16位效驗和
USHORT id; //16位標識符
USHORT seq; //16位序列號
}ICMP_HEADER;
//報文解碼結構
typedef struct
{
USHORT usSeqNo; //序列號
DWORD dwRoundTripTime; //返回時間
in_addr dwIPaddr; //返回報文的IP地址
}DECODE_RESULT;
//計算網際效驗和函數
USHORT checksum(USHORT *pBuf,int iSize)
{
unsigned long cksum=0;
while(iSize>1)
{
cksum+=*pBuf++;
iSize-=sizeof(USHORT);
}
if(iSize)
{
cksum+=*(USHORT*)pBuf;
}
cksum=(cksum>>16)+(cksum&0xffff);
cksum+=(cksum>>16);
return(USHORT)(~cksum);
}
//對數據包進行解碼
BOOL DecodeIcmpResponse(char *pBuf,int iPacketSize,DECODE_RESULT &DecodeResult,BYTE ICMP_ECHO_REPLY,BYTE ICMP_TIMEOUT)
{
//檢查數據報大小的合法性
IP_HEADER *pIpHdr=(IP_HEADER*)pBuf;
int iIpHdrLen=pIpHdr->hdr_len*4;
if(iPacketSize<(int)(iIpHdrLen+sizeof(ICMP_HEADER)))
return FALSE;
//根據ICMP報文類型提取ID字段和序列號字段
ICMP_HEADER *pIcmpHdr=(ICMP_HEADER*)(pBuf+iIpHdrLen);
USHORT usID,usSquNo;
if(pIcmpHdr->type==ICMP_ECHO_REPLY) //ICMP回顯應答報文
{
usID=pIcmpHdr->id; //報文ID
usSquNo=pIcmpHdr->seq; //報文序列號
}
else if(pIcmpHdr->type==ICMP_TIMEOUT) //ICMP超時差錯報文
{
char *pInnerIpHdr=pBuf+iIpHdrLen+sizeof(ICMP_HEADER); //載荷中的IP頭
int iInnerIPHdrLen=((IP_HEADER*)pInnerIpHdr)->hdr_len*4; //載荷中的IP頭長
ICMP_HEADER *pInnerIcmpHdr=(ICMP_HEADER*)(pInnerIpHdr+iInnerIPHdrLen);//載荷中的ICMP頭
usID=pInnerIcmpHdr->id; //報文ID
usSquNo=pInnerIcmpHdr->seq; //序列號
}else{
return false;
}
//檢查ID和序列號以確定收到期待數據報
if(usID!=(USHORT)GetCurrentProcessId()||usSquNo!=DecodeResult.usSeqNo)
{
return false;
}
//記錄IP地址並計算往返時間
DecodeResult.dwIPaddr.s_addr=pIpHdr->sourceIP;
DecodeResult.dwRoundTripTime=GetTickCount()-DecodeResult.dwRoundTripTime;
//處理正確收到的ICMP數據報
if(pIcmpHdr->type==ICMP_ECHO_REPLY||pIcmpHdr->type==ICMP_TIMEOUT)
{
return true;
}else{
return false;
}
return true;
}
int main()
{
//初始化Windows sockets網絡環境
WSADATA wsa;
WSAStartup(MAKEWORD(2,2),&wsa);
char IpAddress[255];
cout<<"請輸入一個IP地址或域名:";
cin>>IpAddress;
//得到IP地址
u_long ulDestIP=inet_addr(IpAddress);
//轉換不成功時按域名解析
if(ulDestIP==INADDR_NONE)
{
hostent *pHostent=gethostbyname(IpAddress);
if(pHostent)
{
ulDestIP=(*(in_addr*)pHostent->h_addr).s_addr;
}
else
{
cout<<"輸入的IP地址或域名無效"<<endl;
WSACleanup();
return 0;
}
}
cout<<"正在 Ping "<<IpAddress<<"具有 32 字節的數據:"<<endl<<endl;
//填充目的端socket地址
sockaddr_in destSockAddr;
ZeroMemory(&destSockAddr,sizeof(sockaddr_in));
destSockAddr.sin_family=AF_INET;
destSockAddr.sin_addr.s_addr=ulDestIP;
//創建原始套接字
SOCKET sockRaw=WSASocket(AF_INET,SOCK_RAW,IPPROTO_ICMP,NULL,0,WSA_FLAG_OVERLAPPED);
//超時時間
int iTimeout=3000;
//接收超時
setsockopt(sockRaw,SOL_SOCKET,SO_RCVTIMEO,(char*)&iTimeout,sizeof(iTimeout));
//發送超時
setsockopt(sockRaw,SOL_SOCKET,SO_SNDTIMEO,(char*)&iTimeout,sizeof(iTimeout));
//構造ICMP回顯請求消息,並以TTL遞增的順序發送報文
//ICMP類型字段
const BYTE ICMP_ECHO_REQUEST=8; //請求回顯
const BYTE ICMP_ECHO_REPLY=0; //回顯應答
const BYTE ICMP_TIMEOUT=11; //傳輸超時
//其他常量定義
const int DEF_ICMP_DATA_SIZE=32; //ICMP報文默認數據字段長度
const int MAX_ICMP_PACKET_SIZE=1024; //ICMP報文最大長度(包括報頭)
const DWORD DEF_ICMP_TIMEOUT=3000; //回顯應答超時時間
const int DEF_MAX_HOP=4; //最大跳站數
//填充ICMP報文中每次發送時不變的字段
char IcmpSendBuf[sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE]; //發送緩衝區
memset(IcmpSendBuf,0,sizeof(IcmpSendBuf)); //初始化發送緩衝區
char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE]; //接收緩衝區
memset(IcmpRecvBuf,0,sizeof(IcmpRecvBuf)); //初始化接收緩衝區
ICMP_HEADER *pIcmpHeader=(ICMP_HEADER*)IcmpSendBuf;
pIcmpHeader->type=ICMP_ECHO_REQUEST; //類型爲請求回顯
pIcmpHeader->code=0; //代碼字段爲0
pIcmpHeader->id=(USHORT)GetCurrentProcessId(); //ID字段爲當前進程號
memset(IcmpSendBuf+sizeof(ICMP_HEADER),'E',DEF_ICMP_DATA_SIZE); //數據字段
USHORT usSeqNo=0; //ICMP報文序列號
int iTTL=64; //TTL初始值爲1
int r=0,o=0;
BOOL bReachDestHost=FALSE; //循環退出標誌
int iMaxHot=DEF_MAX_HOP; //循環的最大次數
DECODE_RESULT DecodeResult; //傳遞給報文解碼函數的結構化參數
while(!bReachDestHost&&iMaxHot--)
{
//設置IP報頭的TTL字段
setsockopt(sockRaw,IPPROTO_IP,IP_TTL,(char*)&iTTL,sizeof(iTTL));
//填充ICMP報文中每次發送變化的字段
((ICMP_HEADER*)IcmpSendBuf)->cksum=0; //效驗和先置爲0
((ICMP_HEADER*)IcmpSendBuf)->seq=htons(usSeqNo++); //填充序列號
((ICMP_HEADER*)IcmpSendBuf)->cksum=checksum((USHORT*)IcmpSendBuf,sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE); //計算效驗和
//記錄序列號和當前時間
DecodeResult.usSeqNo=((ICMP_HEADER*)IcmpSendBuf)->seq; //當前序號
DecodeResult.dwRoundTripTime=GetTickCount(); //當前時間
//發送TCP回顯請求信息
sendto(sockRaw,IcmpSendBuf,sizeof(IcmpSendBuf),0,(sockaddr*)&destSockAddr,sizeof(destSockAddr));
//接收ICMP差錯報文並進行解析處理
sockaddr_in from; //對端socket地址
int iFromLen=sizeof(from); //地址結構大小
int iReadDataLen; //接收數據長度
while(1)
{
//接收數據
iReadDataLen=recvfrom(sockRaw,IcmpRecvBuf,MAX_ICMP_PACKET_SIZE,0,(sockaddr*)&from,&iFromLen);
if(iReadDataLen!=SOCKET_ERROR)//有數據達到
{
//對數據包進行解碼
if(DecodeIcmpResponse(IcmpRecvBuf,iReadDataLen,DecodeResult,ICMP_ECHO_REPLY,ICMP_TIMEOUT))
{
//到達目的地,退出循環
if(DecodeResult.dwIPaddr.s_addr==destSockAddr.sin_addr.s_addr){
//輸出IP地址
if(DecodeResult.dwRoundTripTime)
cout<<"來自 "<<inet_ntoa(DecodeResult.dwIPaddr)<<" 的回覆:字節32 時間="<<DecodeResult.dwRoundTripTime<<"ms TTL:"<<iTTL<<endl;
else
cout<<"來自 "<<inet_ntoa(DecodeResult.dwIPaddr)<<" 的回覆:字節32 時間< 1ms TTL:"<<iTTL<<endl;
r++;
break;
}
}
}
else if(WSAGetLastError()==WSAETIMEDOUT) //接收超時,輸出星號
{
cout<<"請求超時。"<<endl;
o++;
break;
}
else{
break;
}
}
}
cout<<endl;
cout<<IpAddress<<" 的 Ping 統計信息:"<<endl;
cout<<'\t'<<"數據包:已發送=4,已接收="<<r<<",丟失="<<o<<endl;
}
測試結果