利用原始套接字實現tracert路由追蹤
阿新 • • 發佈:2019-02-06
在windows的命令列下,使用tracert 域名/IP地址 可以記錄本機到目的主機所經過的路由器的IP地址。這個功能使用原始套接字也可以實現。
我們通過不斷地向目的主機發送ICMP-ECHORequest包,並且將包的TTL一開始設為1,這樣一到達閘道器路由器後,路由器就檢測到這個包超時了(TTL=0了),於是就會丟棄次包,並返回一個ICMP超時報文,在ICMP超時報文中,包含了路由器的IP地址資訊,於是解析這個資訊並列印就可以了。
接著再發送一個ICMP-ECHORequest報文,這次將TTL設為2,這樣的話將會抵達第二個路由器,第二個路由器發現TTL=0,丟棄後返回ICMP超時報文,解析即可。
同理,迴圈不斷地將TTL值加1,傳送ICMP-ECHO報文Request直到收到目的主機的ICMP-ECHOREPLY報文,說明已經到達目的主機,退出迴圈。
#include "stdafx.h" #pragma pack(4) #define WIN32_LEAN_AND_MEAN #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #include <stdlib.h> #pragma comment(lib,"ws2_32.lib") #define ICMP_ECHOREPLY 0 #define ICMP_DESTUNREACH 3 #define ICMP_SRCQUENCH 4 #define ICMP_REDIRECT 5 #define ICMP_ECHO 8 #define ICMP_TIMEOUT 11 #define ICMP_PARMERR 12 #define MAX_HOPS 30 #define ICMP_MIN 8 // Minimum 8 byte icmp packet (just header) typedef struct iphdr { unsigned int h_len : 4; // Length of the header unsigned int version : 4; // Version of IP unsigned char tos; // Type of service unsigned short total_len; // Total length of the packet unsigned short ident; // Unique identifier unsigned short frag_and_flags; // Flags unsigned char ttl; // Time to live unsigned char proto; // Protocol (TCP, UDP etc) unsigned short checksum; // IP checksum unsigned int sourceIP; // Source IP unsigned int destIP; // Destination IP } IpHeader; typedef struct _ihdr { BYTE i_type; // ICMP message type BYTE i_code; // Sub code USHORT i_cksum; USHORT i_id; // Unique id USHORT i_seq; // Sequence number // This is not the std header, but we reserve space for time //ULONG timestamp; } IcmpHeader; #define DEF_PACKET_SIZE 32 #define MAX_PACKET 1024 void usage(char *progname) { printf("usage: %s host-name [max-hops]\n", progname); ExitProcess(-1); } int set_ttl(SOCKET s, int nTimeToLive) { int nRet; nRet = setsockopt(s, IPPROTO_IP, IP_TTL, (LPSTR)&nTimeToLive, sizeof(int)); if (nRet == SOCKET_ERROR) { printf("setsockopt(IP_TTL) failed: %d\n", WSAGetLastError()); return 0; } return 1; } int decode_resp(char *buf, int bytes, SOCKADDR_IN *from, int ttl) { IpHeader *iphdr = NULL; IcmpHeader *icmphdr = NULL; unsigned short iphdrlen; struct hostent *lpHostent = NULL; struct in_addr inaddr = from->sin_addr;//from是從recv函式裡返回過來的 iphdr = (IpHeader *)buf; // Number of 32-bit words * 4 = bytes iphdrlen = iphdr->h_len * 4;//首部長度的單位是32位字 if (bytes < iphdrlen + ICMP_MIN)//8 printf("Too few bytes from %s\n", inet_ntoa(from->sin_addr)); icmphdr = (IcmpHeader*)(buf + iphdrlen);//指向icmp頭部分 switch (icmphdr->i_type)//檢測ICMP報文型別 { case ICMP_ECHOREPLY: // Response from destination //(如果是ICMP_ECHOREPLY報文,說明不是因為TTL=0被丟棄的,說明到達了目的主機) lpHostent = gethostbyaddr((const char *)&from->sin_addr, AF_INET, sizeof(struct in_addr));//獲取主機名 if (lpHostent != NULL) printf("%2d %s (%s)\n", ttl, lpHostent->h_name, inet_ntoa(inaddr));//列印主機地址 return 1; break; case ICMP_TIMEOUT: // Response from router along the way //(如果是ICMP_TIMEOUT報文的話,說明被路由器超時丟棄了,所以返回值為0,告訴主迴圈還沒有完成) printf("%2d %s\n", ttl, inet_ntoa(inaddr)); return 0; break; case ICMP_DESTUNREACH: // Can't reach the destination at all printf("%2d %s reports: Host is unreachable\n", ttl, inet_ntoa(inaddr)); return 1; break; default: printf("non-echo type %d recvd\n", icmphdr->i_type); return 1; break; } return 0; } USHORT checksum(USHORT *buffer, int size) { unsigned long cksum = 0; while (size > 1) { cksum += *buffer++; size -= sizeof(USHORT); } if (size) cksum += *(UCHAR*)buffer; cksum = (cksum >> 16) + (cksum & 0xffff); cksum += (cksum >> 16); return (USHORT)(~cksum); } void fill_icmp_data(char * icmp_data, int datasize) { IcmpHeader *icmp_hdr; char *datapart; icmp_hdr = (IcmpHeader*)icmp_data; icmp_hdr->i_type = ICMP_ECHO;//icmp_echo_request icmp_hdr->i_code = 0;// icmp_hdr->i_id = (USHORT)GetCurrentProcessId(); icmp_hdr->i_cksum = 0; icmp_hdr->i_seq = 0; datapart = icmp_data + sizeof(IcmpHeader);//將指標指向資料部分以便能填充資料部分 // Place some junk in the buffer. Don't care about the data... memset(datapart, 'E', datasize - sizeof(IcmpHeader)); } int main(int argc, char **argv) { WSADATA wsd; SOCKET sockRaw; HOSTENT *hp = NULL; SOCKADDR_IN dest, from; int ret, datasize, fromlen = sizeof(from), timeout, done = 0, maxhops, ttl = 1; char *icmp_data, *recvbuf; BOOL bOpt; USHORT seq_no = 0; if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) { printf("WSAStartup() failed: %ld\n", GetLastError()); return -1; } if (argc < 2) { usage(argv[0]); } maxhops = 30; //When the af parameter is AF_INET or AF_INET6 and the type is SOCK_RAW, //the value specified for the protocol is set in the protocol field of the IPv6 or IPv4 packet header. sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0, WSA_FLAG_OVERLAPPED); if (sockRaw == INVALID_SOCKET) { printf("WSASocket() failed: %d\n", WSAGetLastError()); ExitProcess(-1); } timeout = 1000; ret = setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout)); if (ret == SOCKET_ERROR) { printf("setsockopt(SO_RCVTIMEO) failed: %d\n", WSAGetLastError()); return -1; } timeout = 1000; ret = setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout)); if (ret == SOCKET_ERROR) { printf("setsockopt(SO_SNDTIMEO) failed: %d\n", WSAGetLastError()); return -1; } ZeroMemory(&dest, sizeof(dest)); dest.sin_family = AF_INET; if ((dest.sin_addr.s_addr = inet_addr(argv[1])) == INADDR_NONE)//如果inet_addr()轉出來的是一個無效的網路地址,說明輸入的是域名 //需要gethostbyname才能獲得目的IP { hp = gethostbyname(argv[1]);//那麼就用gethostbyname()取得網路地址 if (hp) memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length); else { printf("Unable to resolve %s\n", argv[1]); ExitProcess(-1); } } datasize = DEF_PACKET_SIZE;//32 datasize += sizeof(IcmpHeader); icmp_data = (char *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, MAX_PACKET);//分配堆記憶體 recvbuf = (char *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, MAX_PACKET); if ((!icmp_data) || (!recvbuf)) { printf("HeapAlloc() failed %ld\n", GetLastError()); return -1; } memset(icmp_data, 0, MAX_PACKET); fill_icmp_data(icmp_data, datasize); printf("\nTracing route to %s over a maximum of %d hops:\n\n", argv[1], maxhops); for (ttl = 1; ((ttl < maxhops) && (!done)); ttl++) { int bwrote; set_ttl(sockRaw, ttl); ((IcmpHeader*)icmp_data)->i_cksum = 0; //((IcmpHeader*)icmp_data)->timestamp = GetTickCount(); ((IcmpHeader*)icmp_data)->i_seq = seq_no++; ((IcmpHeader*)icmp_data)->i_cksum = checksum((USHORT*)icmp_data, datasize); bwrote = sendto(sockRaw, icmp_data, datasize, 0, (SOCKADDR *)&dest, sizeof(dest)); if (bwrote == SOCKET_ERROR) { if (WSAGetLastError() == WSAETIMEDOUT) { printf("%2d Send request timed out.\n", ttl); continue; } printf("sendto() failed: %d\n", WSAGetLastError()); return -1; } ret = recvfrom(sockRaw, recvbuf, MAX_PACKET, 0, (struct sockaddr*)&from, &fromlen); if (ret == SOCKET_ERROR) { if (WSAGetLastError() == WSAETIMEDOUT) { printf("%2d Receive Request timed out.\n", ttl); continue; } printf("recvfrom() failed: %d\n", WSAGetLastError()); return -1; } done = decode_resp(recvbuf, ret, &from, ttl); Sleep(1000); } HeapFree(GetProcessHeap(), 0, recvbuf); HeapFree(GetProcessHeap(), 0, icmp_data); system("tracert www.nwpu.edu.cn");//與系統自帶的tracert命令進行比較 system("pause"); return 0; }
一開始的時候直接執行程式得到了如下結果(上面的資訊是我的程式的資訊,下面的是windows自帶的tracert打印出來的資訊):
我的程式除了目的主機的ICMP-ECHOREPLY報文收到了以外,其它的ICMP-ECHO請求全部超時了(是socket超時,不是返回超時報文),感覺就是被路由器丟棄了,並且沒有返回ICMP-TIMEOUT報文。用了很多辦法都沒有解決,後來死馬當作活馬醫的心態在控制面板中關閉了windows放火牆,居然就對了,執行結果如下:
與tracert命令的結果一樣,說明追蹤的結果是對的。
可能是windows的防火牆會自動檢測和過濾一些無意義的報文,增加自身作業系統的穩定性。以後網路程式設計的東西要是結果不對,都可以試一試關閉防火牆。
至於頭兩個路由器為什麼一直都沒反應,我猜測是學校的路由器的設定和其它因特網中路由器的設定不一樣,會自動丟棄超時報文而不返回ICMP-TIMEOUT報文。