iputils


1.1 iputils软件包简介
iputils软件包是linux环境下一些实用的网络工具的集合。一开始由Alexey Kuznetsov维护。



iputils包含以下几个工具:

1. ping。使用 ping可以测试计算机名和计算机的ip地址,验证与远程计算机的连接。ping程序由ping.c ping6.cping_common.c ping.h 文件构成

2. tracepath。与traceroute功能相似,使用tracepath测试IP数据报文从源主机传到目的主机经过的路由。tracepath程序由tracepath.c tracepath6.c traceroute6.c 文件构成。

3. arping。使用arping向目的主机发送ARP报文,通过目的主机的IP获得该主机的硬件地址。arping程序由arping.c文件构成。

4. tftpd。tftpd是简单文件传送协议TFTP的服务端程序。tftpd程序由tftp.h tftpd.c tftpsubs.c文件构成。

5. rarpd。rarpd是逆地址解析协议的服务端程序。rarpd程序由rarpd.c文件构成。

6. clockdiff。使用clockdiff可以测算目的主机和本地主机的系统时间差。clockdiff程序由clockdiff.c文件构成。

7. rdisc。rdisc是路由器发现守护程序。rdisc程序由rdisc.c文件构成。


1.2 本文简介
本文是在对源程序的分析的过程中写的总结文档。本文将依次对软件包中的程序进行介绍。介绍主要按照如下几个方面进行:



1. 在前言中简要介绍程序的基本用途、基本原理等。这是对于软件包中各软件的基本认识的总结。

2. 在程序使用中介绍程序的使用方法、使用选项等。这是对于使用软件包中程序的使用方法的介绍。

3. 在程序流程图中给出程序的基本流程。流程图的优点是比较能直观地给出程序的功能实现的流程,方便对程序有全局的掌握。然而不可避免地,流程图隐去了诸多的实现细节,所以如果要进一步分析程序,还需要进一步深入细节。

4. 介绍全局变量的含义、用途、变化等。全局变量是在程序中任何地方都可以访问的变量,所以分析全局变量有助于理解程序的数据变化流程。

5. 对于重要函数的介绍。一些重要的函数,不仅在程序的实现上占有重要的作用,而且理解起来有一定的难度,我觉得有必要进行分析。

6. 对于牵涉到的网络协议的介绍。阅读iputils源码的目的和好处之一就是帮助进一步理解网络协议。iputils涉及到IP、UDP、 ARP、RARP、ICMP、TFTP等不同层次的网络协议。本文将给出网络协议的基本介绍,主要是给出了网络报文的格式等内容。

7. 对于程序中重要的实现方法的介绍。而在iputils源码分析中,遇到了很多计算机网络方面的概念、思想、策略和机制。结合iputils的具体实现方法,本文将介绍计算机网络方面的相关知识。



  1. 对其他知识的介绍,例如基于linux的多线程编程或linux下socket编程的知识。在分析源码的过程中,不可回避地遇到了这些知识的使用。
    1.3 附件说明



本文基于的源码版本为iputils-s20071127。源代码可以在http://www.linux-ipv6.org/gitweb/gitweb.cgi?p=gitroot/iputils.git中下载。

在阅读和分析源代码过程中,对代码进行了大量的注释,附件可以在下载http://download.csdn.net/detail/fsdev/4498604。

为了能够编译通过,定义了rdisc.c需要使用但是源代码中没有定义的宏:

#define OPEN_MAX 10

这个宏的意义是程序所能够打开的最大的文件数目。rdisc程序在退到后台之后,需要关闭除了socket文件之外的所有文件,OPEN_MAX宏就是在这里使用的。为了能够输出测试信息,并尽量不修改原程序代码,定义宏:

#define lixiprintf printf

所有添加输出信息的部分都使用lixiprintf宏。除了加入注释和以上两个更改外,没有修改程序其他地方。


2.1 引言
“ping”这个名字源于声纳定位操作。Ping程序由Mike Muuss编写,目的是为了测试另一台主机是否可达。该程序发送一份ICMP回显请求报文给主机,并等待返回ICMP回显应答。



2.2 ping程序的使用
敲入命令:



[plain] view plain copy
lixi@lixi-desktop:~$ ping -V

ping utility, iputils-sss20071127

说明本机中安装的ping程序和本文研究的ping程序一样,是最新版本。



[plain] view plain copy
lixi@lixi-desktop:~$ ping -T tsonly www.ustc.edu.cn -c 1

PING www.ustc.edu.cn (202.38.64.9) 56(124) bytes of data.

64 bytes from 202.38.64.9: icmp_seq=1 ttl=62 time=0.795 ms

TS: 6123570 absolute

493

364

-857378

0

857378

-363

-493



— www.ustc.edu.cn ping statistics —

1 packets transmitted, 1 received, 0% packet loss, time 0ms

rtt min/avg/max/mdev = 0.795/0.795/0.795/0.000 ms

6123570是时间戳的绝对值,而输出的其他时间戳是相对上一个时间戳的差别。在北京时间9:42做的测试,北京时区为UTC+8,故此有9+42/60-8=1.7



而6123570/60/60/1000=1.7。故此出现这个结果是非常有道理的。



[plain] view plain copy
lixi@lixi-desktop:~$ ping www.ustc.edu.cn -R -c 1

PING www.ustc.edu.cn (202.38.64.9) 56(124) bytes of data.

64 bytes from 202.38.64.9: icmp_seq=1 ttl=62 time=0.852 ms

RR: lixi-desktop.local (210.45.74.25)

202.38.96.36

local-gw.ustc.edu.cn (202.38.64.126)

202.38.64.9

202.38.64.9

202.38.96.33

210.45.74.1

lixi-desktop.local (210.45.74.25)

对照上面的路由信息,我们就可以分析出为什么时间戳信息里会有对称的现象,而对称轴的值是0了。



产生对称性的另一个条件是RTT很小,这里只有0.8ms。



我们可以还可以分析出202.38.64.9的系统时间和其他路由的系统时间相差很大,大约有-14分钟。


[plain] view plain copy
lixi@lixi-desktop:~$ ping -T tsandaddr www.ustc.edu.cn -c 1

PING www.ustc.edu.cn (202.38.64.9) 56(124) bytes of data.

64 bytes from 202.38.64.9: icmp_seq=1 ttl=62 time=1.66 ms

TS: lixi-desktop.local (210.45.74.25) 7375300 absolute

210.45.74.1 828

local-gw.ustc.edu.cn (202.38.64.126) 26

202.38.64.9 -857405

Unrecorded hops: 3



— www.ustc.edu.cn ping statistics —

1 packets transmitted, 1 received, 0% packet loss, time 0ms

rtt min/avg/max/mdev = 1.664/1.664/1.664/0.000 ms

上面是同时记录路由和时间戳信息。不过这里由于IP选项长度的限制,只能存储4个路由和它对应的时间戳。
[plain] view plain copy
lixi@lixi-desktop:~$ ping -T tsprespec 202.38.64.9 202.38.96.33 210.45.74.1 www.ustc.edu.cn -c 1

PING www.ustc.edu.cn (202.38.64.9) 56(124) bytes of data.

64 bytes from 202.38.64.9: icmp_seq=1 ttl=62 time=0.893 ms

TS: 202.38.64.9 6741320 absolute

202.38.96.33 0

210.45.74.1 857353

Unrecorded hops: 1



— www.ustc.edu.cn ping statistics —

1 packets transmitted, 1 received, 0% packet loss, time 0ms

rtt min/avg/max/mdev = 0.893/0.893/0.893/0.000 ms

如果我们一定要得到以后的几个路由和时间戳,我们可以采用上述的办法。



这里在北京时间10:07分的时候进行的测试,202.38.64.9的时间戳是6741320,而我们上面的分析,202.38.64.9的系统时间大约比北京时间晚14分钟。10-8-14/60=1.87。6741320/60/60/1000=1.87。故此出现这个时间戳也是很有道理的。

ping程序的选项解释如下:

-a

可听见的ping。

所谓可听见,不过是在ping.c文件的parse_reply的函数中,输出ASCII码'/a',beep一下。


-A



    自适应的ping。调整报文间隔时间,使其适应于RTT,这样非常有效率地使得网络中传输的不超过一个(如果-l参数设置了就为多个)。对于非超级用户,最小的时间间隔为200毫秒,在一个RTT比较得下的网络中,这个模式和-f的洪泛模式基本相同。

为了调整报文时间间隔,使用update_interval()函数来调整时间间隔。

-b

允许ping广播地址。

设置broadcast_pings为1,当判断到这个选项设为1之后,且地址是广播地址,那么就设置setsockopt(icmp_sock, SOL_SOCKET, SO_BROADCAST,&broadcast_pings, sizeof(broadcast_pings))。

-B

不允许ping改变报文的源主机地址,该地址在ping开始运行的时候就已经指定了。

为了指定源主机地址,ping.c使用函数bind()来把套接字和本地套接字地址绑定,即在已创建的套接字上加上本地套接字地址。

-c <count>

