ping源码分析 (超级详细 带背景分析)

内容比较多,目录如下
ping源码分析

  1. 背景知识简介
    1.1 ping简介
    1.2 ICMP协议简介
    1.3 socket简介
  2. 分析准备
    2.1 源码简介
    2.2 源码获取
    2.3 源码结构
    2.4 编译测试
    2.5 分析对象
  3. 源码概要分析
    3.1 逻辑功能流程
    3.2 主要数据结构
    3.3 主要函数及其功能简介
    3.4 主要函数间调用关系
  4. 源码详细分析
    4.1 重要变量分析
    4.2 代码详细分析

1. 背景知识简介

1.1 ping简介

​ 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

1.2 ICMP协议简介

​ ICMP是(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议簇的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。

1.2.1 ICMP报文分类

​ ICMP报文主要分为两类:一类是差错报文,一类是查询报文。

查询报文

  1. ping命令请求与回复报文
YPE CODE Description
0 0 Echo Reply——回显应答(Ping应答)
8 0 Echo request——回显请求(Ping请求)
  1. 时间戳请求和时间戳答复
YPE CODE Description
13 0 Timestamp request (obsolete)——时间戳请求(作废不用)
14 0 Timestamp reply (obsolete)——时间戳应答(作废不用)
  1. 路由器请求与通告
YPE CODE Description
9 0 Router advertisement——路由器通告
10 0 Route solicitation——路由器请求
  1. 地址码请求与答复
YPE CODE Description
17 0 Address mask request——地址掩码请求
18 0 Address mask reply——地址掩码应答

差错报文

  1. 终点不可达,当数据包不能发送到目标主机或路由时,就会丢弃该数据包向源点发送终点不可达报文。
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——优先中止生效
  1. 源点抑制,用于告知源点应该降低发送数据包的速率。
YPE CODE Description
4 0 Source quench——源端被关闭(基本流控制)
  1. 超时 当路由器收到TTL值为0的数据包时,会丢弃该数据包并向源点发送超时报文。
YPE CODE Description
11 0 TTL equals 0 during transit——传输期间生存时间为0
11 1 TTL equals 0 during reassembly——在数据报组装期间生存时间为0
  1. 参数问题,可能是IP首部有的字段值是错误的或者IP首部被修改,破坏都有可能
YPE CODE Description
12 0 IP header bad (catchall error)——坏的IP首部(包括各种差错)
12 1 Required options missing——缺少必需的选项
  1. 路由重定向
YPE CODE Description
5 1 Redirect for host——对主机重定向
5 2 Redirect for TOS and network——对服务类型和网络重定向
5 3 Redirect for TOS and host——对服务类型和主机重定向

1.2.2 ICMP报文结构

​ 不同的ICMP报文有着不同的报文结构,但是都有着如下的共同结构:

img
  • TYPE字段 8bits 类型字段
  • CODE字段 8bits 提供更多的报文类型信息
  • CHECKSUM字段 16bits 校验和字段

1.2.3 ICMP封装过程

ICMP协议是基于IP协议的。ICMP报文被封装在IP报文的数据段中,其封装原理图如下:

img

1.3 socket简介

​ 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

​ 建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

1.3.1 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的地址类型,在通信中必须采用对应的地址。

    • AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合
    • AF_UNIX决定了要用一个绝对路径名作为地址。
  • type:指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。

    • 流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。
    • 数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用
  • protocol:指定协议。

    • IPPROTO_TCP TCP传输协议
    • IPPROTO_UDP UDP传输协议
    • IPPROTO_STCP STCP传输协议
    • IPPROTO_TIPC TIPC传输协议。

返回值:

  • 成功就返回新创建的套接字的描述符
  • 失败就返回INVALID_SOCKET(Linux下失败返回-1)。

1.3.2 socket绑定

函数int bind(SOCKET socket, const struct sockaddr address, socklen_t address_len);用于将套接字文件描述符绑定到一个具体的协议地址。

参数说明:

  • socket:是一个套接字描述符。

  • address:是一个sockaddr结构指针,该结构中包含了要结合的地址和端口号。

  • address_len:确定address缓冲区的长度。

返回值:

  • 执行成功,返回值为0
  • 执行失败,返回SOCKET_ERROR。

1.3.3 socket选项

使用int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);函数来对socket选项进行设置。

参数说明:

  • socket:是一个套接字描述符。
  • level:选项所在的协议层。
    • SOL_SOCKET:通用套接字选项.
    • IPPROTO_IP:IP选项.
    • IPPROTO_TCP:TCP选项.
  • optname:需要访问的选项名
  • *optval: 新选项值的缓冲
  • optlen: 选项的长度

返回值:

  • 成功:返回0
  • 失败:返回错误码

该程序中使用了的选项值如下:

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 允许传递扩展的可靠的错误信息

1.3.4 socket发送消息

socket使用ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);函数来发送消息

参数说明:

  • socketfd:一个套接字描述符;
  • *msg:  信息头结构指针;
  • flags: 标记参数

返回值:

  • 发生错误:返回-1
  • 正确发送: 返回发送的字节数

