CSDN博客

img benbebnmao

利用多种协议实现路由跟踪

发表于2004/7/1 16:29:00  1564人阅读

利用多种协议实现路由跟踪

作者: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;
}


    线程函数中涉及的用来分析报文的两个函数(AnalyseTCPPacketAnalyseICMPPacket)实现代码如下:

//////////////////////////////////////////////////////////////////////////
// AnalyseTCPPacket                                                     //
//   @ipv4hdr              数据发送缓冲区中的IP头地址                   //

//   @recbv_ipv4hdr        数据接收缓冲区中的IP头地址                   //
//   @nTraceProtocolType   所选择的协议类型                             //
//   @return                                                            //
//      nRetResult        0 表示分析失败,不是需要的数据报              //

//      nRetResult        1 表示分析成功                                //
//////////////////////////////////////////////////////////////////////////
int AnalyseTCPPacket(IPV4_HDR *ipv4hdr, IPV4_HDR *recv_ipv4hdr, int nTraceProtocolType, int &nRetMessageID)
{
    int         nRetResult = 0;
    int         nIPv4_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 *)( (unsigned char *)ipv4hdr + nIPv4_HDRSize );

    nIPv4_HDRSize = (recv_ipv4hdr->ip_verlen % 0x10) * 4;
    recv_tcphdr = (TCP_HDR *)( (unsigned char *)recv_ipv4hdr + nIPv4_HDRSize );

    if (ipv4hdr->ip_destaddr != recv_ipv4hdr->ip_srcaddr)
        goto Exit0;

    if (tcphdr->tcp_destport != recv_tcphdr->tcp_srcport)
        goto Exit0;

    
// 当目标端口没有监听时,对方系统返回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:
    return nRetResult;
}


//////////////////////////////////////////////////////////////////////////////
// AnalyseICMPPacket                                                        //
//   @ipv4hdr              数据发送缓冲区中的IP头地址                       //
//   @recbv_ipv4hdr        数据接收缓冲区中的IP头地址                       //
//   @nTraceProtocolType   所选择的协议类型                                 //
//   @return                                                                //
//      nRetResult         0 表示分析失败,不是需要的数据报                 //
//      nRetResult         1 表示分析成功,到达目标、到达路由器、或者出错   //
//////////////////////////////////////////////////////////////////////////////
int AnalyseICMPPacket(IPV4_HDR *ipv4hdr, IPV4_HDR *recv_ipv4hdr, int nTraceProtocolType, int &nRetMessageID)
{
    ICMPV4_HDR      *icmpv4hdr = NULL;
    ICMPV4_HDR      *recv_icmpv4hdr = NULL;
    unsigned char   *ptr1 = NULL;
    unsigned char   *ptr2 = NULL;
    int             nIPv4_HDRSize = 0;
    int             nRetResult = 0;

    nRetResult = 0;  
// 分析失败,不是需要的数据报
    nRetMessageID = MESSAGE_OTHER_PACKET;  
// 其他的数据包

    nIPv4_HDRSize = (ipv4hdr->ip_verlen % 0x10) * 4;
    icmpv4hdr = (ICMPV4_HDR *)( (unsigned char *)ipv4hdr + nIPv4_HDRSize );

    nIPv4_HDRSize = (recv_ipv4hdr->ip_verlen % 0x10) * 4;
    recv_icmpv4hdr = (ICMPV4_HDR *)( (unsigned char *)recv_ipv4hdr + nIPv4_HDRSize );

    switch (recv_icmpv4hdr->icmp_type)
    {
    case 0:     
// 回送应答报文
        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;

    case 3:     
// 目的站不可达报文
    case 11:    
// 超时报文
        ptr1 = (unsigned char *)ipv4hdr;
        ptr2 = (unsigned char *)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;
    }

    return nRetResult;
}



六、依然没有解决的问题


    到目前所知,依然没有办法解决的问题只有一个:当目标地址是本机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报文,至今没有发现解决办法…… 希望正在看这篇文章的你能够帮助我解决这个问题。


    罗罗嗦嗦的写了这么多,希望您已经看明白了,如果还有不明白或者不认同的地方,欢迎通过文章开头提供的联系方式和我联系,共同讨论。

发表于 2004年06月25日 9:10 PM
img