在发送<count>个ECHO_REQUEST报文。当和-w <deadline>一起设置时,ping等待收到<count>个ECHO_REPLY报文,直到超出时间限制为止。

设置npackets选项为<count>就可以了。在没有设置deadline的情况下,当nreceived + nerrors >=npackets时就可以退出循环,完成ping的任务了。

-d

设置socket中的SO_DEBUG选项,使能调试跟踪。实质上Linux内核中没有使用这个套接字选项。

设置方法:setsockopt(icmp_sock,SOL_SOCKET, SO_DEBUG, (char *)&hold, sizeof(hold));

-F <flow> <label>

这个选项只有ping6才有。

-f

洪泛模式。对每一个ECHO_REQUEST报文的发送,打印一个“.”,当接受到ECHO_REPLY报文时,打印一个backspace字符。这样能够快速地表明网络丢失了多少个报文。如果interval没有设置,则设置interval为0,并按照报文接受的速度和一百次每秒的速度来发送报文(看哪个速度快)。只有超级用户能够和-i 0选项一起使用这个选项。

-i <interval>

在发送每个报文之间等待<interval>秒。默认设置是等待一秒,在洪泛模式下则不等待。只有超级用户才能将<interval>设置为小于0.2秒的数。

interval为<interval>*1000,程序的实现决定了<interval>输入整型数和浮点数都能被正确接受。

-I <interface/address>

设置发送的地址或者网络设备。

程序首先尝试用intinet_pton(int af, const char *src, void *dst)函数由将src代表的字符串转化为dst中的IP地址。如果不能正确转换,则意味着这个选项不是地址,例如210.45.74.25,而是设备名如eth0。如果是后者,则设置device为<interface>,并用bind(icmp_sock, (structsockaddr*)&source, sizeof(source)) setsockopt(probe_fd,SOL_SOCKET, SO_BINDTODEVICE, device, strlen(device)+1)来将套接字和本地套接字地址进行绑定。

-l <preload>

<preload>是在没有接受到回复报文之前能发送的最多报文。非超级用户最多只能设置为3。

尽可能快地发送预载的报文,然后再返回到正常发送模式。

将<preload>值赋到preload变量中。如果不赋值preload默认为1。

-L

禁止多播数据包的回环,只有在ping的目的主机是广播地址时才管用。

-n

只有数字形式ip地址值的输出,不通过查询DNS获知IP地址对应的主机名,以节省时间。

设置F_NUMERIC,不用调用gethostbyaddr来查询DNS主机名了。

用gethostbyaddr的由查询目的主机的IP地址。

-p <pattern>

允许为传输的回显报文中包含的内容指定字节模式。这对于诊断与传输数据有关的网络问题可能很有用。数据采用16进制,例如“-p ff”可将传输的报文填充为全1。

-Q <tos>

用来设置服务质量(Quality of Service )

例如最小开销、 可靠性、吞吐量、低延迟。

IP协议有一个8bit的DS区分服务(以前叫服务类型)。前三位是优先(precedence)字段(在目前,优先字段并未被大家使用),接着4bit是TOS位,最后1bit没有使用,但必须置0。

4比特TOS位的意义分别为D(最小时延)、T(最大吞吐量)、R(最高可靠性)、C(最小代价)。要设置TOS位为对应意义,可以设置-Q <tos>分别为0x10,0x08,0x04,0x02。TOS的各个位不能同时置1。

-q

静默模式。这种模式下,出了开始的提示和结束的数据统计,不会输出任何东西。

-R

记录路由信息。在发送的IP报文首部选项中放入记录路由选项,在接到到报文回复之后,打印出回复报文的路由信息。

注意:IP报文的选项中最多只能计算9个路由信息,计算方式如下:

首部长度HLEN。这4bit字段用来定义首部的长度,以4字节为单位。由于首部长度可变,默认长度是20字节,此时4bit字段值为5。4bit的字段最大可以表示的数为15,故此首部长度最大为15*4byte,即60byte。首部的可变字节数为60-20=40byte,RR选项用去3byte(参见记录路由选项的一般格式),只剩下37byte,最多只能放下9个IP地址。

注意:很多的主机会略过IP报文的路由选项,因此有可能在回复报文中没有路由信息。

注意:不能和-T选项一起使用。

-r

绕过一般的路由表而直接向一个连接着的主机发送报文。如果主机不是通过直接连接的网络相连,则会出现错误。这个选项可以用来ping一个没有通过路由相连而是通过一个接口相连(假设也使用了-I选项)的本地主机。

使用setsockopt函数设置套接字的SOL_SOCKET级别的SO_DONTROUTE选项即可。

-s <packetsize>

设置ICMP报文的数据部分的长度。默认值是56,和ICMP首部的8字节一起作为IP报文的数据部分。

设置datalen变量就可以了,datalen默认为DEFDATALEN(值是56)。长度为datalen的数据和8字节的首部一起作为ICMP报文。

-S <sndbuf>

设置套接字的发送缓冲区大小。如果没有设置,则被设定为不超过一个报文长度的长度。

-t <ttl>

设置TTL(time to live)。

使用setsockopt函数设置套接字的IPPROTO_IP级别的IP_MULTICAST_TTL和IP_TTL选项即可。

-T <timestamp> <option>

设置IP时间戳选项。时间戳选项可以是以下三种:

-T tsonly 只记录时间戳。

-T tsandaddr 收集时间戳和IP地址。

-T tsprespec [host1 [host2 [host3[host4]]]] 收集来自预定的网络段的时间戳和地址,发送端对选项列表进行初始化,存放了4个IP地址和四个取值为0的时间戳。只有在列表中的下一个地址和当前路由地址相匹配时,才记录它的时间戳。

与-R选项的分析类似,首部的可变字节数为60-20=40byte,选项用去4byte(参见时间戳选项的一般格式),只剩下36byte,最多只能放下9个时间戳。

注意:由于IP首部的空间限制,程序限制-R选项与-T不能同时使用。

-M <hint>

设定Path MTU查找选下项,可设置成下列三种:

-M do 不允许分段,甚至不允许在本地分段。

-M want 找出PMTU,在如果包太大就在本地分段。

-M dont 不要设置IP首部中的DF位,即允许分段。

使用setsockopt函数设置套接字的SOL_IP级别的IP_MTU_DISCOVER选项即可。

-U

-v

冗余输出。输出很多具体信息。

-V

打印ping的版本,然后退出。

-w <deadline>

设定时间期限为<deadline>秒,不管已经发送和接到了多少包,只要达到时间期限就结束ping的过程。

-W <timeout>

等待回复的时间,单位是秒。这个选项只在没有接到任何的回复的情况下有效,只要接到了一个回复,就将等待时间设置为两倍的RTT。如果没有设置,则等待时间设置为一个最大值。


2.3 ping程序的流程图
ping程序的流程图如下所示:



2.4 IP报文结构
IP报文结构如下所示:



IP数据报文的首部中有选项部分,这个部分可以用来存储IP时间戳或者IP记录路由选项。

存储IP时间戳,如下图所示:



IP记录路由选项,如下图所示:


2.5 ICMP报文结构
ICMP的封装方式如下图所示:



ICMP报文的结构如下图所示:


2.6 ICMP回显请求和回显应答报文格式
ICMP回显请求和回显应答报文格式如下所示:



2.7 ICMP报文类型列表
不同种类的ICMP报文的首部有所不同。如下:



类型



代码



描述



0



ICMP_ECHOREPLY



0



回显应答



3



ICMP_DEST_UNREACH



目的不可达



0



ICMP_NET_UNREACH



网络不可达



1



ICMP_HOST_UNREACH



主机不可达



2



ICMP_PROT_UNREACH



端口不可达



3



ICMP_PORT_UNREACH



协议不可达



4



ICMP_FRAG_NEEDED



需要进行分片单设置了不分片比特



5



ICMP_SR_FAILED



源站选路失败



6



ICMP_NET_UNKNOWN



目的网络不认识



7



ICMP_HOST_UNKNOWN



目的主机不认识



8



ICMP_HOST_ISOLATED



源主机被隔离(作废不用)



9



ICMP_NET_ANO



目的网络被强制禁止



10



ICMP_HOST_ANO



目的主机被强制禁止



11



ICMP_NET_UNR_TOS



由于服务类型TOS,网络不可达



12



ICMP_HOST_UNR_TOS



由于服务类型TOS,主机不可达



13



ICMP_PKT_FILTERED



由于过滤,通信被强制禁止



14



ICMP_PREC_VIOLATION



主机越权



15



ICMP_PREC_CUTOFF



优先权终止生效



ICMP_SOURCE_QUENCH



4



0



源端被关闭



ICMP_REDIRECT



5



重定向



0



ICMP_REDIR_NET



对网络重定向



1



ICMP_REDIR_HOST



对主机重定向



2



ICMP_REDIR_NETTOS



对服务类型和网络重定向



3



ICMP_REDIR_HOSTTOS



对服务类型和主机重定向



ICMP_ECHO



8



0



请求回显



9