1.3.5 socket接受消息

socket使用ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);函数来接收消息

参数说明:

  • socketfd:一个套接字描述符;
  • *msg:  信息头结构指针;
  • flags: 标记参数

返回值:

  • 发生错误:返回-1
  • 正确发送: 接收了的字节数

2. 分析准备

2.1 源码简介

​ 分析的源码选自Linux环境下的iputils。iputils包是一组用于Linux网络的小型实用工具。它最初由亚历克赛·库兹涅佐夫维护。

2.2 源码获取

​ 本此源码分析选择了iputils-s20121221版本的中的代码作为分析对象。获取来源为github开项目。其地址为https://github.com/dgibson/iputils

2.3 源码结构

​ 与ping程序有关的主要为以下4个文件,其各自的名字及功能简介如下:

ping.c   // ipv4 下的ping程序
ping6.c  // ipv6 下的ping程序
ping_common.c //ipv4与ipv6共有的 与协议无关的共同代码 
ping_common.h //ping_common.c的头文件

2.4 编译测试

  1. 在Linux环境下下载iputils源码。其目录结构如下:

    在这里插入图片描述
  2. 使用make命令进行编译,编译完成后。 运行ping命令测试。可以看到编译通过且程序测试成功。

    在这里插入图片描述

2.5 分析对象

​ 本次实验以ipv4下的ping程序下的完整流程为分析对象,重点了解ping程序的完整流程以及学习其中网络编程的方法。

3. 源码概要分析

3.1 逻辑功能流程

​ 代码的逻辑功能主要分为1. 使用UDP报文对目标主机进行一个connetc尝试 2. 时候使用循环发送ICMP报文并对回复报文进行处理 这两个主要部分。当遇到中断等条件时候打印信息后进行退出。

flowchat
st=>start: 开始 用户调用ping程序
getopt_ipv4=>operation: 根据用户的设置的选项设置参数
creat_set_UDP=>operation: 创建并设置UDP探针报文 
connect_UDP=>operation: 连接UDP探针报文 
get_info=>operation: 获得目标主机的必要信息
setopt_ICMP=>operation: 设置ICMP报文选项

exit_condition=>condition: 是否退出?(中断 超时等)

print_info=>operation: 打印出信息
end=>end: 退出整个程序

send_ICMP=>operation: 发送ICMP报文
time_condition=>condition: 还有时间?

process_RECV=>operation: 处理ICMP答复报文

st(left)->getopt_ipv4->setopt_ICMP
setopt_ICMP->exit_condition
exit_condition(yes)->print_info->end
exit_condition(no)->send_ICMP->time_condition
time_condition(no)->process_RECV(top)->exit_condition
time_condition(yes)->send_ICMP

3.2 主要数据结构

3.2.1 网络通信地址sockaddr_in

用来表示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;     // 地址是按照网络字节顺序排列的
};

3.2.2 主机信息 hostent

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地址列表
};

3.2.3 I/O向量 iovec

I/O vector,与readv和wirtev操作相关的结构体。readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。其主要用于发送和接收ICMP报文中。

struct iovec {
    ptr_t iov_base; //开始地址
    size_t iov_len; //长度
};

3.2.4 消息组织结构 msghdr

在套接字发送接收系统调用流程中,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; //异步请求控制块
};

3.2.5 时间变量 timeval

时间结构体,用来记录时间。其在本程序中主要用于接受报文部分中。

struct timeval  
{  
__time_t tv_sec;        //秒
__suseconds_t tv_usec;  //微秒  
};

3.2.6 ICMP报文头部 icmphdr

用来表示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;
};

3.2.7 IP报文头部 iphdr

表示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 <asm/byteorder.h>"
#endif
    __u8    tos;       //服务类型
    __be16 -tot_len;   //总长度
    __be16 -id;        //标识
    __be16 -frag_off;  //标志——偏移
    __u8    ttl;       //生存时间
    __u8    protocol;  //协议
    __be16 -check;     //首部校验和
    __be32 -saddr;     //源IP地址
    __be32 -daddr;     //目的IP地址
};

3.3 主要函数及其功能简介

3.3.1 main函数(ping.c中)

main函数为ping.c程序中,是整个ping程序流程中的开始入口。其完成了如下的几个功能:

  1. 创建ICMP报文;

  2. 根据用户的选项参数来循环设置选项标志;

  3. 处理用户后面的地址参数,将其保存在route数组中;

  4. 对目标地址连接一个UDP报文,获知目标主机的基本情况;

  5. 之后设置ICMP报文选项(ipv4特有);

  6. 调用ping_common.c/setup()函数来对ICMP报文设置参数(与ipv6共用);

  7. 调用ping_common.c/main_loop()函数来完成探测

3.3.2 setup函数 (ping_common.c中)

该函数主要负责对ICMP报文进行设置选项,其是ipv4与ipv6的公共部分。

3.3.3 main_loop函数 (ping_common.c中)

该函数主要负责来循环发送报文,分析报文。在一个for无限循环中,完成了了如下的几个功能:

  1. 检查是否因为 中断,超出次数限制,超出时间限制,SIGQUIT中断而退出;
  2. 调用ping.c/pinger()函数来 发送ICMP报文;
  3. 调用recvmsg()函数来接收报文;
  4. 如果没有正确接收报文,就调用ping.c/receive_error_msg()函数来处理ICMP差错报文;
  5. 如果正确处理了接收报文,就调用ping.c/parse_reply()函数来解析ICMP回复报文;
  6. 如此循环知道不满足条件之后调用ping.c/finish()函数来打印统计信息之后退出;

3.3.4 pinger函数 (ping_common.c中)

该函数用来编写和发送ICMP数据包。

  1. 调用ping.c/send_probe()函数来组成并发送ICMP回显请求包;
  2. 发送成功则返回剩余时间并退出;
  3. 发送失败则根据各种情况处理失败:
  4. 非常见错误调用abort()函数退出;
  5. ENOBUFS:输出网络接口缓存满 或者 ENOMEM:没有内存。则减缓发送速度;
  6. EAGAIN:缓冲区满 则增加时间间隔返回;
  7. ICMP错误报文 使用 ping.c/receive_error_msg()函数来处理ICMP差错报文;

3.3.5 receive_error_msg函数 (ping.c中)

用来处理ICMP错误报文信息。

  1. 首先通过 recvmsg()函数来获得接收消息;
  2. 如果是本地错误,则根据不同的标志情况来处理;
  3. 否则则是ICMP差错报文信息,如果不是我们的错误,则退出。如果是网络出错,则安装一个更严格的过滤器。之后根据不同模式处理退出。

3.3.6 parse_reply函数(ping.c中)

该函数用来处理ICMP答复报文。

  1. 首先先检查IP头部 错误则退出;
  2. 检查ICMP头部,调用ping.c/in_cksum()函数来检测校验和;
  3. 如果是ICMP回复报文,则根据进程ID判断是不是回复本进程。并做出对应操作之后退出;
  4. 如果不是ICMP回复报文,则根据不同的报文类型来进行不同的操作。

3.3.7 send_prob函数 (ping.c中)

该函数用来发送ICMP报文。

  1. 首先先对ICMP报文头部进行了一些设置:

    头部位 设置值
    type ICMP_ECHO 回复报文类型
    code 0
    ckecksum 0 (后面计算)
    un.echo.sequence htons(ntransmitted+1) 将主机字节序转换为网络字节序
    un.echo.id ident 进程ID
  2. 之后调用ping.c/in_cksum()函数来计算校验和并写入;

  3. 之后嗲用sendmsg()函数发送ICMP报文;

3.3.8 in_cksum函数 (ping.c中)

此函数主要用来验证校验和,使用32累加器,向它添加连续的16位字,在最后,将所有的进位从前16位折回到下16位。

3.3.9 finish函数(ping_common.c中)

此函数在最后时候调用,主要用来打印一些统计信息。

3.3.10 recvmsg函数(共享库函数)

用来接收socket套接字,通用的I/O函数,可以接收面向连接或者非连接套接字。返回值返回读取的字节数。

3.3.11 sendmsg函数(共享库函数)

用来发送socket套接字,通用的I/O函数,可以接收面向连接或者非连接套接字。返回值返回发送的字节数。

3.4 主要函数间调用关系

graph TD

MAIN[main 解析参数 设置套ipv4特有接字选项 ]-->| 设置共有套接字选项|SETUP[setup]
MAIN-->| 发送和接收ICMP报文|MAIN_LOOP[main_loop]
MAIN_LOOP-->|发送ICMP报文|PINGER[pinger]
MAIN_LOOP-->|重复运行 不满足退出条件|MAIN_LOOP
MAIN_LOOP-->|接收ICMP报文|RECVMSG[recvmsg]
MAIN_LOOP-->|没有正确接收|RECV_ERROR[receive_error_msg]
MAIN_LOOP-->|正确接收|PARSE_REPLY[parse_reply]
MAIN_LOOP-->|因为中断等退出|FINISH[finish]
PINGER-->| 构造和发送ICMP消息|SEND_PROBE[send_probe]


SEND_PROBE-->|发送报文|SENDMSG[sendmsg]

PARSE_REPLY-->|检查校验和|CKSUM[in_cksum 检查校验和]

4. 源码详细分析

4.1 重要变量分析

4.1.1 ping.c中的全局变量分析

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的对应设备为该设备

4.1.2 ping_common.c中的全局变量分析

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 */

4.2 代码详细分析

4.2.1 main函数

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 <hostname> 参数 给套接字绑定一个协议地址
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);
}

4.2.2 main_loop函数

该函数是在报文参数等已经设置好之后,进入主要的逻辑。进行报文的发送和回复报文的接收。

首先其参数有 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(); //逻辑简单 后面不再详细分析
}

4.2.3 pinger函数

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);
}

4.2.4 recive_error_msg函数

该函数用来处理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*)&target;
    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;
}

4.2.5 parse_reply函数

该函数用来解析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;
}

推荐阅读更多精彩内容