利用原始套接字实现tracert路由追踪

时间:2021-08-09 01:48:52

在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打印出来的信息):

利用原始套接字实现tracert路由追踪

我的程序除了目的主机的ICMP-ECHOREPLY报文收到了以外,其它的ICMP-ECHO请求全部超时了(是socket超时,不是返回超时报文),感觉就是被路由器丢弃了,并且没有返回ICMP-TIMEOUT报文。用了很多办法都没有解决,后来死马当作活马医的心态在控制面板中关闭了windows放火墙,居然就对了,运行结果如下:

利用原始套接字实现tracert路由追踪

与tracert命令的结果一样,说明追踪的结果是对的。

可能是windows的防火墙会自动检测和过滤一些无意义的报文,增加自身操作系统的稳定性。以后网络编程的东西要是结果不对,都可以试一试关闭防火墙。

至于头两个路由器为什么一直都没反应,我猜测是学校的路由器的设置和其它因特网中路由器的设置不一样,会自动丢弃超时报文而不返回ICMP-TIMEOUT报文。