0



路由器通告



10



0



路由器请求



ICMP_TIME_EXCEEDED



11



超时



0



ICMP_EXC_TTL



传输请见生存时间为0



1



ICMP_EXC_FRAGTIME



在数据包组装期间生存时间为0



ICMP_PARAMETERPROB



12



参数问题



0



坏的IP首部



1



缺少必需的选项



ICMP_TIMESTAMP



13



0



时间戳请求



ICMP_TIMESTAMPREPLY



14



0



时间戳应答



ICMP_INFO_REQUEST



15



0



信息请求



ICMP_INFO_REPLY



16



0



信息应答



ICMP_ADDRESS



17



0



地址掩码请求



ICMP_ADDRESSREPLY



18



0



地址掩码应答



pr_icmph()函数中分析ICMP报文类型,并针对错误报文打印出出错问题。惨照上表就能比较好地分析各种问题出现的大致原因了。

另外在rdisc.c文件中使用了ICMP的路由器通告报文(类型为9)和ICMP路由器请求报文(类型为10)。

各种ICMP类型和代码的常量定义在linux-2.6.27/include/linux/icmp.h文件中。


2.8 socket选项
程序中使用setsockopt()函数设定了套接字的选项。用到的选项如下:



level(级别)



optname(选项名)



说明



标志



SOL_SOCKET



SO_BROADCAST



允许或禁止发送广播数据



Ö



SO_ATTACH_FILTER



安装过滤器。



SO_SNDBUF



设置发送缓冲区的大小。



SO_RCVBUF



设置接收缓冲区的大小。



SO_DEBUG



打开或关闭调试信息



Ö



SO_DONTROUTE



打开或关闭路由查找功能。



Ö



SO_TIMESTAMP



打开或关闭数据报中的时间戳接收。



Ö



SO_SNDTIMEO



设置发送超时时间。



SO_RCVTIMEO



设置接收超时时间。



SO_BINDTODEVICE



将套接字绑定到一个特定的设备上。



SOL_RAW



ICMP_FILTER



设置套接字ICMP过滤选项。



IPPROTO_IP



IP_OPTIONS



设置发出的数据报中的IP选项



IP_MULTICAST_LOOP



多播API,禁止组播数据回送



Ö



IP_MULTICAST_TTL



多播API,设置输出组播数据的TTL值



IP_TOS



设置发出的数据报中的IP TOS



SOL_IP



IP_MTU_DISCOVER



为套接字设置Path MTU Discovery setting(路径MTU发现设置)



Ö



IP_RECVERR



允许传递扩展的可靠的错误信息



Ö



程序首先取得了一个UDP的套接字probe_fd,并根据用户的输入配置套接字的选项。probe_fd用到的选项主要有:SO_BINDTODEVICE、SO_BINDTODEVICE、SO_BROADCAST、IP_TOS等。

ICMP报文的套接字icmp_sock用到的选项除了SO_BINDTODEVICE选项以外,列表中的所有选项都用到了。


2.9 ping.c程序的全局变量的分析
static int ts_type;



    timestamp的类型

在-T选项中设置,可以设置为IPOPT_TS_TSONLY、IPOPT_TS_TSANDADDR或者IPOPT_TS_PRESPEC。

static int nroute = 0;

主机输入的总数,最多为9个,因为IP首部选项中最多能存储9个地址

static __u32 route[10];

在输入多个主机时,存储地址。

可能输入多个主机的情况是:-Ttsprespec [host1 [host2 [host3 [host4]]]] 选项,或者ping hostName1 hostName2 ... hostNameN;前者是想获得确定几个路由对应的时间戳,而后者为什么这么设置,我还不大明白 。

struct sockaddr_in whereto;

存储了目的主机的信息。

int optlen = 0;

ip选项的长度。

由IP的协议可知,最大为40,在需要在IP首部选项字段中存储数据时(例如-T、-R选项)就设置为最大值。

int settos = 0;

服务质量的设置。

可以用-Q选项用来设置服务质量,例如最小开销、 可靠性、吞吐量、低延迟。

IP协议有一个8bit的DS区分服务(以前叫服务类型)。前三位是优先(precedence)字段(在目前,优先字段并未被大家使用),接着4bit是TOS位,最后1bit好像没有使用。

4比特TOS位的意义分别为D(最小时延)、T(最大吞吐量)、R(最高可靠性)、C(最小代价)。

要设置TOS位为对应意义,可以设置-Q <tos>中的 <tos>分别为0x10,0x08,0x04,0x02 。

int icmp_sock;

ICMP的soket文件描述符。

u_char outpack[0x10000];

用来存储ICMP报文首部和数据的数组,为ICMP报文分配的存储空间。

int maxpacket = sizeof(outpack);

用来存储ICMP报文首部和数据的数组的最大大小。

static int broadcast_pings = 0;

标识用户是不是想ping广播地址。

可以通过-b选项设置。

如果不设置,则默认为0。

struct sockaddr_in source;

存储了源主机的信息。

如果-I选项后面带的是源主机地址而不是设备名的话,就将主机的信息存储在source中。在socket试探的连接成功后,程序还用getsockname重新确定了source的值。

char *device;

如果-I选项后面带的是设备名而不是源主机地址的话,如eth0,就用device指向该设备名。

该device指向一个设备名之后,会设置socket的对应设备为该设备。

int pmtudisc = -1;


2.10 ping_common.c程序的全局变量的分析
int options;



    存储各种选项的FLAG设置情况。

在判断输入选项时设置各个bit位。

int sndbuf;

发送缓冲区大小。

可以在-S <sndbuf>中设置,如果没有设置,则估计一个大小。

int ttl;

报文ttl的值。

可以在-t选项中设置。

在设置soket选项时设置IP广播报文TTL和IP报文的TTL都为ttl值。

int rtt;

用指数加权移动平均算法估计出来的RTT值。

初始值是0。

gather_statistics()函数中根据上次的RTT值和原来的rtt值加权得到新rtt的值。

在update_interva()函数中用来计算新的interval的值。

int rtt_addend;

配合rtt使用。

用来计算新的interval的值,似乎是更具上个rtt的值给interval留部分余量。

__u16 acked;

接到ACK的报文的16bit序列号。

在gather_statistics()函数里更新,实际的更新方法似的acked不超过0x7FFF,不然就会发生回绕。

int mx_dup_ck = MAX_DUP_CHK;



long npackets;

需要传输的最多报文数。

可以在-c 选项里设置。

如果没有设置则默认是0,故此每次在查询此值时就判断是否为0,0似乎作为无穷大来考虑。

long nreceived;

得到回复的报文数。

初始值是0。

在gather_statistics函数中递加,进行统计。在程序执行finsh时,使用这个变量,打印出来作为参考。

long nrepeats;

重复的报文数。

初始值是0。

在gather_statistics函数中递加,进行统计。在程序执行finsh时,使用这个变量,打印出来作为参考。

long ntransmitted;

发送的报文的最大序列号。

初始值是0。

在pinger函数中递加,进行统计。在程序执行finsh时,使用这个变量,打印出来作为参考。

long nchecksum;

checksum错误的恢复报文。

初始值是0。

在gather_statistics函数中,若csfailed为1的时候,则递加,进行统计。在程序执行finsh时,使用这个变量,打印出来作为参考。

不过似乎checksum是不会被改变的,因为gather_statistics的选项csfailed在唯一的一次调用中(parse_reply()函数中)为0。

long nerrors;

icmp错误数。

初始值是0。

在程序接受到出错的报文之后,就会调用receive_error_msg。在这个函数里如果判断确实是一个错误,错误有可能是本地出错,有可能是网络出错,不管是哪个出错,都将这nerrors递加。parse_reply也会改变这个变量。在程序执行finsh时,使用这个变量,打印出来作为参考。

int interval = 1000;

发送两个相邻报文之间相距的时间,单位为毫秒。

可以在-i选项中设置。

在设置-f的洪泛模式下,会设置interval为0。

如果没有设置,则默认是1000。

int preload;

在接受到第一个回复报文之前所发送的报文数。

可以通过-l <preload>选项设置。

如果没有设置,默认值是1。

int deadline = 0;

在deadline秒之后,程序退出。

可以由-w选项设置。如果设置了,则在setup函数中设置闹钟,当程序执行到deadline秒时产生SIGALRM中断,退出程序。


如果没有设置则默认值是0,程序运行没有时间限制。



int lingertime = MAXWAIT*1000;

等待回复的最长时间,单位为毫秒。

可以通过-W选项设置。这个值在完成一次正确发收过程后就由2*tmax代替,而失去作用了。

默认值是MAXWAIT*1000即10000,MAXWAIT定义在ping_common.h中。

struct timeval start_time;

程序运行开始时的主机时间。

在setup函数中使用gettimeofday初始化,在finish函数中和cur_time一起用来计算程序运行的时间。

struct timeval cur_time;

程序运行时当前的主机时间。

volatile int exiting;

程序是不是应该退出。

初始值是0,就是不应该退出。

在中断处理程序sigexit中会将这个值设为1。这个中断处理程序只在产生SIGALRM和SIGINT中断时(可以用Ctrl+c产生)才会执行。中断处理程序在setup函数中安装。

volatile int status_snapshot;

程序是不是应该调用status()函数打印出程序的运行状态。

初始值是0。

在中断处理程序sigstatus中会将这个值设为1。这个中断处理程序只在产生SIGQUIT中断时(可以用Ctrl+\产生)才会执行。中断处理程序在setup函数中安装。

int confirm = 0;

表明sendmsg函数的选项的MSG_CONFIRM选项是否设置。

如果设置MSG_CONFIRM,则会告诉链路层的传送有了进展:已经接受到对方的一个成功的答复。由于MSG_CONFIRM的这个意义,所以在发送第一个数据是MSG_CONFIRM选项不因该设置,即confirm初始值为0。在成功接受到一个回复之后,confirm则应该设置为MSG_CONFIRM了。只有在确定取得一个回复时才将confirm由0改为MSG_CONFIRM,这就是为什么confirm只有在gather_statistics()才会被改变的原因。然而更麻烦的是MSG_CONFIRM选项只有在Linux 2.3及以上内核中才支持,所以就需要confirm_flag变量了。

int confirm_flag = MSG_CONFIRM;

用来修补老版本linux内核的问题。

confirm_flag的初始值为MSG_CONFIRM。这样在gather_statistics()里confirm就更新为confirm_flag了。但是,如果由于设置MSG_CONFIRM而产生了发送错误(linux版本较老,不支持MSG_CONFIRM选项)。这样就会在下个循环里调用gather_statistics(),更新confirm变量,保证不会发送出错了。

int working_recverr;



int timing;

是否能够在ping过程中测算时间

如果ICMP报文的数据长度足以存储timeval结构数据,则timing设置为1。如果timing设置为1,则在ICMP报文中插入发送的时间,这样在接受到ICMP回复时,就可以根据该数据计算RRT。否则就无法计算RRT,也就无法进行时间统计了。

从根本上说timing的值由datalen变量的大小决定。

可以尝试运行ping -s1 www.ustc.edu.cn -c 1,看看运行结果怎样。

可以看到没有时间统计输出,因为-s选项设置的datalen值太小。

long tmin = LONG_MAX; /*minimum round trip time */

最小RRT

初始值为LONG_MAX,每次接受到回复报文之后,就在gather_statistics函数中本次RRT是不是比tin大,如果是,就更新tmin。在程序执行完成之后,将打印出这个信息作为参考。

long tmax;

最大RRT

初始值为0,每次接受到回复报文之后,就在gather_statistics函数中本次RRT是不是比tmax大,如果是,就更新tmax。在程序执行完成之后,将打印出这个信息作为参考。

此外tmax还作为每次发送报文后等待接受报文的时间长度的参考,见__schedule_exit函数。如果超出这个时间长度还没有完成一次发送和接受,则发生超时中断。

long long tsum; /*sum of all times, for doing average */

每次RRT之和。

初始值为0,每次接受到回复报文之后,就在gather_statistics函数中加上本次RRT。

用来计算平均RRT。

long long tsum2;

每次RRT的平方和。

初始值为0,每次接受到回复报文之后,就在gather_statistics函数中加上本次RRT的平方。

用来计算RRT的方差。

int pipesize =-1;

初始值为-1。

int datalen = DEFDATALEN;

数据长度。

初始值为DEFDATALEN,即56。

可以通过-s选项设置 。

char *hostname;

目的主机名字。

在开始的时候,由用户作为程序的选项输入。随后通过gethostbyname()函数由主机名得到主机,然后将主机名改为函数返回的官方主机名。

在最后输出的目的主机名就是这个名字。

int uid;

用户ID。

在main函数中通过getuid()取得。

如果uid不是0,即用户不是超级用户,则在设置选项的时候有限制:

-i<interval>,<interval>不得小于0.2;在ping广播地址时,<interval>不能设置为小于1的数。

-M<hint>,在ping广播地址时,<hint>不能设置为IP_PMTUDISC_DO之外的IP_PMTUDISC_DONT或IP_PMTUDISC_WANT。

-s<packetsize>, <packetsize>不能超过sizeof(outpack)-8。

-v,不会输出比较敏感的冗长信息,例如parse_reply函数中可能输出的额外信息。

-l<preload>,ping广播地址时,<preload>不能大于3。

-f,必须要和-i选项配合使用,且<interval>不小于0.2。

int ident;

本进程的ID。

在setup函数中通过getpid()取得。

在ICMP的数据中添加进程ID,并通过判断接受到的ICMP回复的进程ID是不是正确来判断ICMP回复是不是本进程的回复。

static int screen_width = INT_MAX;

窗口的宽度大小,也就是控制台一行能打印多少字符。

在setup函数中通过ioctl()取得。


2.11 重要函数的分析
int main(int argc, char **argv);



    主函数。

在这个函数里:取得用户输入的选项,并根据这些选项及其参数设置相应的标识和参数值。根据这些标识和参数值,首先连接(connect)一个探测的UDP报文,以探知目的地址的基本情况。然后设置ICMP报文的套接字选项,然后调用setup()函数来进一步设置与协议无关的套接字选项(与ping6公用)。在套接字设置好后,调用main_loop()函数完成探测。

定义在ping.c文件中。

void main_loop(int icmp_sock, __u8 *packet, intpacklen);

完成报文发送、分析的主要函数。

在这个函数里:一直调用pinger()函数发ICMP报文和调用recvmsg()函数接受报文。如果recvmsg()函数没有正确接受报文,调用receive_error_msg()函数处理接受到的ICMP差错报文。如此反复,直到用户要求终止或者报文发送次数达到要求,或者超出的程序的时间限制,程序才停止发送/接受;程序在停止发送/接受后,调用finish()函数打印出统计数据。

在main()函数中调用到此函数。

定义在ping_common.c文件中。在这个文件中的所有函数都能够被ping和ping6共同使用。

void int pinger(void);

构成并发送报文。

在这个函数里:调用send_probe()尝试发送报文,并处理send_probe()没有成功发送时出现的错误。在处理某些种类的错误时,用到receive_error_msg()函数。

在main_loop()函数中调用到此函数。

定义在ping_common.c文件中。

int send_probe()

构建报文,并发送报文。

在这个函数里:根据用户的参数设置,设置ICMP报文的类型、代码、序号、标识符,并往ICMP报文的选项数据部分添加发送时间,然后计算校验和。构建出这个ICMP报文后,调用sendmsg()函数发送ICMP报文。此函数不处理发送出错。

在pinger()函数中调用到此函数。

定义在ping.c文件中。

int receive_error_msg()

处理ICMP差错报文。

在这个函数里:调用设置了MSG_ERRQUEUE标识的recvmsg()来接收错误队列中的ICMP错误报文。取得错误信息之后,分析出错的原因是由于本地原因还是网络原因,并进行处理(比如设置更严格的ICMP过滤)。

在main_loop()函数和pinger()函数中调用到此函数。

定义在ping.c文件中。

void setup(int icmp_sock)

设置与协议无关的选项。

在这个函数里:根据用户设置,这些设置包括interval的设置,socket的是否打开调试信息(SO_DEBUG)、是否打开路由查找功能(SO_DONTROUTE)、是否打开数据报中的时间戳接收(SO_TIMESTAMP)、发送时间限制(SO_SNDTIMEO)、接受时间限制(SO_RCVTIMEO)等选项,往报文内填内容的设置,中断处理程序的设置,闹钟的设置等。

在main()函数和pinger()函数中调用到此函数。

定义在ping_common.c文件中。


2.12 时间间隔和报文预发机制的实现
程序使用一个分配时间片的概念,来控制发送报文的时间间隔,并实现在没有接到回复报文之前就预先发送preload个请求报文。



初始时分配interval*preload的时间片用来发送报文(程序中第一次发送设置时间片为interval*(preload-1),由于设置后没有减去第一次发送用去的interval时间片,所以相当于分配了interval*preload的时间片)。每次发送报文都要用掉interval毫秒的时间片。如果时间片不为负数的话,则一直持续发送报文。如果时间片为负数,则退出循环,开始处理接受到的回复报文。处理接受到的回复报文,会用去比较长的时间。

从上次发送报文,到当前准备发送报文的时间被计时器记录(实际上是通过记录上次发送报文的系统时间到当前系统时间之差来记录的),并作为新的时间片加入原时间片中,作为下次发送报文的时间片。为了确保没有接到回复而发送了的报文数目不会超过preload个,这个新的时间片如果超过interval*preload,则被改为interval*preload。如果新的时间片小于发送一个报文的时间interval,则仍然不发送报文,退出发送报文的循环,接受回复报文和处理可能出现的中断。

通过上述方法,实现了两个功能:

1. 可以在不等待回复的情况下,预先发送preload个报文。由于初始时分配的时间片为interval*preload,所以刚开始,程序就连续发送interval个请求报文;如果程序等了很长时间没有发送报文,则计时器的引入使得这一段时间也作为发送时间片的新的一部分,这样程序又可以连续发送几个报文。

