1. 程式人生 > >ICMP的應用之路由追蹤程式(Tracert)

ICMP的應用之路由追蹤程式(Tracert)

實現原理:

Tracert 程式關鍵是對 IP 頭部生存時間(time to live)TTL 欄位的使用,程式實現是向目的主機發送一個 ICMP 回顯請求報文,初始時 TTL 等於 1 ,這樣當該資料報抵達途中的第一個路由器時,TTL 的值就被減為 0,導致傳送超時錯誤,因此該路由生成一份 ICMP 超時差錯報文返回給源主機。隨後,主機將資料報的 TTL 值遞增 1 ,以便 IP 報能傳送到下一個路由器,並由下一個路由器生成 ICMP 超時差錯報文返回給源主機。不斷重複這個過程,直到資料報達到目的主機或超過跳數限制,到達目的主機後,目的主機返回 ICMP 回顯應答報文。這樣,源主機只需要對返回的每一份 ICMP 報文進行解析處理,就可以掌握資料報從源主機到達目的主機途中所經過的路由資訊。

#include <iostream>
#include <winsock2.h>       
#include <ws2tcpip.h>
using namespace std;

#pragma comment(lib, "Ws2_32.lib")

//IP報頭
typedef struct IP_HEADER
{
    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 ICMP_HEADER { BYTE type; //8位型別欄位 BYTE code; //8位程式碼欄位 USHORT cksum; //16位校驗和 USHORT id; //16位識別符號 USHORT seq; //16位序列號 } ICMP_HEADER; //報文解碼結構 typedef struct DECODE_RESULT { 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 )//如果 iSize 為正,即為奇數個位元組 { cksum += *(UCHAR *)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; //ip報頭的長度是以4位元組為單位的 //若資料包大小 小於 IP報頭 + ICMP報頭,則資料報大小不合法 if ( iPacketSize < ( int )( iIpHdrLen + sizeof( ICMP_HEADER ) ) ) return FALSE; //根據ICMP報文型別提取ID欄位和序列號欄位 ICMP_HEADER *pIcmpHdr = ( ICMP_HEADER * )( pBuf + iIpHdrLen );//ICMP報頭 = 接收到的緩衝資料 + IP報頭 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 ) { //輸出往返時間資訊 if(DecodeResult.dwRoundTripTime) cout<<" "<<DecodeResult.dwRoundTripTime<<"ms"<<flush; else cout<<" "<<"<1ms"<<flush; } return true; } void 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; } } cout<<"Tracing roote to "<<IpAddress<<" with a maximum of 30 hops.\n"<<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 = 30; //最大跳站數 //填充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 = 1; //TTL初始值為1 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) ); cout<<iTTL<<flush; //輸出當前序號,flush表示將緩衝區的內容馬上送進cout,把輸出緩衝區重新整理 //填充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 ) bReachDestHost = true; //輸出IP地址 cout<<'\t'<<inet_ntoa( DecodeResult.dwIPaddr )<<endl; break; } } else if( WSAGetLastError() == WSAETIMEDOUT ) //接收超時,輸出*號 { cout<<" *"<<'\t'<<"Request timed out."<<endl; break; } else { break; } } iTTL++; //遞增TTL值 } }

執行結果如下:
這裡寫圖片描述