1. 程式人生 > >利用原始套接字實現tracert路由追蹤

利用原始套接字實現tracert路由追蹤

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