2. 可以控制报文发送的时间间隔为interval。从初始时开始,在连续发送preload个报文后,时间片被耗尽。只有在计时器中累加的时间片超过interval时才能再连续发送一个或几个报文(不超过preload个)。

相关函数:

int pinger(void);

void main_loop(int icmp_sock, __u8 *packet, int packlen);

相关选项:

-l <preload>

-i <interval>


2.13 回复等待计时的实现
当用户使用-c 设置了需要传送/接受的报文数,且通过-w 设置了程序运行的时间,那么则程序只需要在发送个报文,并等待接受报文,直到接受到个回复或者程序运行时间超过限制为止。如果用户只使用-c设置了需要传送/接受的报文数,没有设置程序运行的时间,那么鉴于有些请求报文丢失而永远不会接到报文,程序不能在发送了个报文之后一直等待。程序一直等待一个可能再也不会出现的事情是难以接受的,它应该做的是在发送个请求报文后,等待一段时间,如果实在没有等到回复报文,就退出。



上面说的等待时间怎么确定呢?如果程序成功地收到了一个或者几个针对请求报文的回复,那么就将两倍的最大RTT作为等待的时间。如果程序没有接到任何的回复,RTT无从得知,就使用lingertime作为等待的最长时间。这个lingertime可以通过-W <timeout>选项由用户设置;如果用户没有设置则为一个常量(程序中,默认等待10秒)。不过值得主注意的是lingertime这个变量在程序成功地收到了回复之后,就没有任何作用了。

最长等待时间由一个闹钟实现。如上所述,设定这个闹钟的条件有下面几个:

1. 需要传送/接受的报文被设置了。

2. 程序运行的时间没有被设置。

3. 已经发送的报文数等于或大于需要传送/接受的报文数。

闹钟的时间被设置为:

1. 如果程序成功地收到了一个或者几个针对请求报文的回复,那么就将两倍的最大RTT作为等待的时间。

2. 否则,设置为lingertime。

当超出闹钟的时间之后,就会产生SIGALRM中断,使得程序退出。

相关函数:

void main_loop(int icmp_sock, __u8 *packet, int packlen);

staticinline int schedule_exit(int next);

schedule_exit(int next)

相关选项:

-c<count>

-w<deadline>


3.1 引言
在IP报文的首部和ICMP报文的首部都可以放入时间戳数据。clockdiff程序正是使用时间戳来测算目的主机和本地主机的系统时间差。



3.2 clockdiff程序的使用
[plain] view plaincopy
lixi@lixi-desktop:~$ ping -T tsandaddr www.ustc.edu.cn -c 1

PING www.ustc.edu.cn (202.38.64.9) 56(124) bytes of data.

64 bytes from 202.38.64.9: icmp_seq=1 ttl=62 time=0.823 ms

TS: lixi-desktop.local (210.45.74.25) 12522473 absolute

210.45.74.1 -251

local-gw.ustc.edu.cn (202.38.64.126) 248

202.38.64.9 -857514

Unrecorded hops: 3



— www.ustc.edu.cn ping statistics —

1 packets transmitted, 1 received, 0% packet loss, time 0ms

rtt min/avg/max/mdev = 0.823/0.823/0.823/0.000 ms

首先由上面的得出在RRT不大的时候,几个ICMP时间戳的关系。本地主机和202.38.64.9之间的时间差约为:-857514+248-251=-857517。



分别用-o(IP选项中时间戳)和不带选项(ICMP路由时间戳)上述路由的系统时间进行测试。



得到的结果:


[plain] view plaincopy
lixi@lixi-desktop:~# ./clockdiff -o 202.38.64.9

…………………………………………..

host=202.38.64.9 rtt=1(0)ms/1ms delta=-857517ms/-857517ms Wed Dec 17 11:28:30 2008



[plain] view plaincopy
lixi@lixi-desktop:~# ./clockdiff 202.38.64.9

.

host=202.38.64.9 rtt=750(187)ms/0ms delta=-857517ms/-857517ms Wed Dec 17 11:28:35 2008



两种方法测试的都比较准确.



[plain] view plaincopy
lixi@lixi-desktop:~#./clockdiff gigagate1.Princeton.EDU

…………………………………………..

host=gigagate1.Princeton.EDU rtt=307(21)ms/271ms delta=-5ms/-5ms Wed Dec 17 11:50:16 2008

上面是测试一个RTT较大的目的主机和本地主机的系统时间差。不过在使用clockdiff的时候,需要一点运气,因为很多路由会忽略ICMP或IP时间戳。



对clockdiff选项的解释如下:

-o

使用IP时间戳选项来测量系统时间差。时间戳只用3个。

-o1

使用IP时间戳选项来测量系统时间差。用4个时间戳。如果-o和-o1都没有设置,那么就是用ICMP时间戳来测试系统时间差。


3.3 clockdiff程序的流程图



3.4 clockdiff程序的主要函数的分析
int main(int argc, char *argv[]);



    主函数。

在这个函数里:取得用户输入的选项,并根据这些选项及其参数设置相应的标识和参数值。然后取得ICMP报文的套接字。如果设置-o或者-o1选项,设置IP报文的套接字时间戳选项,且调用measure_opt()函数来使用IP时间戳选项来测量本地主机和服务器主机的系统时间差。如果没有设置-o或者-o1选项,并调用measure()函数来使用ICMP时间戳报文来测量本地主机和服务器主机的系统时间差。测量完成后,打印出测试信息或者出错信息。

int measure_opt(struct sockaddr_in * addr);

使用IP时间戳选项来测量本地主机和服务器主机的系统时间差。

函数设置ICMP的报文,并发送出去。然后,程序接受ICMP报文,取得IP时间戳选项,并计算本地主机和服务器系统时间差。

int measure(struct sockaddr_in * addr);

使用ICMP时间戳报文来测量本地主机和服务器主机的系统时间差。

函数设置ICMP的报文,并发送出去。然后,程序接受ICMP报文,取得ICMP时间戳选项,并计算本地主机和服务器系统时间差。


6.5 clockdiff程序的全局变量的分析
int interactive = 0;



    标识标准输入输出是不是和一个终端相连,如果是就输出比较详细的信息,否则只输出必要数据。

例如clockdiff -o www.ustc.edu.cn >a.txt,就只会输出三个数据,因为标准输出被重定向到了文件的写入里,不与终端相连。

int id;

当前进程的ID,放在ICMP时间戳请求和应答报文中的标识符中。

在接受到ICMP回复报文时,用这个标识符来判断ICMP报文是不是本进程发出的ICMP报文的回复报文。

int sock;



int sock_raw;

ICMP报文的套接字。

struct sockaddr_in server;

服务器主机的地址。

要对比本机和目的主机的系统时间,目的主机就相当于一个服务器。

int ip_opt_len = 0;

ip_opt_len是ip选项中用来存储时间戳的长度

可以通过-o和-o1选项来设置。

如果选择-o选项,则ip_opt_len为4 + 4*8,也就是可以在IP选项中存储4个IP时间戳。时间戳的组织形式为:



如果选择-o1选项,则ip_opt_len为4 + 3*8,也就是可以在IP选项中存储3个IP时间戳。时间戳的组织形式为:



#define BIASP 43199999

程序通过计算时间戳中标明的时间来计算本地主机和服务器主机的系统时间差。在系统时间发生回绕的时候,会出现系统时间差的计算问题。这里BIASP就是为了解决这个问题。

在假设本地主机和服务器的系统时间差最多不超过12个小时(即43200000毫秒)的情况下:

对于主机发送报文,如果本地主机在发送报文的时刻,本地主机系统时间已经超过0点。而该报文到达服务器主机的时刻,服务器主机系统时间仍然没有超过0点,则两个时间戳的差值(接受时间减去发送时间)会大于BIASP毫秒。

同样,对于逆过程(主机接受服务器的报文),如果服务器主机在发送报文的时刻,服务器主机系统时间已经超过0点。而该报文到达本地主机的时刻,本地主机系统时间仍然没有超过0点,则两个时间戳的差值也会大于BIASP毫秒。

#define BIASN -43200000

与BIASP类似,BIASN也是为了解决系统时间回绕不一致的问题。

在假设本地主机和服务器的系统时间差最多不超过12个小时(即43200000毫秒)的情况下:

对于主机发送报文,如果本地主机在发送报文的时刻,本地主机系统时间没有超过0点。而该报文到达服务器主机的时刻,服务器主机系统时间已经超过0点,则两个时间戳的差值会小于BIASN毫秒。

同样,对于逆过程(主机接受服务器的报文),如果服务器主机在发送报文的时刻,服务器主机系统时间没有超过0点。而该报文到达本地主机的时刻,本地主机系统时间已经超过0点,则两个时间戳的差值也会小于BIASN毫秒。

