ICMP的應用之路由追蹤程式(Tracert)
阿新 • • 發佈:2019-01-08
實現原理:
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值
}
}
執行結果如下: