如何利用多种协议实现路由跟踪?
如何利用多种协议实现路由跟踪?
利用多种协议实现路由跟踪
作者:haha567
[开发及运行环境]
开发环境:Visual C++ 7.1(Visual Studio.Net 2003)、Windows XP
运行环境:建议在 Windows 2000 及其以上版本系统上运行
[下载地址和联系方式]
文档地址:http://haha567.com/files/traceroute/traceroutedoc.htm
代码地址:http://haha567.com/files/traceroute/traceroutesrc.rar
网站:blog.haha567.com
QQ:4494634
E-Mail:limin4@21cn.com
由于代码是在VC 7.1下面编写的,所以无法用VC 6打开,使用VC 6的朋友可以用工具(VC++7 to VC++6 project converter)转换一下,该工具介绍及下载页面为 http://www.codetools.com/tools/prjconverter.asp。另外,使用VC 6编译时请尽可能使用最新版的SDK,以免找不到个别头文件。
[运行截图]
[参考书籍]
1、《用TCP/IP协议进行网际互联——第一卷:原理、协议与结构》,第四版,[美] Douglas E. Comer 著,电子工业出版社
2、《TCP/IP详解——卷1:协议》,[美] W. Richard Stevens 著,机械工业出版社
3、《Windows网络编程》,第二版,[美] Anthony Jones, Jim Ohlund 著,清华大学出版社
4、《Windows核心编程》
之所以把参考资料放到文章的最前面,是因为在文中将会提到很多知识,限于篇幅,其中部分知识只能一笔带过,为了方便不熟悉这些知识的朋友查阅有关书籍,将书名列举于此。
[感谢]
感谢CSDN社区和安全焦点论坛的各位网友的帮助,特别感谢网友“烦”和CSDN社区VC网络版版主“PiggyXP”的大力帮助。
[正文]
一、ICMP协议简介及TraceRoute的实现原理
所有的互联网服务都使用一个底层无连接的分组交付系统,其中,一种称为IP路由器或者IP网关的计算机提供了物理网络之间的所有互连,它用来选择用于发送分组的路径。当一台路由器收到一个分组后,分析分组中的源地址和目标地址,借助路由器内部的路由表,选择一条最合适的路径,将该分组交付给路径中的下一台主机或者路由器。那么,当我们从一台主机发送数据报到另外一台主机时,有时为了了解网络结构或者分析故障,需要了解数据报所经过的网络路径,这时采用的办法就是路由跟踪,这样的过程也被称作TraceRoute。
在介绍TraceRoute的实现方法之前,先介绍一下ICMP协议,实现TraceRoute必须依靠该协议。另外,后文还会提及UDP和TCP协议,不过不打算在这里对这两种协议格式进行详细描述,具体知识可以看参考书籍1和2。
为了让互联网中的路由器报告错误或者提供有关意外情况的信息,设计人员在TCP/IP中加入了一种具有特殊用途的报文机制,这种报文机制称为网际控制报文协议ICMP(Internet Control Message Protocol)。在正常的IP数据报传输中,当路由器发现某个数据报有错误或者无法转发时,会向发出该IP数据报的原始站点发送ICMP报文,描述IP数据报中所存在的错误。需要注意的是,ICMP报文和其他协议报文(如UDP报文、TCP报文)一样,也是封装在IP数据报中的,如下图所示,ICMP报文分为ICMP首部和ICMP数据两部分,被一起封装在IP数据报中,IP数据报又被封装在物理帧中。
在ICMP报文中,有类型和代码2个字段,通过他们的值的组合来将问题产生的原因分为好多种。
以下列举几种常见的ICMP报文格式,也是TraceRoute中会遇到到的几种格式。
1、回送请求和应答报文格式
通常用这种报文格式来判断对方主机是否可达。主机或者路由器向指定的目的站发送ICMP回送请求报文,任何收到此报文的计算机形成一个回送应答报文,将其返回给最初的发送者。ping命令其实就是发送这种回送请求报文,然后等待接收回送应答报文。
当类型值为8时,表示回送请求报文,当类型值为0时,表示回送应答报文。在发送端,一般约定通过标识符来标识该报文属于哪个进程(一般以进程ID作为标识符),通过序号来标识该报文属于该进程发送的第几个报文。
2、目的站不可达报文格式
当路由器无法转发或者直接交付IP数据报时,就会产生上述格式的目的站不可达报文。
根据其中代码值可以分为13种情况,需要特别提及的是当代码为3时,表示的含义为“端口不可达”,但事实上此时数据报已经到达目标主机,只是目标端口未响应,所以目标主机产生“端口不可达”的报文,仅仅从命名上容易产生误解,一定要注意。
当给目标主机的一个无法响应的端口发送UDP报文时,目标主机就会产生这种报文,其中代码值为3。
3、数据报超时报文格式
当路由器的选路表出现差错时,可能会形成一个选路循环,为了避免进入该循环的数据报在循环中无休止的传输下去,每个数据报都包含了一个寿命计时器,也就是IP数据报中的TTL字段。
只要路由器处理了某个数据报,就会将其中的TTL值减1,并且在该值减到0时丢弃该数据报,同时产生这样一个数据报超时报文,其中类型值为11,代码值为0。
下面我们举一个例子来描述TraceRoute的实现原理。
假设从源主机向目标主机发送一个IP数据报,当该数据报到达目标主机时,一共经过10个路由器转发,显然源主机发出的IP数据报中的TTL值应该是大于10的,否则该数据报会在途中超时,无法到达目标主机。
如果源主机向目标主机发送一个TTL值为n(1≤n≤10)的IP数据报,那么该数据报是无法到达目标主机的,会在途中第n个路由器超时,该路由器会向源主机发送一个ICMP超时报文,源主机则可以根据该超时报文获取途中第n个路由器的IP地址。
如果源主机向目标主机发送一个TTL值为m(m≥11)的IP数据报,那么该数据报可以到达目标主机。如果在该IP数据报中封装不同类型的协议报文,目标主机则会做出不同的反应,下面我们分类说明。
1、如果该IP数据报内封装的是ICMP回送请求报文,目标主机就会向源主机发送一个ICMP回送应答报文。
2、如果该IP数据报内封装的是UDP报文,在这种报文中会指定目标端口,如果此时目标主机上的目标端口并没有与某个UDP套接字绑定,那么目标主机就会向源主机发送一个ICMP端口不可达报文,但是,如果此时目标主机上的目标端口已经和某个UDP套接字绑定,那么该UDP报文会被发给相关套接字,系统将不会产生ICMP端口不可达报文,因此在采用这种协议时,必须注意目标端口的选择。
3、如果该IP数据报内封装的是TCP报文,该报文也要指定目标端口,并将其中SYN标志置1,作为第一次握手。如果此时目标主机上的目标端口正处在监听状态,那么目标主机会向源主机回应一个TCP报文,其中SYN和ACK标志位置1,确认号为上一次握手的序列号加1,但是如果此时目标主机上的目标端口不处在监听状态,那么目标主机也会向源主机回应一个TCP报文,不过将其中RST和ACK标志位置1,表示复位,不过确认号为上一次握手的序列号加上收到的字节数再加上1,这里收到的字节数指的是TCP报文中的数据部分字节数,为了验证这一点,程序中为第一次握手的TCP报文加上了10个字节的数据部分。
那么,为了获取源主机到目标主机沿途所经过的路由器地址,源主机只需依次发送TTL值为1、TTL值为2、TTL值为3……的IP数据报,接收沿途路由器返回的ICMP超时报文,从中提取路由器地址,直到最后收到目标主机的回应为止。另外,当路由器无法将数据报交付给目标主机时,可能是因为选路失败或者网络故障,路由器会产生ICMP目的站不可达报文发送给源主机。此时TraceRoute过程也应该终止。
经过比较发现,利用第一种方式进行路由跟踪是最简单的,但是我为什么还要实现另外两种方式呢?呵呵,详见后记。
二、利用原始socket实现及其要点
为了能够方便的构造上文提到的各种报文,可以采用原始socket,自行填充IP数据报和内部封装的报文的所有字段。
另外,原始socket经过设置以后,系统会将收到所有ICMP报文、UDP报文和TCP报文都复制给这个原始socket,所以可以利用相同的socket来监听系统收到的以上三种类型的协议报文,不过我们仅仅关注其中的ICMP和TCP两种类型,对他们逐一识别,从中过滤出由目标主机返回的报文。
这里不打算对原始socket进行细致讲解,具体知识可以看参考书籍3。不过仍需要提及本程序中对于原始socket的3点特殊设置。
IP_HDRINCL:
通过设置该选项,可以自行构造IP头。对于生成和发送UDP和TCP协议报文,这是必须的,但是对于生成和发送ICMP报文,就不是必须的了,为了统一处理,设置了该选项,不论IP数据报中封装的何种协议报文,均需手动构造IP头。
IPPROTO_IP:
由于程序中的原始socket可能涉及ICMP、UDP、TCP三种协议报文的发送和接收,而他们都是被封装在IP数据报中的,所以将该原始socket协议的类型设置为IPPROTO_IP,这样就可以兼顾这三种协议报文。
SIO_RCVALL:
通过设置该选项,可以让原始socket接收到与之绑定的本地网络接口上收到的所有数据。假如不设置该选项,那么发送设置SYN标志的TCP报文时,由于是通过原始socket发送的,系统并不会对这个TCP连接进行登记,所以当系统接收到目标主机返回的设置ACK标志的TCP报文时,会直接将报文其丢弃,而不会转给这个原始socket,所以必须设置该选项,以接收目标主机的回应。
三、界面说明和端口选择
如图,界面上的控制元素很多,下面逐一讲解其用途。
目标地址:顾名思义,就是目标主机的地址,这里可以填入其IP地址,也可以填入其域名,程序会自动解析其IP地址,如果无法解析,则报错提示。
协议类型:提供ICMP、UDP、TCP三种协议类型供选择,分别对应上文提到的三种TraceRoute的方法。
目标端口:此选项针对UDP协议和TCP协议,对ICMP协议无效。
源端口:此选项针对UDP协议和TCP协议,对ICMP协议无效。
超时时间:等待目标主机或者路由器回应的时候,没有必要一直等下去,所以提供超时时间设置。
连续超时次数最大值:
如果在超时时间内没有收到目标主机或者路由器的回应,那么据此终止TraceRoute是不合理的,因为还有可能是因为超时时间设置过短,或者途中某个路由器禁止响应超时的IP数据报。比较稳妥的做法增加TTL值,继续探测下一个路由器,当然,我们也不应该一直忽略下去,所以提供选项设置一个连续超时次数最大值,当达到此最大值后,才得出“等待超时”的结论,停止TraceRoute,以便用户考虑是否加长超时时间,或者得出“目标主机不可达”的结论。
序号:表示该路由器或者目标主机是路径上第几个节点。
地址:显示该路由器或者目标主机的IP地址。
状态:用来表示当前处于何种状态(到达路由器、到达目标主机或者无法到达目标主机)。
接收包序号:用来表示该包在所有接收到的包中的序号。
端口选择注意事项:
当选择ICMP协议时,无需关注端口。
当选择UDP协议时,目标端口可能需要尝试多个不同的端口,以便选择一个在目标主机系统上没有被绑定的UDP端口,否则无法收到所需的应答。
当选择TCP协议时,目标端口建议选择80端口,因为假如目标主机是一个Web服务器,大都会在该端口开启服务,即使不是,一般也会收到RST+ACK的应答。
另外,目标主机上的防火墙规则将直接影响目的端口和源端口的选择,在确定目标主机存在的时候,如果依然超时,一般是由于目标主机上防火墙的配置原因,请换其他的端口或者协议尝试,例如对于部分站点当通过TCP方式时,源端口不能为0,当通过UDP方式时,目标端口不能为80。
一般来说,如果对方主机确实存在,那么对于ICMP方式,只要网关不做限制,成功到达目标的几率是最大的,对于TCP方式,当目标端口为80,源端口不为0时,可以成功连接到大多数Web站点,而UDP方式就需要尝试多个端口号了。
四、程序控制要点
1、在程序中,创建辅助线程用来进行TraceRoute,因为假如整个程序只有一个线程,那么网络操作会影响窗口消息循环,导致窗口“死掉”。
2、采用WSAEventSelect模型,不过由于程序控制的需要,将通过WSAWaitForMultipleEvents等待以下3个事件,关于该模型的知识可以看参考书籍3。
WSAEVENT_TRACE:
该事件用来关联FD_READ。
WSAEVENT_WAIT_STOP:
当从程序界面上要求终止TraceRoute时,是不能强行终止相关辅助线程的,为了让辅助线程尽快得知这个事件并进行收尾工作,采用该事件进行通知。
WSAEVENT_TIMER:
每次发送报文后,等待路由器或者目标主机的回应,这种等待有一个超时时间,按照一般的思路,可以将该超时时间作为WSAWaitForMultipleEvents函数的超时时间参数,但其实还忽略了一个问题,因为我们在使用原始socket接收数据,会收到许多不是发给这个socket的数据,因为对于系统而言,原始socket发送的数据是没有记录的,所以它会把接收到的ICMP报文复制一份给原始socket,当选择TCP协议时,由于设置了SIO_RCVALL,收到的无关数据就更多了。每收到一个无关的数据报,就会触发WSAEVENT_TRACE,因此在网络很繁忙时,WSAWaitForMultipleEvents几乎不可能超时,也就无法进行有效的超时检测。
为了解决这个问题,在程序中选择了一种与网络无关的超时控制方式——定时器,定时器作为一个内核对象,同样可以接受WSAWaitForMultipleEvents的管理。每次发送一个报文时,重新设置定时器,所以无论接收多少无关报文,都不会影响超时检测。
五、代码流程
用于进行TraceRoute过程的辅助线程代码全部位于文件TraceThread.cpp中,其中流程比较复杂,所以用文字将主函数的代码简单描述如下。
DWORD WINAPI TraceThread(PVOID pvParam)
{
根据所选协议类型,计算待发送的IP数据报的长度,分配适当空间作为报文发送缓冲区;
填充报文发送缓冲区中的IP头,并置TTL值为0,填充其中封装的协议报文;
创建原始socket,协议类型为IPPROTO_IP,配置该socket为自行构造IP头(IP_HDRINCL),将该socket绑定到本地网络接口上;
如果选择采用TCP协议,则配置该socket接收与之绑定的网络接口上收到的所有数据(SIO_RCVALL);
将上文提到的三个事件初始化,并通过WSAEventSelect函数将 WSAEVENT_TRACE 与 FD_READ 相关联;
置Trace结束标志为假,bTraceEnd = false,该标志用来在后面的循环中判断何时结束;
do
{
将IP头中的TTL值加1,发送该IP数据报;
重新设置定时器,该定时器用来检查超时;
置数据报有效标志为假,bRecvDataIsValid = false;
do
{
WSAWaitForMultipleEvents,等待上文提到的3个事件之一被传信;
if (函数WSAWaitForMultipleEvents调用失败)
标记为错误退出,转 Exit0;
if (事件WSAEVENT_TIMER被传信)
根据连续超时次数判断,选择超时退出,或者break内层循环,以便进入下一轮外层循环,继续探测下一个路由器;
if (事件WSAEVENT_WAIT_STOP被传信)
标记为手动终止,转 Exit0;
// 已经有数据可以接收
接收数据;
if (接收到的数据不是发给本机的)
continue,继续等待下一个数据报;
根据接收到的报文的类型,分别交给函数AnalyseTCPPacket或者AnalyseICMPPacket进行分析;
(上述分析函数会识别该报文是否有效,即是否表示“到达路由器”、“到达目标主机”、“无法到达目标主机”三个状态之一)
if (报文无效)
continue,继续等待下一个数据报;
// 报文经分析有效
置数据报有效标志为真,bRecvDataIsValid = true,将连续超时次数清零;
if (报文表示含义为到达目标主机)
置Trace结束标志为真,bTraceEnd = true;
if (报文表示含义为目标不可达)
置Trace结束标志为假,bTraceEnd = false;
通知窗口界面显示发送该报文的主机IP地址,以及当前状态(到达路由器、到达目标主机或者无法到达目标主机);
} while (bRecvDataIsValid == false);// 如果数据报无效,才进入下一轮循环,继续等待下一个数据报
} while (bTraceEnd == false);// 如果Trace终止标志无效,则进入下一轮循环,继续探测下一个路由器
Exit0:
进行最后的收尾工作,获取错误代码,关闭事件对象,释放发送缓冲区内存,关闭socket;
}
线程函数中涉及的用来分析报文的两个函数(AnalyseTCPPacket和AnalyseICMPPacket)实现代码如下:
//////////////////////////////////////////////////////////////////////////
//AnalyseTCPPacket//
//@ipv4hdr数据发送缓冲区中的IP头地址//
//@recbv_ipv4hdr数据接收缓冲区中的IP头地址//
//@nTraceProtocolType所选择的协议类型//
//@return//
//nRetResult0表示分析失败,不是需要的数据报//
//nRetResult1表示分析成功//
//////////////////////////////////////////////////////////////////////////
intAnalyseTCPPacket(IPV4_HDR*ipv4hdr,IPV4_HDR*recv_ipv4hdr,intnTraceProtocolType,int&nRetMessageID)
{
intnRetResult=0;
intnIPv4_HDRSize=0;
TCP_HDR*tcphdr=NULL;
TCP_HDR*recv_tcphdr=NULL;
nRetResult=0;
nRetMessageID=MESSAGE_OTHER_PACKET;//其他的数据包
nIPv4_HDRSize=(ipv4hdr->ip_verlen%0x10)*4;
tcphdr=(TCP_HDR*)((unsignedchar*)ipv4hdr+nIPv4_HDRSize);
nIPv4_HDRSize=(recv_ipv4hdr->ip_verlen%0x10)*4;
recv_tcphdr=(TCP_HDR*)((unsignedchar*)recv_ipv4hdr+nIPv4_HDRSize);
if(ipv4hdr->ip_destaddr!=recv_ipv4hdr->ip_srcaddr)
gotoExit0;
if(tcphdr->tcp_destport!=recv_tcphdr->tcp_srcport)
gotoExit0;
//当目标端口没有监听时,对方系统返回ack+rst,确认号为第一次握手的序列号加收到的字节数加1
if(recv_tcphdr->tcp_flags==0x14&&
ntohl(recv_tcphdr->tcp_acknowledgement)==ntohl(tcphdr->tcp_sequence)+10+1)
{
nRetResult=1;
nRetMessageID=MESSAGE_REACH_DEST;
}
//当目标端口正在监听时,对方系统返回ack+syn,确认号为第一次握手的序列号加1
if(recv_tcphdr->tcp_flags==0x12&&
ntohl(recv_tcphdr->tcp_acknowledgement)==ntohl(tcphdr->tcp_sequence)+1)
{
nRetResult=1;
nRetMessageID=MESSAGE_REACH_DEST;
}
//这两段代码有些冗余,不过希望把条件描述清楚了
Exit0:
returnnRetResult;
}
//////////////////////////////////////////////////////////////////////////////
//AnalyseICMPPacket//
//@ipv4hdr数据发送缓冲区中的IP头地址//
//@recbv_ipv4hdr数据接收缓冲区中的IP头地址//
//@nTraceProtocolType所选择的协议类型//
//@return//
//nRetResult0表示分析失败,不是需要的数据报//
//nRetResult1表示分析成功,到达目标、到达路由器、或者出错//
//////////////////////////////////////////////////////////////////////////////
intAnalyseICMPPacket(IPV4_HDR*ipv4hdr,IPV4_HDR*recv_ipv4hdr,intnTraceProtocolType,int&nRetMessageID)
{
ICMPV4_HDR*icmpv4hdr=NULL;
ICMPV4_HDR*recv_icmpv4hdr=NULL;
unsignedchar*ptr1=NULL;
unsignedchar*ptr2=NULL;
intnIPv4_HDRSize=0;
intnRetResult=0;
nRetResult=0;//分析失败,不是需要的数据报
nRetMessageID=MESSAGE_OTHER_PACKET;//其他的数据包
nIPv4_HDRSize=(ipv4hdr->ip_verlen%0x10)*4;
icmpv4hdr=(ICMPV4_HDR*)((unsignedchar*)ipv4hdr+nIPv4_HDRSize);
nIPv4_HDRSize=(recv_ipv4hdr->ip_verlen%0x10)*4;
recv_icmpv4hdr=(ICMPV4_HDR*)((unsignedchar*)recv_ipv4hdr+nIPv4_HDRSize);
switch(recv_icmpv4hdr->icmp_type)
{
case0://回送应答报文
if(nTraceProtocolType==IPPROTO_ICMP&&
ipv4hdr->ip_destaddr==recv_ipv4hdr->ip_srcaddr&&
icmpv4hdr->icmp_id==recv_icmpv4hdr->icmp_id&&
icmpv4hdr->icmp_sequence==recv_icmpv4hdr->icmp_sequence
)
{
nRetResult=1;
nRetMessageID=MESSAGE_REACH_DEST;
}
break;
case3://目的站不可达报文
case11://超时报文
ptr1=(unsignedchar*)ipv4hdr;
ptr2=(unsignedchar*)recv_icmpv4hdr+8;//指向产生差错的IP首部
//检查IP头和内层数据报的头8字节是否一致
*(ptr2+4)=0;//将ip_id置零,便于后续比较
*(ptr2+5)=0;
*(ptr2+10)=0;//将ip_checksum置零,便于后续比较
*(ptr2+11)=0;
if(memcmp(ptr1,ptr2,8)!=0)
break;
if(memcmp(ptr1+9,ptr2+9,17)!=0)//比较时跳过ttl字段,包括udp首部,但是不含其校验和(没有明白为啥不能含校验和)
break;
if(recv_icmpv4hdr->icmp_type==11&&recv_icmpv4hdr->icmp_code==0)
{
nRetResult=1;
nRetMessageID=MESSAGE_REACH_ROUTER;
break;
}
if(recv_icmpv4hdr->icmp_type==3)
{
nRetResult=1;
if(recv_icmpv4hdr->icmp_code==3&&//端口不可达报文
ipv4hdr->ip_protocol==IPPROTO_UDP&&//UDP协议时才会产生这种报文
recv_ipv4hdr->ip_srcaddr==sinDest.sin_addr.s_addr//是否由目标地址返回
)
nRetMessageID=MESSAGE_REACH_DEST;
else
nRetMessageID=MESSAGE_CANNOT_REACH_DEST;
break;
}
break;
}
returnnRetResult;
}
六、依然没有解决的问题
到目前所知,依然没有办法解决的问题只有一个:当目标地址是本机IP地址时,通过ICMP和UDP两种方式都可以成功到达目标主机,但是通过TCP方式却无法到达目标主机,等待超时。
究其原因,当网络上层意识到某个数据包是由本机发给本机时,就会直接将数据包转给相关模块处理,而不经由网卡发送。而三种协议方式都是通过原始socket来获取相关报文,监听的层次决定了是否可以收到从本机到本机的数据包,在选择ICMP和UDP方式时,与在选择TCP方式时,该原始socket的监听设置是不一样的,猜测是这2种设置的不同决定了监听层次的差别,从而导致前两种方式可以接收从本机到本机的数据包,而后一种方式不行。
不过,当目标地址是本机地址时,不需要经过任何路由,所以这个问题并不会影响程序原本的设计意图。
七、后记
从我刚开始学习ICMP协议到现在这个小程序开发完成,断断续续的也快有半年了,在文章最后来回忆一下整个来龙去脉,其中有很多的想法和思考……
最早的时候,手上只有2本书,《用TCP/IP进行网际互联》和《Windows网络编程》,对于前一本书,我一章一章的学,在学习完ARP协议,写了一个图形化界面的ARP包发送工具,正是因为这个东东,结识了PiggyXP和烦,感谢他们这么久以来对我的帮助。现在看到PiggyXP写的ARP发送工具,我那个东东都不敢拿出来了,呵呵……
在学完ARP以后,就开始学IP和ICMP,还是那个习惯,打算做点东西,从《Windows网络编程》上看到了利用原始socket实现TraceRoute的例子,觉得不错,于是就照葫芦画瓢利用ICMP协议做了一个,当时还真没有想起来可以用其他协议实现,不过很郁闷的是在公司竟然跑不了,猜测离自己最近的一个网关被配置为不响应和转发ICMP,大概是因为当时病毒闹得凶的原因吧。后来只好传给网友帮忙测试,呵呵,测试结果则通过截图传给我!
认识烦以后(别看错了,这个网友的名字就叫“烦”,后文我懒得打双引号了),谈到了在公司遇到的这种郁闷事情,当时他提议我用UDP协议试一下,于是将《TCP/IP详解》上的相关章节发给我看,看后豁然开朗,于是趁着第二天公司限电不上班,赶到湾仔沙的书店去买了这本书,在海边找了一条石凳,坐在那里吹着海风看了一个下午的书(呵呵,有没有珠海的朋友,看到这里应该很熟悉吧),这本书比另外一本要详细一些,而且有很多情景分析的例子,非常适合对协议的进一步理解。
看了书以后才意识到还可以用UDP和TCP来实现这个东东,不过心中依然存有疑虑,万一公司网关把双向的ICMP都禁止了咋办,于是当天晚上就抄起iris,做了简单的试验,得出了结论,公司网关仅仅对内网发给它的ICMP做了限制,嗯,不得不提的是iris确实是一个好东西,尤其是可以自己改包然后发出去,真的很方便,不过如果可以像PiggyXP那样自己写sniffer就可以做更顺心的sniffer了。
后来,开始打算用ICMP、UDP和TCP三种协议分别实现TraceRoute,由于手上参考资料比较齐全,很快就利用原始socket通过UDP达到了效果,但是在测试连接一个不存在的IP地址时,发现无法达到超时的效果,调试发现经常出现一个由本机地址发给本机地址的UDP报文,而当时的超时时间是作为函数WSAWaitForMultipleEvents的超时时间参数,所以如前文所分析,是很难达到我所想像的超时的。郁闷了整整一周,突然有天发现当我关闭SQL Server服务管理器时程序可以正常的超时,嗯……原来如此,那个SQL Server服务管理器默认情况下每5秒钟会检查本机上的SQL Server服务器状态,所以我会经常收到一个从本机到本机的UDP报文。
如上,问题出现了,如何解决呢?还是一周过后,模模糊糊的摸索解决方案时,幸得同事提醒用定时器,于是当晚便抓了本《Windows核心编程》猛啃相关章节,连夜编码,成功解决这个问题,哈哈……原理就不多罗嗦了,前文应该写清楚了。
很快,遇到了下一个问题就是关于TCP的,当时对于TCP还不够了解,以为自己发过去一个syn,对方就应该会一个ack+syn,并且确认号应该等于我发的序号加1,但是发现连接一些不处在监听状态的端口是返回的却是ack+rst,而且确认号和我预算的也不一样。后来由于工作繁忙的原因,这个问题就放下了,于是一放就过了五一,打算把《TCP/IP详解》好好看看,待到6月上旬的时候,才看到TCP这里,才明白了rst的缘由,嗯……又一个难关攻破了,而且此时程序已经大体完工,开始大量测试……
在测试过程中发现,假如目标主机确实存在,当使用UDP和TCP两种协议时,是否可以成功到达目标主机和两个端口号的选择有很大关系。早期界面上的端口号默认值是目标端口80,源端口0,在内网没问题,外网的许多站点也都没问题,但是也有例外,比如用TCP方式连接www.microsoft.com,后来用iris经过多种尝试,发现问题在于源端口号上,该值不能为0,后来更改为2000,成功到达目标主机。至于端口号的选取要点在前文已经概括,这里就不多罗嗦了。
测试过程中还发现了一个问题,当选择TCP方式时,目标地址填写本机地址,最终无法到达目标主机。这个问题的具体情况在本文的第六节已经描述过,后来归结为如何通过原始socket接收从本机到本机的TCP报文,至今没有发现解决办法…… 希望正在看这篇文章的你能够帮助我解决这个问题。
罗罗嗦嗦的写了这么多,希望您已经看明白了,如果还有不明白或者不认同的地方,欢迎通过文章开头提供的联系方式和我联系,共同讨论。