为了解决系统时间回绕不一致的问题,当时间差不处在BIASN和BIASP之间的情况下,则将它们对应到这个去区间内。特别需要强调的是这里有基本假设:本地主机和服务器的系统时间差最多不超过12个小时(即43200000毫秒)。如果不满足这个假设,这种对应关系是错误的。

#define MODULO 86400000

24个小时就是86400000毫秒,与在BIASN和BIASP一起处理系统时间回绕问题。

#define PROCESSING_TIME 0

由于记录时间和报文发送的准确时间会有一定的偏差,所以这类处理过程消耗的时间可能会对最终计算出来的系统时间差会产生一个偏移量的影响。这里PROCESSING_TIME就是为了消除这个偏移量的影响的。这里忽略了这个偏移量。而且可以预见的是,想要分析和给出偏移量的影响大小并不容易,因为它与太多的变量有关系。

#define PACKET_IN 1024

接受报文的存储字节数。

int measure_delta;

计算的系统时间差。

计算的系统时间差有两种假设,measure_delta1是另一种假设下的计算结果。

int measure_delta1;

计算的系统时间差。

计算的系统时间差有两种假设,measure_delta是另一种假设下的计算结果。

static u_short seqno;

发送报文的序列号。

每次发送报文都设置ICMP报文的序列号为seqno,seqno递加。

static u_short seqno0;

发送报文的最小序列号。

当接受到报文时要判断ICMP报文的序列号是不是介于seqno0和seqno之间,否则将不认为这个ICMP报文是本程序的恢复报文。

static u_short acked;

接受到ICMP回复报文的最大序列号。

当接受到报文时,如果ICMP报文的序列号大于现在的acked,则更新acked。

long rtt = 1000;

对RTT的预测。

预测方法是使用指数加权移动平均。

和rtt_sigma一起用来设置超时时间,用的就是Jacobson/Karels算法。

long min_rtt;

RTT的最小值。

long rtt_sigma = 0;

对RTT预测的误差。

和rtt一起用来设置超时时间,用的就是Jacobson/Karels算法。


3.6 clockdiff程序RTT预测的实现
clockdiff程序使用Jacobson/Karels算法,使用以前的RTT实测值来预测下一次的RTT,并设定传输时间超时值。



Jacobson/Karels算法在[[1]]文中有介绍。伪代码如下:

Difference = SampleRTT - EstimatedRTT

EstimatedRTT = EstimatedRTT + (δ × Difference)

Deviation = Deviation + δ × (|Difference| - Deviation)

TimeOut = μ × EstimatedRTT + φ × Deviation

其中:

SampleRTT是测量所得的新的RTT数据。

EstimatedRTT是预测的RTT值。

Deviation是预测的偏差值。

TimeOut是超时时间值。

δ为0到1之间的常数。

μ和φ均是一个常数。

在clockdiff程序的实现中:

δ设置为1/4。

μ和φ均设置为1。

相关函数:

int measure_opt(struct sockaddr_in * addr)void main_loop(inticmp_sock, __u8 *packet, int packlen);

int measure(struct sockaddr_in * addr);


6.7 clockdiff程序系统时间差测量的实现
设两台主机的系统时间相差detaT,即源主机的系统时间为T的时刻,目的主机的系统时间为T+detaT。



通过ICMP时间戳或者IP选项时间戳,可以获得如下信息:

delta1:接受时间戳减去发起时间戳。

delta2:接到回复报文时间减传送时间戳。

时间戳的插入过程如下图所示:




由上图可以知道:

delta1 = (T + dataT + RTT/2) – T = RTT/2+detaT

delta2 = (T + RTT) - (T + dataT + RTT/2) =RTT/2-detaT

故此(delta1 - delta2) / 2就是两个主机之间的系统时间差。

由于一次测量的delta1和delta2可能会由于网络拥塞情况的变化而发生较大偏差,故此在实际的实现中多次测量求较优值。引入了如下几个变量:

min1:多次传送中delta1的最小值。

min2:多次传送中delta2的最小值。

min_rtt:多次传送中delta1+delta2的最小值。

PROCESSING_TIME:处理过程中所消耗的时间。

在基于以下的几个基本假设情况下,可以测算系统时间的差值:

1. RTT中发送到目的主机的时间和返回源主机的时间基本相等都为RTT/2。

2. 当min1最小时,min1是对RTT/2+detaT的较优预测。同样,当min2最小时,min2是对RTT/2-detaT的较优预测。故此,(min1 - min2)/2是对deltaT的较优预测。这种预测方法的系统时间差预测值存储为变量measure_delta。

3. 当min_rtt最小时,(delta1 - delta2)/2也是对deltaT的较优预测。这种预测方法的系统时间差预测值存储为变量measure_delta1。

4. 各主机从接受到报文到记录接受到报文时间,这两个时刻的时间间隔为可以忽略;即发送和接受报文的处理过程中所消耗的时间可以忽略。实际上PROCESSING_TIME正是用来消除由处理过程的时间造成的对于计算出来的系统时间差别的影响。不过这里PROCESSING_TIME设置为0,认为处理消耗时间可以忽略。

以上的假设决定了clockdiff测算出来的系统时间差别的不准确性。

相关函数:

int measure_opt(struct sockaddr_in * addr)void main_loop(inticmp_sock, __u8 *packet, int packlen);

int measure(struct sockaddr_in * addr); .1 引言
tracepath和更为强大和更为广泛使用的程序traceroute一样,可以让我们看到IP数据报从一台主机传到另一台主机所经过的路由。

tracepath的作者是Alexey Kuznetsov。


4.2 tracepath程序的使用
lixi@lixi-desktop:~$ tracepath 210.45.74.25/8888
1: lixi-desktop.local (210.45.74.25) 0.123ms pmtu 16436
1: lixi-desktop.local (210.45.74.25) 0.054ms reached
1: lixi-desktop.local (210.45.74.25) 0.045ms reached
Resume: pmtu 16436 hops 1 back 64
210.45.74.25是本地主机的IP地址,8888是选择的测试端口。



可以发现在本机进行了三次测试,为什么有三次测试,在下面的内容中有分析。


lixi@lixi-desktop:~$ tracepath 210.45.74.25/8888
1: lixi-desktop.local (210.45.74.25) 0.122ms pmtu 16436
1?: reply received 8)
1: lixi-desktop.local (210.45.74.25) 0.048ms reached
Resume: pmtu 16436 hops 1 back 64
编写简单的UDP服务程序,对8888端口的UDP请求进行服务(程序见<./test/udpserv.c>)。在运行这个服务程序之后,得到的测试结果如上。



在第二轮时程序接受到了UDP的程序,所以输出了一个'?'表示疑问。


lixi@lixi-desktop:~$ tracepath 210.45.74.25/44444
1: lixi-desktop.local (210.45.74.25) 0.131ms pmtu 16436
1: lixi-desktop.local (210.45.74.25) 0.054ms reached
1: lixi-desktop.local (210.45.74.25) 0.046ms reached
Resume: pmtu 16436 hops 1 back 64
在运行对8888端口进行服务的UDP服务程序时,如果tracepath采用其他端口就不会产生上例中的情况了。



lixi@lixi-desktop:~$ tracepath www.ustc.edu.cn
1: lixi-desktop.local (210.45.74.25) 0.198ms pmtu 1500
1: 210.45.74.1 (210.45.74.1) 0.777ms
1: 210.45.74.1 (210.45.74.1) 0.775ms
2: 202.38.96.33 (202.38.96.33) 1.068ms
3: 202.38.64.9 (202.38.64.9) 1.012ms reached
Resume: pmtu 1500 hops 3 back 253
对比此例和上例,可以发现PMTU发生了变化,由16436变成了1500。



lixi@lixi-desktop:~$ tracepath www.ustc.edu.cn -l 1500
1: 210.45.74.1 (210.45.74.1) 0.828ms
2: 202.38.96.33 (202.38.96.33) 0.988ms
3: 202.38.64.9 (202.38.64.9) 1.140ms reached
Resume: pmtu 1500 hops 3 back 253
我们将MTU手动设置为1500,程序就不会默认将MTU设置为一个很大的数,然后找出PMTU了。



tracepath程序的选项解释如下:

-n

与ping命令的-n选项差不多。

只有数字形式的输出,不查找DNS主机以节省时间,不查寻主机名,仅仅给出ip地址值。

只要设置了F_NUMERIC,就不用调用gethostbyaddr来查询DNS主机名了。

用gethostbyaddr的由查询目的主机的IP地址。

-l

设置初始的包的大小。如果不设置则,则报文的大小为65535


4.3 tracepath程序的流程图
tracepath程序的流程图如下:



深入理解iputils网络工具第4篇 tracepath:路由追踪程序



4.4 tracepath重要函数的分析
int main(int argc, char argv);



    接受用户的选项,设置发送MTU或者设置是否不要验证主机名等标识。取得一个UDP类型的套接字,并设置好这个套接字的选项。,从1开始递增,直至31,设置套接字的IP_TTL选项为不同的值,调用probe_ttl()函数,直到probe_ttl()函数告知找到目的主机或者出现严重的错误为止。

