Traceroute(路由追踪)的原理及实现

Traceroute(路由追踪)的原理及实现

(1)相应的协议和原理

IP协议IP协议是TCP/IP协议族中最核心的部分,它的作用是在两台主机之间传输数据,所有上层协议的数据(HTTP、TCP、UDP等)都会被封装在一个个的IP数据包中被发送到网络上。

ICMP ICMP全称为互联网控制报文协议,它常用于传递错误信息,ICMP协议是IP层的一部分,它的报文也是通过IP数据包来传输的。

TTL TTL(time-to-live)是IP数据包中的一个字段,它指定了数据包最多能经过几次路由器。从我们源主机发出去的数据包在到达目的主机的路上要经过许多个路由器的转发,在发送数据包的时候源主机会设置一个TTL的值,每经过一个路由器TTL就会被减去一,当TTL为0的时候该数据包会被直接丢弃(不再继续转发),并发送一个超时ICMP报文给源主机。

基于ICMP实现

实现原理:Tracert 程序关键是对 IP 头部生存时间(time to live)TTL 字段的使用,程序实现是向目的主机发送一个 ICMP 回显请求报文,初始时 TTL 等于 1 ,这样当该数据报抵达途中的第一个路由器时,TTL 的值就被减为 0,导致发送超时错误,因此该路由生成一份 ICMP 超时差错报文返回给源主机。随后,主机将数据报的 TTL 值递增 1 ,以便 IP 报能传送到下一个路由器,并由下一个路由器生成 ICMP 超时差错报文返回给源主机。不断重复这个过程,直到数据报达到目的主机或超过跳数限制,到达目的主机后,目的主机返回 ICMP 回显应答报文。这样,源主机只需要对返回的每一份 ICMP 报文进行解析处理,就可以掌握数据报从源主机到达目的主机途中所经过的路由信息。

采用这种方案的实现流程如下:

  1. 客户端发送一个TTL为1的ICMP请求回显数据包,在第一跳的时候超时并返回一个ICMP超时数据包,得到第一跳的地址。
  2. 客户端发送一个TTL为2的ICMP请求回显数据包,得到第二跳的地址。
  3. 客户端发送一个TTL为3的ICMP请求回显数据包,到达目标主机,目标主机返回一个ICMP回显应答,traceroute结束。
  1. 实现的C++代码(VS2017)

#include <iostream>
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#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 = WSASocketW(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值
	}
	system("pause");
}

 

 

  1. 运行结果:(可以看到访问相同的网站所经过的路由路径不唯一

 

 

 

 

 

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

衣带渐宽人憔悴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值