在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报文。