int probe_ttl(int fd, int ttl);

循环执行十次如下操作,直到正确发送报文跳出循环,或者recverr()返回0:

调用sendto()函数尝试发送UDP报文到目的地址,如果发送出现错误则调用recverr()函数处理接受到的ICMP差错报文。如果正确发送报文,则跳出循环。

如果循环过程中:recverr()函数返回0,则本函数返回0;如果recverr()函数返回大于0的数,则重新进行如上循环。

如果循环超过十次,则表明因为某种原因,无法发送UDP报文,程序返回0。

如果正确发送了UDP报文,则尝试使用recv()函数接受UDP报文的。正常情况下不会有UDP的,如果果真接受到了,则打印‘?’号表示吃惊,返回0。如果正如所预见到的,没有接到,则调用recverr()函数处理可能接受到的ICMP差错报文,返回recverr()返回的数值。

总结本函数的返回值意义如下:

返回0表示找到主机或者有严重的错误。

返回负数-1,表示没有接受到ICMP差错报文。

返回正数是当前MTU,表示接受并处理了一些错误,但是还没有找到目的主机。

int recverr(int fd, int ttl);

函数将progress初始化为-1,然后不断循环执行如下操作,直到循环中函数返回:调用recvmsg()函数接受错误报文,并处理错误,如果没有错误返回progress。在错误队列中查找对应错误(SOL_IP级别IP_RECVERR类型),progress设置为MTU的值,并处理错误。如果错误队列的错误不是对应错误,返回0。

并处理错误的几种情况如下:

如果是MTU太大(EMSGSIZE),则修改MTU的变量值,继续循环。

如果是UDP端口不可达错误(ECONNREFUSED),则UDP报文已经在规定TTL内传送到了目的主机。这种情况返回0。

如果是EHOSTUNREACH错误且出错原因是接受到了ICMP差错报文,这个ICMP差错报文如果类型为11,代码为0,则表示因为在传输期间TTL等于0所以出错(参看ICMP报文类型)。这就说明UDP还没有到达目的主机TTL就变成了0,需要进一步递加TTL进行试探。

如果是其他的错误,对于不严重的错误继续循环,否则返回0。

总结本函数的返回值意义如下:

返回0表示找到主机或者有严重的错误。

返回的数如果大于0,其值是当前的MTU,表示接受并处理了一个或几个错误。

返回负数-1,表示没有发现任何错误,也就是没有进展(progress)。


4.5 tracepath全局变量的分析
struct hhistory his[64];



    用来存放历史上发出的UDP报文的ttl设置值和发送时间。

当发送UDP报文时,将报文的端口号设置为base_port + hisptr,在his[hisptr]元素中,存储该UDP报文的ttl设置值和发送时间。

当接受到一个UDP的报文(“端口不可达”错误ICMP报文)时,通过ICMP报文的端口号,就可以知道该UDP报文对应的ttl设置值和发送时间存储在his数组的哪个元素里了。

his数组大小有限,只有64个元素。不过已经能够保证即使hisptr回绕也不会出错了。

int hisptr;

用来指向his数组的元素。

当发送UDP报文时,hisptr递加,将发出的UDP报文的ttl设置值和发送时间存储在his[hisptr]元素中。

由于his数组大小为64,故此hisptr每次加到63,下一次就会回绕到0。

struct sockaddr_in target;

要查询的目的主机的地址。

包括地址种类(IPv4)、IP地址、端口号等信息。

端口号会被设置为基础端口号加上hisptr。

__u16 base_port;

基础端口号

可以在设定目的主机时连带设定,否则程序默认是44444端口。

基础端口号加上hisptr就是UDP报文的发送端口号。

设置这么大的端口号,是为了使得目的主机的任何一个应用程序都不可能使用该端口,而产生一个“端口不可达”错误。

这个值很大,目的就是让UDP出现“端口不可达”错误。

const int overhead = 28;

在UDP数据部分之前的头部大小。

IP首部为20,UDP首部为8,故此总共为28字节。

这个数是个常量。

int mtu = 65535;

可以通过-l选项设置,如果设置的值不大于传输路径的MTU。

如果不设置默认值是65535,这个默认值肯定会超过传输路径的MTU,当超过了路径的MTU时,程序会受到错误消息,并根据这个错误消息所带的MTU值,更新MTU值。这样就tracepath能找出路径的MTU了。

int hops_to = -1;

从本地主机到目的主机的跳数。

如果目的主机不可达,则hops_to一直维持-1,最后就不会输出hops_to。

当程序接受到目的主机发出的“拒绝服务”ICMP错误报文时,就说明探测到了目的主机。hops_to取为此时的recverr()函数局部变量sndhops的值。

sndhops有两种方式取得,一种是取得发送时存储在his数组中的ttl值(前面已经谈到如何通过错误报文的IP端口得到存储地址),另一种是取得当前探测阶段发送的UDP报文的ttl的值。

如果不出意外,第一种方式能比较可靠地取得;但是由于某种原因,前一种方式出问题后,就用后一种方式作为代替。

int hops_from = -1;

从目的主机到本地主机的剩余TTL值。

如果目的主机不可达,则hops_from一直维持-1,最后就不会输出。

hops_from从目的主机发送给本地主机的IP报文头取得TTL字段值即可。

int no_resolve = 0;

标识是否不要验证主机名。

可以通过-n选项设置为1。

如果设置为0,就调用gethostbyaddr的由查询目的主机的IP地址,否则就不用,以节省时间。 5.1 引言
ARP协议是“Address Resolution Protocol”(地址解析协议)的缩写。在同一以太网中,通过地址解析协议,源主机可以通过目的主机的IP地址获得目的主机的MAC地址。arping程序就是完成上述过程的程序。

ARP协议可以参看RFC 826。


5.2 arping程序的使用
敲入命令:



[plain] view plain copy
lixi@lixi-desktop:~/temp/iputils/iputils-s20071127$ arping 210.45.74.29 -c 1 -D

ARPING 210.45.74.29 from 0.0.0.0 eth0

Unicast reply from 210.45.74.29 [00:40:D0:59:CD:D3] 0.684ms

Sent 1 probes (1 broadcast(s))

Received 1 response(s)

在本地主机的局域网内有一台IP地址为210.45.74.29的主机,所以会接到一个回复。



[plain] view plain copy
lixi@lixi-desktop:~$ arping 210.45.74.28 -c 1 -D

ARPING 210.45.74.28 from 0.0.0.0 eth0

Sent 1 probes (1 broadcast(s))

Received 0 response(s)

向一个不存在的IP发送报文不会接受到回复。



[plain] view plain copy
root@lixi-desktop:~# arping 210.45.74.25 –U

root@lixi-desktop:~# tcpdump arp -n | grep 210.45.74.25

得到输出结果如下:



[plain] view plain copy
11:03:13.848653 arp who-has 210.45.74.25 (ff:ff:ff:ff:ff:ff) tell 210.45.74.25

这里就是一个免费ARP的例子。



-A

与-U选项类似,但是发送的是ARP 回复报文,而不是ARP请求报文。

-b

只发送MAC级别的广播。一般的arping开始时发送广播,在接受到回复后开始发送单播。

-c <count>

在发送count个ARP请求后就退出。在和deadline选项一起使用时,arping程序一直等到收到count个ARP回复报文或者时间消耗完毕时才退出。


-D



    重复地址检测模式(DAD,Duplicate  address detection  mode)。参见RFC2131,4.4.1。如果DAD成功,则返回0,即不会接受到没有任何回复。

-f

在接受到第一个确定目标主机存在的回复之后,就结束程序,否则一直发送ARP请求。

-I <interface>

设置网络设备的名字,这个名字就是发送ARP请求报文的设备名字。

-h

打印帮助信息,然后退出。

-q

静默输出,不打印探测结果。

-s <source>

在ARP报文中使用的IP源地址。如果这个选项没有设置,则源地址设置方法为:

1. DAD模式下(-D选项),设置为0.0.0.0。

2. 在主动ARP模式(-U或者-A选项),设置为目的地址。

3. 其他情况下,通过路由表得到。

-U

为了更新以太网邻居的ARP快速缓存而主动进行的ARP。也就是免费ARP(gratuitous ARP)。

-V

打印出版本信息,然后退出。

-w deadline

设定时间期限为<deadline>秒,不管已经发送和接到了多少包,只要达到时间期限就结束ping的过程。在这种情况下,这样arping程序只有在接受到cout个回复或者deadline的时间消耗完后才退出;而不是像只有-c选项的情况,在发送count个ARP请求的就退出。


5.3 arping程序的流程图
arping程序的流程图如下所示:



5.4 ARP报文的分组格式
ARP报文的分组格式如下图所示:



5.5 arping程序的全局变量的分析
int quit_on_reply=0;



    标识是否在接受到一个回复之后,就马上退出程序。

可以在-f选项和-D选项中设定为非0值(同时有-f、-D选项或者有多个同种选项)。

char *device="eth0";

源主机的网络设备号。

可以通过-I参数设置。

默认为eth0。

