内容比较多,目录如下
Ping是Windows、Unix和Linux系统下的一个命令。ping也属于一个通信协议,是TCP/IP协议的一部分。利用“ping”命令可以检查网络是否连通,可以很好地帮助我们分析和判定网络故障。ping程序是基于ICMP协议实现的。
Ping命令常见格式:ping [-t] [-a] [-n count] [-l length] [-f] [-i ttl] [-v tos] [-r count] [-s count] [[-j -Host list] | [-k Host-list]] [-w timeout] destination-list
。
ICMP是(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议簇的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。
ICMP报文主要分为两类:一类是差错报文,一类是查询报文。
查询报文:
YPE | CODE | Description |
---|---|---|
0 | 0 | Echo Reply——回显应答(Ping应答) |
8 | 0 | Echo request——回显请求(Ping请求) |
YPE | CODE | Description |
---|---|---|
13 | 0 | Timestamp request (obsolete)——时间戳请求(作废不用) |
14 | 0 | Timestamp reply (obsolete)——时间戳应答(作废不用) |
YPE | CODE | Description |
---|---|---|
9 | 0 | Router advertisement——路由器通告 |
10 | 0 | Route solicitation——路由器请求 |
YPE | CODE | Description |
---|---|---|
17 | 0 | Address mask request——地址掩码请求 |
18 | 0 | Address mask reply——地址掩码应答 |
差错报文:
YPE | CODE | Description |
---|---|---|
3 | 0 | Network Unreachable——网络不可达 |
3 | 1 | Host Unreachable——主机不可达 |
3 | 2 | Protocol Unreachable——协议不可达 |
3 | 3 | Port Unreachable——端口不可达 |
3 | 4 | Fragmentation needed but no frag. bit set——需要进行分片但设置不分片比特 |
3 | 5 | Source routing failed——源站选路失败 |
3 | 6 | Destination network unknown——目的网络未知 |
3 | 7 | Destination host unknown——目的主机未知 |
3 | 8 | Source host isolated (obsolete)——源主机被隔离(作废不用) |
3 | 9 | Destination network administratively prohibited——目的网络被强制禁止 |
3 | 10 | Destination host administratively prohibited——目的主机被强制禁止 |
3 | 11 | Network unreachable for TOS——由于服务类型TOS,网络不可达 |
3 | 12 | Host unreachable for TOS——由于服务类型TOS,主机不可达 |
3 | 13 | Communication administratively prohibited by filtering——由于过滤,通信被强制禁止 |
3 | 14 | Host precedence violation——主机越权 |
3 | 15 | Precedence cutoff in effect——优先中止生效 |
YPE | CODE | Description |
---|---|---|
4 | 0 | Source quench——源端被关闭(基本流控制) |
YPE | CODE | Description |
---|---|---|
11 | 0 | TTL equals 0 during transit——传输期间生存时间为0 |
11 | 1 | TTL equals 0 during reassembly——在数据报组装期间生存时间为0 |
YPE | CODE | Description |
---|---|---|
12 | 0 | IP header bad (catchall error)——坏的IP首部(包括各种差错) |
12 | 1 | Required options missing——缺少必需的选项 |
YPE | CODE | Description |
---|---|---|
5 | 1 | Redirect for host——对主机重定向 |
5 | 2 | Redirect for TOS and network——对服务类型和网络重定向 |
5 | 3 | Redirect for TOS and host——对服务类型和主机重定向 |
不同的ICMP报文有着不同的报文结构,但是都有着如下的共同结构:
ICMP协议是基于IP协议的。ICMP报文被封装在IP报文的数据段中,其封装原理图如下:
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。
函数int socket(int domain, int type, int protocol);
用于创建一个新的socket。
参数说明:
domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址。
type:指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。
protocol:指定协议。
返回值:
函数int bind(SOCKET socket, const struct sockaddr address, socklen_t address_len);
用于将套接字文件描述符绑定到一个具体的协议地址。
参数说明:
socket:是一个套接字描述符。
address:是一个sockaddr结构指针,该结构中包含了要结合的地址和端口号。
address_len:确定address缓冲区的长度。
返回值:
使用int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
函数来对socket选项进行设置。
参数说明:
返回值:
该程序中使用了的选项值如下:
level 级别 | 选项名字 | 说明 |
---|---|---|
SOL_SOCKET | SO_BROADCAST | 允许或禁止发送广播数据 |
SOL_SOCKET | SO_DEBUG | 打开或关闭调试信息 |
SOL_SOCKET | SO_DONTROUTE | 打开或关闭路由查找功能。 |
SOL_SOCKET | SO_TIMESTAMP | 打开或关闭数据报中的时间戳接收。 |
IPPROTO_IP | IP_MULTICAST_LOOP | 多播API,禁止组播数据回送 |
SOL_IP | IP_MTU_DISCOVER | 为套接字设置Path MTU Discovery setting(路径MTU发现设置) |
SOL_IP | IP_RECVERR | 允许传递扩展的可靠的错误信息 |
socket使用ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);
函数来发送消息
参数说明:
返回值:
socket使用ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
函数来接收消息
参数说明:
返回值:
分析的源码选自Linux环境下的iputils。iputils包是一组用于Linux网络的小型实用工具。它最初由亚历克赛·库兹涅佐夫维护。
本此源码分析选择了iputils-s20121221版本的中的代码作为分析对象。获取来源为github开项目。其地址为https://github.com/dgibson/iputils
与ping程序有关的主要为以下4个文件,其各自的名字及功能简介如下:
ping.c // ipv4 下的ping程序
ping6.c // ipv6 下的ping程序
ping_common.c //ipv4与ipv6共有的 与协议无关的共同代码
ping_common.h //ping_common.c的头文件
本次实验以ipv4下的ping程序下的完整流程为分析对象,重点了解ping程序的完整流程以及学习其中网络编程的方法。
代码的逻辑功能主要分为1. 使用UDP报文对目标主机进行一个connetc尝试 2. 时候使用循环发送ICMP报文并对回复报文进行处理 这两个主要部分。当遇到中断等条件时候打印信息后进行退出。
用来表示socket的通信地址,其与sockaddr的区别是将端口和地址进行了区别。
struct sockaddr_in {
sa_family_t sin_family; //地址种类 AF_INET代表IPV4
u_int16_t sin_port; //端口
struct in_addr sin_addr; //地址
}
struct in_addr {
u_int32_t s_addr; // 地址是按照网络字节顺序排列的
};
hostent是host entry的缩写,该结构记录主机的信息,包括主机名、别名、地址类型、地址长度和地址列表。
struct hostent
{
char *h_name; //正式主机名
char **h_aliases; //主机别名
int h_addrtype; //主机IP地址类型:IPV4-AF_INET
int h_length; //主机IP地址字节长度,对于IPv4是四字节,即32位
char **h_addr_list; //主机的IP地址列表
};
I/O vector,与readv和wirtev操作相关的结构体。readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。其主要用于发送和接收ICMP报文中。
struct iovec {
ptr_t iov_base; //开始地址
size_t iov_len; //长度
};
在套接字发送接收系统调用流程中,send/recv,sendto/recvfrom,sendmsg/recvmsg最终都会使用内核中的msghdr来组织数据。其主要用于发送和接收ICMP报文中。
struct msghdr {
void *msg_name; //指向socket地址结构
int msg_namelen; //地址结构长度
struct iov_iter msg_iter; //数据
void *msg_control; //控制信息
__kernel_size_t msg_controllen; //控制信息缓冲区长度
unsigned int msg_flags; //接收信息的标志
struct kiocb *msg_iocb; //异步请求控制块
};
时间结构体,用来记录时间。其在本程序中主要用于接受报文部分中。
struct timeval
{
__time_t tv_sec; //秒
__suseconds_t tv_usec; //微秒
};
用来表示ICMP报文的头部信息。其主要用于处理ICMP差错报文部分中。
struct icmphdr
{
u_int8_t type; //报文类型
u_int8_t code; //类型子代码
u_int16_t checksum; //校验和
union
{
struct
{
u_int16_t id;
u_int16_t sequence;
} echo; //回复数据流
u_int32_t gateway; //网关地址
struct
{
u_int16_t __unused;
u_int16_t mtu;
} frag; //路径MTU
} un;
};
表示IP报文的头部的结构体,其主要用于处理ICMP回复报文中。
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4, //首部长度
version:4; //版本
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4, //版本
ihl:4; //首部长度
#else
#error "Please fix "
#endif
__u8 tos; //服务类型
__be16 -tot_len; //总长度
__be16 -id; //标识
__be16 -frag_off; //标志——偏移
__u8 ttl; //生存时间
__u8 protocol; //协议
__be16 -check; //首部校验和
__be32 -saddr; //源IP地址
__be32 -daddr; //目的IP地址
};
main函数为ping.c程序中,是整个ping程序流程中的开始入口。其完成了如下的几个功能:
创建ICMP报文;
根据用户的选项参数来循环设置选项标志;
处理用户后面的地址参数,将其保存在route数组中;
对目标地址连接一个UDP报文,获知目标主机的基本情况;
之后设置ICMP报文选项(ipv4特有);
调用ping_common.c/setup()函数来对ICMP报文设置参数(与ipv6共用);
调用ping_common.c/main_loop()函数来完成探测
该函数主要负责对ICMP报文进行设置选项,其是ipv4与ipv6的公共部分。
该函数主要负责来循环发送报文,分析报文。在一个for无限循环中,完成了了如下的几个功能:
该函数用来编写和发送ICMP数据包。
用来处理ICMP错误报文信息。
该函数用来处理ICMP答复报文。
该函数用来发送ICMP报文。
首先先对ICMP报文头部进行了一些设置:
头部位 | 设置值 |
---|---|
type | ICMP_ECHO 回复报文类型 |
code | 0 |
ckecksum | 0 (后面计算) |
un.echo.sequence | htons(ntransmitted+1) 将主机字节序转换为网络字节序 |
un.echo.id | ident 进程ID |
之后调用ping.c/in_cksum()函数来计算校验和并写入;
之后嗲用sendmsg()函数发送ICMP报文;
此函数主要用来验证校验和,使用32累加器,向它添加连续的16位字,在最后,将所有的进位从前16位折回到下16位。
此函数在最后时候调用,主要用来打印一些统计信息。
用来接收socket套接字,通用的I/O函数,可以接收面向连接或者非连接套接字。返回值返回读取的字节数。
用来发送socket套接字,通用的I/O函数,可以接收面向连接或者非连接套接字。返回值返回发送的字节数。
static int nroute = 0; // 输入的主机数目
static __u32 route[10]; // 多个主机存储数组
struct sockaddr_in whereto; /* 套接字地址 目标地址 ping谁 */
struct sockaddr_in source; /* 套接字地址 源地址 谁在ping */
int optlen = 0; //ip选项的长度
int settos = 0; /* 设置TOS 服务质量 */
int icmp_sock; /* icmp socket 文件描述符 */
static int broadcast_pings = 0;//是不是ping广播地址
char *device; //如果-I选项后面带的是设备名而不是源主机地址的话,如eth0,就用device指向该设备名。该device指向一个设备名之后,会设置socket的对应设备为该设备
int options; /*存储各种选项的FLAG设置情况 在判断输入选项时候设置*/
int sndbuf; // 发送缓冲区的大小 可以通过-S参数指定
int ttl; /* 报文 ttl值 */
int rtt; //RTT值 用指数加权移动平均算法估计出来
__u16 acked; //接到ACK的报文的16bit序列号
/* 计数器 会在finish函数中调用 */
long npackets; //需要传输的最多报文数 -c参数可以设置
long nreceived; //得到回复的报文数
long nrepeats; //重复的报文数
long ntransmitted; //发送的报文的最大序列号
long nchecksum; //checksum错误的恢复报文
long nerrors; // icmp错误数
int interval = 1000; /* 两个相邻报文之间相距的时间,单位为毫秒 -i参数可以设置 -f 洪泛模式下为0 */
int preload; /* 在接受到第一个回复报文之前所发送的报文数 -l参数可以设置 默认为1 */
int deadline = 0; /* 退出时间 -w参数设置 SIGALRM中断 */
int lingertime = MAXWAIT*1000;/* 等待回复的最长时间,单位为毫秒 -W参数设置 默认值10000 */
struct timeval start_time, cur_time; /* 程序运行开始时的主机时间 当前的主机时间 */
volatile int exiting; //程序是不是要退出
/* 计时 */
int timing; // 能否测算时间
long tmin = LONG_MAX; // 最小RRT 初始值为LONG_MAX
long tmax; // 最大RRT 初始值为0
long long tsum; // 每次RRT之和
long long tsum2; // 每次RRT的平方和
int datalen = DEFDATALEN; /* 数据长度 初始值为56 -s参数设置*/
char *hostname; /* 目的主机名字 通过gethostbyname()函数获得 */
int uid; /* 用户ID getuid()取得 如果不是超级用户则有限制*/
int ident; /* 本进程的ID */
main函数是整个程序的入口,其主要功能主要是解析ping命令参数,然后创建socket,设置socket选项。
首先是函数的定义,以及一些变量定义。定义了变量来存储主机信息,错误码 主机名字缓冲区等。
int
main(int argc, char **argv)
{
struct hostent *hp; //记录主机的信息
int ch, hold, packlen;
int socket_errno;
u_char *packet;
char *target; //目标主机
char hnamebuf[MAX_HOSTNAMELEN];
char rspace[3 + 4 * NROUTES + 1]; /* record route space */
之后创建了ICMP套接字,其协议域为ipv4(AF_INT),socket类型原始套接字(SOCK_RAW),协议类型为ICMP(IPPROTO_ICMP)。并指定了socket的错误类型代码。
enable_capability_raw();
/* 创建ICMP套接字 ipv4 原始套接字 ICMP协议*/
icmp_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
socket_errno = errno;
disable_capability_raw();
/* 源地址是ipv4类型 */
source.sin_family = AF_INET;
preload = 1; //接收回复报文前发送报文数为1
解析选项参数:使用一个while循环和switch结构来解析命令的选项参数,并将其保存在options参数中供后面使用。
while ((ch = getopt(argc, argv, COMMON_OPTSTR "bRT:")) != EOF) {
switch(ch) {
// 设置广播
case 'b':
broadcast_pings = 1;
break;
// 设置服务质量
case 'Q':
// 测试反向路由
case 'R':
if (options & F_TIMESTAMP) {
fprintf(stderr, "Only one of -T or -R may be used\n");
exit(2);
}
options |= F_RROUTE;
/* 其余每个case里面的代码省略 用此代码表示说明选项保存在全局变量option中*/
break;
case 'T':
// 设置源主机的接口地址或者设备名
case 'I':
//设置时间间隔
case 'M':
// 打印版本信息
case 'V':
printf("ping utility, iputils-%s\n", SNAPSHOT);
exit(0);//输出后退出
// 共有参数
COMMON_OPTIONS //ping.c 与 ping6.c 共有的参数解析
common_options(ch);
break;
// 非法参数
default:
usage();//报错
}
}
解析目标主机地址参数,最多有10个,将其转换成统一 地址类型,并保存在route数组中。
//参数移动
argc -= optind;
argv += optind;
/*------------------------------------------------
先对地址参数个数情况进行一个判断
不同的命令模式下有不同的要求
-------------------------------------------------*/
/* 没有地址参数 */
if (argc == 0)
usage();//报错
/* 参数多于一个地址 */
if (argc > 1) {
// F_RROUTE情况
if (options & F_RROUTE)
usage(); //报错
// -T tsprespec [host1 [host2[host3 [host4]]]]
else if (options & F_TIMESTAMP) {
if (ts_type != IPOPT_TS_PRESPEC)
usage();
// 此情况最多4个参数
if (argc > 5)
usage();
} else {
if (argc > 10) //route[] 最多存储10个地址
usage();
options |= F_SOURCEROUTE;
}
}
/*------------------------------------------------
对地址参数进行转换并存储在route数组中
-------------------------------------------------*/
while (argc > 0) {
//目标主机为参数值
target = *argv;
//where_to 设置为0
memset((char *)&whereto, 0, sizeof(whereto));
whereto.sin_family = AF_INET;
// 地址转换 将目标数字点形式地址转化为 struct in_addr结构
if (inet_aton(target, &whereto.sin_addr) == 1) {
hostname = target;
if (argc == 1)
options |= F_NUMERIC;
} else {
char *idn;
idn = target;
// 不是ipv4类型地址 通过域名查询DNS获取地址
hp = gethostbyname(idn);
// 无法获取正确地址
if (!hp) {
fprintf(stderr, "ping: unknown host %s\n", target);
exit(2);
}
//将获得得到的 hp->h_addr复制到 whereto中
memcpy(&whereto.sin_addr, hp->h_addr, 4);
//将 h_name复制到 hnamebuf中 h_name: 官方名字
strncpy(hnamebuf, hp->h_name, sizeof(hnamebuf) - 1);
hostname = hnamebuf;
}
if (argc > 1)
route[nroute++] = whereto.sin_addr.s_addr; //将主机地址存储在route数组中
//处理下一个
argc--;
argv++;
}
如果没有设置源主机地址(source.sin_addr.s_addr == 0),则申请一个UDP类型的主机探针来探测目标主机信息。
//没有设置源主机地址
if (source.sin_addr.s_addr == 0) {
socklen_t alen;
struct sockaddr_in dst = whereto; //目标主机地址
/*------------------------------------
UDP 类型的socket描述符
AFINT: ipv4
SOCK_DGRAM: 无连接 不可靠
探针 用来探测目标主机的基本情况
--------------------------------------*/
int probe_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (probe_fd < 0) { //申请失败
perror("socket");
exit(2);
}
// 如果设置了网络设备的名字
if (device) {
struct ifreq ifr;
int rc;
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, device, IFNAMSIZ-1);
enable_capability_raw();
//设置socket选项
// SO_BINDTODEVICE: 绑定socket到一个网络设备
rc = setsockopt(probe_fd, SOL_SOCKET, SO_BINDTODEVICE, device, strlen(device)+1);
disable_capability_raw();
if (rc == -1) {
// 测试 是不是一个多播地址
if (IN_MULTICAST(ntohl(dst.sin_addr.s_addr))) {
struct ip_mreqn imr;
if (ioctl(probe_fd, SIOCGIFINDEX, &ifr) < 0) {
fprintf(stderr, "ping: unknown iface %s\n", device);
exit(2);
}
memset(&imr, 0, sizeof(imr));
// 组播
// IP_MULTICAST_IF: 为组播socket设置一个当地设备
imr.imr_ifindex = ifr.ifr_ifindex;
if (setsockopt(probe_fd, SOL_IP, IP_MULTICAST_IF, &imr, sizeof(imr)) == -1) {
perror("ping: IP_MULTICAST_IF");
exit(2);
}
} else {
perror("ping: SO_BINDTODEVICE");
exit(2);
}
}
}
// 如果设置了服务质量参数 则对socket选项设置
if (settos &&
setsockopt(probe_fd, IPPROTO_IP, IP_TOS, (char *)&settos, sizeof(int)) < 0)
perror("Warning: error setting QOS sockopts");
// 将主机字节序转换为网络的字节序 (因为主机有不同的大小端)
dst.sin_port = htons(1025);
// 多个route
if (nroute)
dst.sin_addr.s_addr = route[0];
// 试探主机的基本情况 connect
if (connect(probe_fd, (struct sockaddr*)&dst, sizeof(dst)) == -1) {
// 出错: 尝试连接一个广播地址而没有设置socket广播标志
if (errno == EACCES) {
// 确定没有设置广播 提示后退出
if (broadcast_pings == 0) {
fprintf(stderr, "Do you want to ping broadcast? Then -b\n");
exit(2);
}
fprintf(stderr, "WARNING: pinging broadcast address\n");
// 否则,设置广播标志
// SO_BROADCAST: 设置广播标志
if (setsockopt(probe_fd, SOL_SOCKET, SO_BROADCAST,
&broadcast_pings, sizeof(broadcast_pings)) < 0) {
perror ("can't set broadcasting");
exit(2);
}
// 尝试再次连接
if (connect(probe_fd, (struct sockaddr*)&dst, sizeof(dst)) == -1) {
perror("connect");
exit(2);
}
} else {//其他错误
perror("connect");
exit(2);
}
}
// 将当前socket名字复制给 source
alen = sizeof(source);
if (getsockname(probe_fd, (struct sockaddr*)&source, &alen) == -1) {
perror("getsockname");
exit(2);
}
// 设置source端口号为0
source.sin_port = 0;
close(probe_fd);//关闭探针
} while (0)
之后根据option中的标志来设置套接字选项,main函数中为IPV4特有的,之后会调用 setup设置公共的。
/******************************************************
/* 根据option标志情况对socket进行设置
/*
*******************************************************/
// 广播ping -b 参数
if (broadcast_pings || IN_MULTICAST(ntohl(whereto.sin_addr.s_addr))) {
if (uid) {
// 间隔时间太短
if (interval < 1000) {
fprintf(stderr, "ping: broadcast ping with too short interval.\n");
exit(2);
}
// 非超级用户不允许在ping广播地址时进行分段
/*-----------------------------------------------------
/* pmtudisc:
IP_PMTUDISC_DO 2 Always DF 不允许分段
IP_PMTUDISC_DONT 1 User per route hints
IP_PMTUDISC_WANT 0 Never send DF frame
------------------------------------------------------------*/
if (pmtudisc >= 0 && pmtudisc != IP_PMTUDISC_DO) {
fprintf(stderr, "ping: broadcast ping does not fragment.\n");
exit(2);
}
}
// 未设置-M参数
if (pmtudisc < 0)
pmtudisc = IP_PMTUDISC_DO;
}
// 设置了-M参数 IP_PMTUDISC
if (pmtudisc >= 0) {
// 设置IP_MTU_DISCOVER 在ip 标志第二位设置为1 即不可以分段
if (setsockopt(icmp_sock, SOL_IP, IP_MTU_DISCOVER, &pmtudisc, sizeof(pmtudisc)) == -1) {
perror("ping: IP_MTU_DISCOVER");
exit(2);
}
}
// 设置了 -I 参数 给套接字绑定一个协议地址
if ((options&F_STRICTSOURCE) &&
bind(icmp_sock, (struct sockaddr*)&source, sizeof(source)) == -1) { //绑定
perror("bind");
exit(2);
}
// 设置过滤器
if (1) {
struct icmp_filter filt;
/*------------结构体原型---------------------
struct icmp_filter {
__u32 data;
};
每种消息对应一位 能把ICMP消息过滤掉
---------------------------------------------*/
filt.data = ~((1<<ICMP_SOURCE_QUENCH)|
(1<<ICMP_DEST_UNREACH)|
(1<<ICMP_TIME_EXCEEDED)|
(1<<ICMP_PARAMETERPROB)|
(1<<ICMP_REDIRECT)|
(1<<ICMP_ECHOREPLY));
// 设置socket的过滤器选项
if (setsockopt(icmp_sock, SOL_RAW, ICMP_FILTER, (char*)&filt, sizeof(filt)) == -1)
perror("WARNING: setsockopt(ICMP_FILTER)");
}
hold = 1;
// 设置socket 允许传递拓展的可靠错误信息
if (setsockopt(icmp_sock, SOL_IP, IP_RECVERR, (char *)&hold, sizeof(hold)))
fprintf(stderr, "WARNING: your kernel is veeery old. No problems.\n");
/* record route option */
//route选项
if (options & F_RROUTE) {
memset(rspace, 0, sizeof(rspace));
rspace[0] = IPOPT_NOP; //IPOPT_NOP 没有操作
rspace[1+IPOPT_OPTVAL] = IPOPT_RR;//IPOPT_RR 路由信息
rspace[1+IPOPT_OLEN] = sizeof(rspace)-1;//IPOPT_OLEN 选项长度
rspace[1+IPOPT_OFFSET] = IPOPT_MINOFF;//
optlen = 40;
//设置socket的IP选项
if (setsockopt(icmp_sock, IPPROTO_IP, IP_OPTIONS, rspace, sizeof(rspace)) < 0) {
perror("ping: record route");
exit(2);
}
}
// 时间戳
if (options & F_TIMESTAMP) {
memset(rspace, 0, sizeof(rspace));
rspace[0] = IPOPT_TIMESTAMP;// 指明IP选项的类型是时间戳
rspace[1] = (ts_type==IPOPT_TS_TSONLY ? 40 : 36);
rspace[2] = 5;
rspace[3] = ts_type; //将标志字段设置为对应的时间戳选项
//如果是-T tsprespec [host1 [host2 [host3 [host4]]]] 选项预先存入各主机地址
if (ts_type == IPOPT_TS_PRESPEC) {
int i;
rspace[1] = 4+nroute*8;
for (i=0; i<nroute; i++)
*(__u32*)&rspace[4+i*8] = route[i];
}
//设置socket 接口设置
if (setsockopt(icmp_sock, IPPROTO_IP, IP_OPTIONS, rspace, rspace[1]) < 0) {
rspace[3] = 2;
if (setsockopt(icmp_sock, IPPROTO_IP, IP_OPTIONS, rspace, rspace[1]) < 0) {
perror("ping: ts option");
exit(2);
}
}
optlen = 40;
}
// source route
if (options & F_SOURCEROUTE) {
int i;
memset(rspace, 0, sizeof(rspace));
rspace[0] = IPOPT_NOOP;
rspace[1+IPOPT_OPTVAL] = (options & F_SO_DONTROUTE) ? IPOPT_SSRR
: IPOPT_LSRR;
rspace[1+IPOPT_OLEN] = 3 + nroute*4;
rspace[1+IPOPT_OFFSET] = IPOPT_MINOFF;
for (i=0; i<nroute; i++)
*(__u32*)&rspace[4+i*4] = route[i];
if (setsockopt(icmp_sock, IPPROTO_IP, IP_OPTIONS, rspace, 4 + nroute*4) < 0) {
perror("ping: record route");
exit(2);
}
optlen = 40;
}
/* Estimate memory eaten by single packet. It is rough estimate.
* Actually, for small datalen's it depends on kernel side a lot. */
hold = datalen + 8;/*ICMP报文的大小*/
hold += ((hold+511)/512)*(optlen + 20 + 16 + 64 + 160);
sock_setbufs(icmp_sock, hold);
//广播数据报
if (broadcast_pings) {
// 设置socket 运行发送广播数据报
if (setsockopt(icmp_sock, SOL_SOCKET, SO_BROADCAST,
&broadcast_pings, sizeof(broadcast_pings)) < 0) {
perror ("ping: can't set broadcasting");
exit(2);
}
}
// 多播数据报回填
if (options & F_NOLOOP) {
int loop = 0;
//设置socket 禁止多播数据包回填
if (setsockopt(icmp_sock, IPPROTO_IP, IP_MULTICAST_LOOP,
&loop, 1) == -1) {
perror ("ping: can't disable multicast loopback");
exit(2);
}
}
// TTL设置
if (options & F_TTL) {
int ittl = ttl;
//设置输出组播数据的TTL值
//IP_MULTICAST_TTL: 为输出组播数据报设置TTL值
if (setsockopt(icmp_sock, IPPROTO_IP, IP_MULTICAST_TTL,
&ttl, 1) == -1) {
perror ("ping: can't set multicast time-to-live");
exit(2);
}
//设置socket指定TTL存活时间
if (setsockopt(icmp_sock, IPPROTO_IP, IP_TTL,
&ittl, sizeof(ittl)) == -1) {
perror ("ping: can't set unicast time-to-live");
exit(2);
}
}
之后判断ICMP报文的长度是否超出了最大长度。
/******************************************************
/* 最后对判断ICMP数据报是否超出长度
/*
*******************************************************/
// 超出了最大长度
// 2^16(长度字段16bit)-8(ICMP头部)-20(IP固定头部)-optlen(IP选项长度)
if (datalen > 0xFFFF - 8 - optlen - 20) {
// 超级用户可以超过上面那个长度设置 但是不能超过 outpack-8
if (uid || datalen > sizeof(outpack)-8) {
fprintf(stderr, "Error: packet size %d is too large. Maximum is %d\n", datalen, 0xFFFF-8-20-optlen);
exit(2);
}
/* Allow small oversize to root yet. It will cause EMSGSIZE. */
fprintf(stderr, "WARNING: packet size %d is too large. Maximum is %d\n", datalen, 0xFFFF-8-20-optlen);
}
if (datalen >= sizeof(struct timeval)) /* can we time transfer */
timing = 1;
packlen = datalen + MAXIPLEN + MAXICMPLEN;
if (!(packet = (u_char *)malloc((u_int)packlen))) {
fprintf(stderr, "ping: out of memory.\n");
exit(2);
}
最后调用setup函数设置公共的套接字选项,再调用main_loop进入循环执行发送和接收报文程序。
//其他与协议无关的参数设置和选项设置 【后面不再详细分析】
setup(icmp_sock);
//进入main_loop 主循环 发送和接收报文
main_loop(icmp_sock, packet, packlen);
}
该函数是在报文参数等已经设置好之后,进入主要的逻辑。进行报文的发送和回复报文的接收。
首先其参数有 icmp套接字,包内容packet 和长度 packlen。
void main_loop(int icmp_sock, __u8 *packet, int packlen)
{
char addrbuf[128];
char ans_data[4096]; //回答数据
struct iovec iov; //io向量
struct msghdr msg; //消息头部
struct cmsghdr *c;
int cc; //用于接受recvmsg返回值
int next; // 下一个报文
int polling; //是否阻塞
iov.iov_base = (char *)packet; //将包内容转化为io向量
之后是一个for(;;)无限循环主体,循环主体中首先是判断退出条件。
for (;;) {
/*-----------------------------------
/* 检查退出
/*各种退出原因 1.中断 2.接收包超过限制 3.超出时间限制 4.SIGQUIT
-------------------------------------*/
/* Check exit conditions. */
// 检查中断 有中断退出
if (exiting)
break;
// 接收回复超线 退出
if (npackets && nreceived + nerrors >= npackets)
break;
// 超出时间限制 退出
if (deadline && nerrors)
break;
/* Check for and do special actions. */
// SIGQUIT中断 退出
if (status_snapshot)
status(); // 打印信息 退出
如果没有退出,则调用pinger函数来发送报文。
/* Send probes scheduled to this time. */
do {
next = pinger();/*编写和传输ICMP echo 请求数据包*/
next = schedule_exit(next); /*计算下一次发送探针的时间*/
} while (next <= 0);//如果时间紧迫 尽快发送
报文发送默认为阻塞模式,但是在自适应模式下等情况下需要进行调整。
polling = 0;// 默认为阻塞
if ((options & (F_ADAPTIVE|F_FLOOD_POLL)) || next<SCHINT(interval)) {
int recv_expected = in_flight();
/* If we are here, recvmsg() is unable to wait for
* required timeout. */
if (1000 % HZ == 0 ? next <= 1000 / HZ : (next < INT_MAX / HZ && next * HZ <= 1000)) {
/* Very short timeout... So, if we wait for
* something, we sleep for MININTERVAL.
* Otherwise, spin! */
if (recv_expected) {
next = MININTERVAL;
} else {
next = 0;
/* When spinning, no reasons to poll.
* Use nonblocking recvmsg() instead. */
polling = MSG_DONTWAIT; //设置为不可阻断运行
/* But yield yet. */
sched_yield();
}
}
if (!polling &&
((options & (F_ADAPTIVE|F_FLOOD_POLL)) || interval)) {
struct pollfd pset;
pset.fd = icmp_sock;
pset.events = POLLIN|POLLERR;
pset.revents = 0;
if (poll(&pset, 1, next) < 1 ||
!(pset.revents&(POLLIN|POLLERR)))
continue;
polling = MSG_DONTWAIT;
}
}
之后是又一个循环,用来接收回复报文。
for (;;) {
struct timeval *recv_timep = NULL;
struct timeval recv_time; //接收到的时间
int not_ours = 0; //其他进程的 接收到的
/* 对msg消息进行设置*/
iov.iov_len = packlen;
memset(&msg, 0, sizeof(msg));
msg.msg_name = addrbuf;
msg.msg_namelen = sizeof(addrbuf);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = ans_data;
msg.msg_controllen = sizeof(ans_data);
//接收消息
cc = recvmsg(icmp_sock, &msg, polling);
polling = MSG_DONTWAIT;// 第二次设置为不可阻断运作
// 没有接收到消息
if (cc < 0) {
// 错误类型为 EAGAIN: 再试一次 EINTR: 数据准备好之前 接受者被中断
if (errno == EAGAIN || errno == EINTR)
break;
// 调用receive_error_msg()处理错误报文
if (!receive_error_msg()) {
if (errno) {
perror("ping: recvmsg");
break;
}
not_ours = 1;
}
} else {
//F_LATENCY 延迟 ipv6
if ((options&F_LATENCY) || recv_timep == NULL) {
if ((options&F_LATENCY) ||
ioctl(icmp_sock, SIOCGSTAMP, &recv_time))
gettimeofday(&recv_time, NULL);
recv_timep = &recv_time;
}
not_ours = parse_reply(&msg, cc, addrbuf, recv_timep);
}
// 还有其他人再使用 多用户系统
if (not_ours)
install_filter();
if (in_flight() == 0)
break;//返回到pinger
}
}
如果程序跳出,则调用finish函数进行打印统计信息之后进行退出。
/*------------------------------------------------
输出数据然后退出
-------------------------------------------------*/
finish(); //逻辑简单 后面不再详细分析
}
pinger函数用来编写和发送ICMP报文。
/* 编写和传输ICMP echo 请求数据包*/
int pinger(void)
{
static int oom_count;
static int tokens;
int i;
首先先对时间片进行判断处理。
/****************************************************
/* 时间间隔 时间片
/*
****************************************************/
/* Have we already sent enough? If we have, return an arbitrary positive value. */
/* 我们已经送够了吗?如果有,返回一个任意的正值。*/
// 返回一个超级大的数 确保main_loop有时间来判断是否退出
if (exiting || (npackets && ntransmitted >= npackets && !deadline))
return 1000;
/* Check that packets < rate*time + preload */
/* 检查数据包<速率*时间+预加载*/
if (cur_time.tv_sec == 0) {
// 第一次执行
gettimeofday(&cur_time, NULL);//初始化cur_time 为当前时间
tokens = interval*(preload-1);//初始化时间片 发送一个报文需要的时间
} else {
// 不是第一次
long ntokens;
struct timeval tv;
// 当前时刻与上一次报文的时间间隔 没有接收报文则被忽略
gettimeofday(&tv, NULL);
ntokens = (tv.tv_sec - cur_time.tv_sec)*1000 +
(tv.tv_usec-cur_time.tv_usec)/1000;
/* interval 默认值1000 -i 参数*/
if (!interval) {
/* Case of unlimited flood is special;
* if we see no reply, they are limited to 100pps */
// 如果没有等到等到间隔时间 并且有preload个报文在传输
// 则等待一会在发送
if (ntokens < MININTERVAL && in_flight() >= preload)
return MININTERVAL-ntokens;
}
ntokens += tokens;
// 分配的时间片不能发送超过preload-1个报文
if (ntokens > interval*preload)
ntokens = interval*preload;
// 剩下的时间片不足一个间隔
if (ntokens < interval)
return interval - ntokens;// 不再发送 开始接收报文
cur_time = tv;
tokens = ntokens - interval;
}
处理好之后调用send_probe函数来发送ICMP报文探针,使用i来接收返回值。其在标签resend下,可以进行重试。
resend:
i = send_probe(); //发送ICMP消息
如果返回值正确,如果非静默模式且洪泛模式下输出一堆点。之后返回退出。
//发送成功
if (i == 0) {
oom_count = 0;
advance_ntransmitted();
// 非静默模式 且洪泛模式
if (!(options & F_QUIET) && (options & F_FLOOD)) {
/* Very silly, but without this output with
* high preload or pipe size is very confusing. */
if ((preload < screen_width && pipesize < screen_width) ||
in_flight() < screen_width)
write_stdout(".", 1); // 输出一堆点 来代表多少个报文没有回答
}
return interval - tokens;// 是否还有多余时间片
}
如果返回值不正确,则根据具体情况处理。errno保存了一些错误信息。如果是错误报文,则使用receive_error_msg函数来处理。
// i>0 致命的BUG
if (i > 0) {
/* Apparently, it is some fatal bug. */
abort();
}
//ENOBUFS:输出网络接口缓存满了
//ENOMEM:没有内存了
else if (errno == ENOBUFS || errno == ENOMEM) {
int nores_interval;
/* Device queue overflow or OOM. Packet is not sent. */
/* 内存不够或者缓冲满了*/
tokens = 0;
/* Slowdown. This works only in adaptive mode (option -A) */
/* 减慢发送速度 TTL适应模式*/
rtt_addend += (rtt < 8*50000 ? rtt/8 : 50000);
if (options&F_ADAPTIVE)
update_interval();
nores_interval = SCHINT(interval/2);
if (nores_interval > 500)
nores_interval = 500;
oom_count++;
if (oom_count*nores_interval < lingertime)
return nores_interval;
i = 0;
}
// socket 缓冲区满了
else if (errno == EAGAIN) {
/* Socket buffer is full. */
tokens += interval;
return MININTERVAL;
} else {
// ICMP错误报文
if ((i=receive_error_msg()) > 0) {
/* An ICMP error arrived. */
tokens += interval;
return MININTERVAL;
}
/* Compatibility with old linuces. */
if (i == 0 && confirm_flag && errno == EINVAL) {
confirm_flag = 0;
errno = 0;
}
if (!errno)
goto resend; //重新发送
}
之后进行一些结尾操作结束发送报文。
/* 本地错误 发送了数据包*/
advance_ntransmitted();
//静默模式
if (i == 0 && !(options & F_QUIET)) {
if (options & F_FLOOD)
write_stdout("E", 1);
else
perror("ping: sendmsg");
}
tokens = 0;
return SCHINT(interval);
}
该函数用来处理ICMP错误报文。
/* 接收错误报文*/
int receive_error_msg()
{
int res;
char cbuf[512];
struct iovec iov; //io向量
struct msghdr msg; //消息
struct cmsghdr *cmsg;
struct sock_extended_err *e; //套接字错误
struct icmphdr icmph; //icmp报文头部
struct sockaddr_in target;// 目标主机地址
int net_errors = 0; //计数 错误次数
int local_errors = 0;//计数 本地错误次数
int saved_errno = errno; // 保存错误编码
/* 设置msg*/
iov.iov_base = &icmph;
iov.iov_len = sizeof(icmph);
msg.msg_name = (void*)⌖
msg.msg_namelen = sizeof(target);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_flags = 0;
msg.msg_control = cbuf;
msg.msg_controllen = sizeof(cbuf);
使用recvmsg函数来接收icmp_sockt回复消息,保存在msg结构体中。
//接收消息 MSG_DONTWAIT 不可打断
res = recvmsg(icmp_sock, &msg, MSG_ERRQUEUE|MSG_DONTWAIT);
if (res < 0)
goto out;
之后读取错误信息类型,取最后一个。
e = NULL;
for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_IP) {
if (cmsg->cmsg_type == IP_RECVERR)
// sock_extended_err: 错误描述
// 通过 IP_RECVERR SOL_IP信息来进行传递
e = (struct sock_extended_err *)CMSG_DATA(cmsg);
}
}
if (e == NULL)
abort();
之后根据不同的错误类型来进行不同的错误处理。主要为本地错误和ICMP差错报文错误。
// 本地产生错误
if (e->ee_origin == SO_EE_ORIGIN_LOCAL) {
local_errors++;
if (options & F_QUIET)
goto out;
if (options & F_FLOOD)
write_stdout("E", 1);
else if (e->ee_errno != EMSGSIZE)
fprintf(stderr, "ping: local error: %s\n", strerror(e->ee_errno));
else
fprintf(stderr, "ping: local error: Message too long, mtu=%u\n", e->ee_info);
nerrors++;
}
// ICMP差错报文
else if (e->ee_origin == SO_EE_ORIGIN_ICMP) {
struct sockaddr_in *sin = (struct sockaddr_in*)(e+1);
if (res < sizeof(icmph) ||
target.sin_addr.s_addr != whereto.sin_addr.s_addr ||
icmph.type != ICMP_ECHO ||
icmph.un.echo.id != ident) {
/* Not our error, not an error at all. Clear. */
saved_errno = 0;
goto out;
}
//ICMP差错报文的源主机地址和本机主机地址、ICMP类型和ICMP_ECHO、报文中的标识符和本进程ID都符合
acknowledge(ntohs(icmph.un.echo.sequence));
if (!working_recverr) {
struct icmp_filter filt;
working_recverr = 1;
/* OK, it works. Add stronger filter. */
// 如果是网络出错则安装一个更加严格的过滤器
filt.data = ~((1<<ICMP_SOURCE_QUENCH)|
(1<<ICMP_REDIRECT)|
(1<<ICMP_ECHOREPLY));
if (setsockopt(icmp_sock, SOL_RAW, ICMP_FILTER, (char*)&filt, sizeof(filt)) == -1)
perror("\rWARNING: setsockopt(ICMP_FILTER)");
}
net_errors++;
nerrors++;
/*********************
* 不同模式
**********************/
//静默模式 退出
if (options & F_QUIET)
goto out;
// 洪泛模式
if (options & F_FLOOD) {
write_stdout("\bE", 2);
} else {
print_timestamp();
// ICMP差错报文源主机地址,序列号
printf("From %s icmp_seq=%u ", pr_addr(sin->sin_addr.s_addr), ntohs(icmph.un.echo.sequence));
//分析并打印出网络出错原因
pr_icmph(e->ee_type, e->ee_code, e->ee_info, NULL);
fflush(stdout);
}
}
最后部分为退出处理,将errno恢复为开始的值 并返回错误类型。
out:
errno = saved_errno;
return net_errors ? : -local_errors;
}
该函数用来解析ICMP回复报文。
int parse_reply(struct msghdr *msg, int cc, void *addr, struct timeval *tv)
{
struct sockaddr_in *from = addr; //来源地址
__u8 *buf = msg->msg_iov->iov_base; //设置buf位置
struct icmphdr *icp; //ICMP报文头部
struct iphdr *ip; //IP报文头部
int hlen;
int csfailed;
首先先提取IP头部信息,检查报文长度正不正确。
//检查IP头部
ip = (struct iphdr *)buf;
hlen = ip->ihl*4;//ip报文长度
if (cc < hlen + 8 || ip->ihl < 5) {
if (options & F_VERBOSE)
fprintf(stderr, "ping: packet too short (%d bytes) from %s\n", cc,
pr_addr(from->sin_addr.s_addr));
return 1;
}
之后提取ICMP报文头部信息,检查校验和。
// 检测ICMP 校验和
cc -= hlen;
icp = (struct icmphdr *)(buf + hlen);
csfailed = in_cksum((u_short *)icp, cc, 0);
如果是ICMP回复报文类型,则依据是不是回复本进程ID来进行不同的处理。之后进行退出。
/*-------------------------------------------
/* 是ICMP的回复
-------------------------------------------**/
if (icp->type == ICMP_ECHOREPLY) {
//ICMP的回复的ID不是本进程的ID
if (icp->un.echo.id != ident)
return 1; /* 'Twas not our ECHO */
//是本进程的ICMP的回复
if (gather_statistics((__u8*)icp, sizeof(*icp), cc,
ntohs(icp->un.echo.sequence),
ip->ttl, 0, tv, pr_addr(from->sin_addr.s_addr),
pr_echo_reply))
return 0;
}
如果不是ICMP回复报文类型,则使用switch结构来依据不同的报文类型来进行处理。
/*-------------------------------------------
/* 如果不是ICMP的回复
-------------------------------------------**/
else {
switch (icp->type) {
case ICMP_ECHO:
/* MUST NOT */
return 1;
case ICMP_SOURCE_QUENCH:
case ICMP_REDIRECT:
case ICMP_DEST_UNREACH:
case ICMP_TIME_EXCEEDED:
case ICMP_PARAMETERPROB:
{
/*具体处理省略*/
}
default:
/* MUST NOT */
break;
}
最后根据不同的模式进行结尾处理退出。
if (!(options & F_FLOOD)) {
//非洪泛类型
pr_options(buf + sizeof(struct iphdr), hlen);
if (options & F_AUDIBLE)
putchar('\a');
putchar('\n');
fflush(stdout);
} else {
putchar('\a');
fflush(stdout);//校验和错误
}
return 0;
}