(setsockopt(probe_fd, SOL_SOCKET,SO_BINDTODEVICE, device, strlen(device)+1)。

不过好像只有超级用户这个才能执行。

int ifindex;

Interface number。

char *source;

存储-s设置的源地址。

地址的形式可以是IPv4的标准数字和点组成的形式,如210.45.74.25;也可以是主机名字的形式,如www.ustc.edu.cn。

struct in_addr src;

存储源IP地址,即对ARP的回复报文所要发往的主机的IP地址,有可能是广播地址。

可以通过-s选项设置。如果这个选项没有设置,则源地址设置方法为:

1. DAD模式下(-D选项),设置为0.0.0.0。

2. 在主动ARP模式(-U或者-A选项),设置为目的地址。

3. 其他情况下,通过路由表得到。

struct in_addr dst;

存储目的IP地址,即ARP报文所要发往的主机的IP地址。

char *target;

存储用户设置的目的地址,地址的形式必须是IPv4的标准数字和点组成的形式。

int dad;

标识是不是DAD模式。

如果是DAD模式,则原源主机地址一直没有设置,那么就意味着源地址为0.0.0.0。这样当目的主机接到之后,就会向0.0.0.0发送回复,就相当于广播给以太网中所有的主机。因为进行D重复地址检测模式的原因很可能是由于源主机的IP地址没有设置,从而想设置自身的IP地址。在IP地址没有设置的时候,主机只能接受到地址为0.0.0.0的广播信号。

可以通过-D参数设置。

int unsolicited;

标识是不是发送免费ARP。

在-A选项和-U选项中设置unsolicited为1。

int advert;

标识在免费ARP模式下发送的是ARP回复报文,而不是ARP请求报文。

在-A选项中设置advert为1。

int quiet;

标识是否静默输出。

可以通过-q选项设置。

int count=-1;

发送ARP的个数。

可以通过-c选项设置,如果不设置,默认值为-1,即没有个数限制(回绕成0基本不可能)。

int timeout;

程序运行的时间限制。

通过-w选项设置。

int unicasting;

标识是不是应该发送单播报文。

在程序接受到一个ARP的回复之后,已经能够知道回复者的IP地址了,这时候就可以不广播,而设置传播地址。因此,在接受到ARP回复之后,如果broadcast_only没有被设置,unicasting就应该设置为1,以让下次进行单播。

int s;

ARP报文的套接字。

int broadcast_only;

标识是不是一直发送广播报文,而不在接受到一个回复以后就改成单播报文。

通过-b选项可以设置broadcast_only为1。

struct sockaddr_ll me;

存储本地主机的信息,包括本地主机的以太网地址、硬件地址的类型、硬件地址长度和协议地址长度等信息

struct sockaddr_ll he;

存储本地主机的信息。

struct timeval start;

程序发送第一个报文的系统时间。

记录这个时间,可以用来判断程序是否超出时间限制。如果当前的系统时间减去start超过用户设置的时间限制有500毫秒,则程序退出。

struct timeval last;

程序发送上一个报文的系统时间。

记录这个时间,可以用来判断是否应当发出下一个ARP请求。如果当前系统时间减去last超过500毫秒,则发出下一个ARP请求。

int sent;

程序发送的ARP报文数量。

每次在发送ARP报文之后递加。

int brd_sent;

程序广播的ARP报文数量。

每次在发送ARP报文之后,如果ARP报文是广播报文,则递加。

int received;

程序接受的ARP报文数量。

每次在接受到正确的ARP报文之后,递加。

int brd_recv;

程序接受的ARP广播报文数量。

每次在接受到正确的ARP报文之后,如果报文不是单播报文则递加。

int req_recv;

程序接受到ARP请求报文数量。

每次在接受到正确的ARP报文之后,如果报文是ARP请求报文则递加。 7.1 引言
TFTP ( Trivial File Transfer Protocol)即简单文件传送协议,是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供简单的、低开销的文件传输服务。tftpd程序就是进行tftp服务的服务程序。


TFTP协议可以参看RFC 1350。



7.2 tftpd程序的使用
由于这个程序需要inetd程序的配合,而环境比较难搭建,所以对程序的测试比较困难。



7.3 tftpd程序的流程图



7.4 TFTP报文格式
TFTP报文格式如下所示:



7.5 tftpd.c程序的全局变量的分析
int peer;



    服务连接的套接字。

int rexmtval =TIMEOUT;

程序采用停止和等待的自动请求重发(ARQ)算法,当接受ACK报文或者数据报文的时间超过rexmtval,则认为接受超时,重新开始接受报文过程。

rexmtval一直维持TIMEOUT的值,没有被改变过。

int maxtimeout =5*TIMEOUT;

当接受数据报文或者ACK报文的时候,如果出现超时,则会进入中断处理程序。如果中断次数过多,则timeout会累加rexmtval时间。一旦超时中断过多,导致timeout超过maxtimeout,则程序退出,停止服务。

maxtimeout一直维持5*TIMEOUT的值,没有被改变过。

#define PKTSIZE SEGSIZE+4

如果TFTP报文的操作码是data,表明传输的是0到512字节的数据。

SEGSIZE是TFTP报文的数据的最大长度,即512字节。

由于TFTP报文还包括2字节的操作码和2字节的块编号,所以TFTP数据报文的长度为SEGSIZE+4。

char buf[PKTSIZE];

缓冲空间,在以下的情况下,作为存储TFTP报文的内存空间:

1. 清楚初始时接收的报文。

2. 发送操作码为error类型的TFTP报文。

3. 在文件传输完毕时(上一次接受到的数据不足512字节),尝试接受操作码为data数据类型的TFTP报文,因为服务器传给用户主机的最后一个ACK有可能丢失。

char ackbuf[PKTSIZE];

缓冲空间,在以下的情况下,作为存储TFTP报文的内存空间:

1. 接受操作码为ACK类型的TFTP报文。

2. 发送操作码为ACK类型的TFTP报文。

union {

struct sockaddr sa;

struct sockaddr_in sin;

struct sockaddr_in6 sin6;

} from;

描述客户连接的地址。

socklen_t fromlen;

from所占的内存空间大小。

#define MAXARG 1

在启动tftpd程序的时候,需要指定ftp文件夹的路径。MAXARG是所能指定文件夹的个数。

char *dirs[MAXARG+1];

dirs[0]里保存了ftp文件夹的路径。

int confirmed;

表明sendmsg函数的选项的MSG_CONFIRM选项是否设置。

如果设置MSG_CONFIRM,则会告诉链路层的传送有了进展:已经接受到对方的一个成功的答复。由于MSG_CONFIRM的这个意义,所以在发送第一个数据是MSG_CONFIRM选项不因该设置,即confirm初始值为0。在成功接受到一个回复之后,confirm则应该设置为MSG_CONFIRM了。

int timeout;

表示由于等待接受超时的时间总和。

当接受数据报文或者ACK报文的时候,如果出现超时,则会进入中断处理程序,将timeout递加rexmtval秒,如果。一旦超时中断过多,导致timeout超过maxtimeout,则程序退出,停止服务。

jmp_buf timeoutbuf;

在发生等待接收超时时,应当将要发送的报文重新发送(报文可能为ACK报文或者data报文)。setjmp()和longjmp()函数就可以用来实现这种跳转的功能。

由于等待超时时会进入计时器中断处理程序,在中断处理程序中调用longjmp()函数来跳转到最后一次用setjmp()设置timeoutbuf的地方运行,也就是重新进行报文的发送。

timeoutbuf就记录了调用setjmp()的时候的程序上下文。


7.6 tftpsub.c程序的全局变量的分析
struct bf {



    int counter;

charbuf[PKTSIZE];

} bfs[2];

缓冲空间,在以下的情况下,作为存储TFTP报文的内存空间:

1. 接受操作码为data类型的TFTP报文。

2. 发送操作码为data类型的TFTP报文。

counter用来标识存储的缓冲空间的数据是一下三种的哪一种:

1. BF_ALLOC,标识是已经申请的存储空间。

2. BF_FREE,标识存储空间没有使用。

3. 大于0的数,标识里面已经存储数据。

static int nextone;

待使用的下一个缓冲的标号。

static int current;

正在使用的当前缓冲的标号。

int newline = 0;

在数据传输是按照8位的ASCII码形式(netascii)组织的情况下,标识是不是有新的行出现。

在顺次读取或者写入字节流时,如果遇到'\n'或者'\r'字符都会设置newline为1,方便进行特殊处理。

int prevchar = -1; /* putbuf: previous char (cr check) */

在数据传输是按照8位的ASCII码形式(netascii)组织的情况下,记录上个处理的字符。

和newline一样,prevchar是为了处理'\r'或者'\n'的特殊字符。

处理的效果是:

1. 如果要发送'\r'字符则传送的实际是\r\0";如果要发送'\n'字符则传送的实际是"\r\n"。

2. 如果接受到"\r\n",则保存的实际是字符'\n';如果接受到"\r\0",则保存的实际是字符'\r'。

Category linux