利用ICMPv4协议实现一个ping程序

2022-11-14 14:43:22 浏览数 (1)

文章目录

  1. 1. 抓包
  2. 2. ICMP协议数据报
  3. 3. ping程序原理
  4. 4. Cpp代码实现
  5. 5. traceroute
  6. 6. 问题记录
  7. 7. 参考

Icmp(Internet Control Message Protocol)协议一般与IP协议结合使用,以便给IP协议提供诊断和控制信息。 Icmp通常被认为是Ip协议的一部分,传输的时候也是被封装在Ip报文内。 我们在判断网络状况时用的ping程序就利用了ICMP协议。接下来先运行系统上的ping程序,用tcpdump抓包查看一下传输的数据。 然后解释一下icmp数据报的各个字段。最后思考一下ping程序的结构,然后用c 实现一个自己的ping程序。

抓包

首先在1号终端运行tcpdump。

代码语言:javascript复制
tcpdump -X > icmp.txt   # -X是以十六进制和ascii显示数据

在2号终端执行ping程序,然后ctrl C退出ping程序。

代码语言:javascript复制
root@yifei:/home/yifei# ping baidu.com
PING baidu.com (220.181.38.148) 56(84) bytes of data.
64 bytes from 220.181.38.148 (220.181.38.148): icmp_seq=1 ttl=44 time=5.36 ms
64 bytes from 220.181.38.148 (220.181.38.148): icmp_seq=2 ttl=44 time=5.30 ms
^C
--- baidu.com ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 5.300/5.330/5.360/0.030 ms

查看icmp.txt中的icmp数据包,查找到跟220.181.38.148的通信。

代码语言:javascript复制
13:10:07.606759 IP yifei > 220.181.38.148: ICMP echo request, id 7657, seq 1, length 64
     0x0000:  4500 0054 e26f 4000 4001 a2ee ac15 05ec  E..T.o@.@.......
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     0x0010:  dcb5 2694 0800 324a 1de9 0001 af58 195e  ..&...2J.....X.^
              ^^^^^^^^^ *******************
     0x0020:  0000 0000 1742 0900 0000 0000 1011 1213  .....B..........
     0x0030:  1415 1617 1819 1a1b 1c1d 1e1f 2021 2223  .............!"#
     0x0040:  2425 2627 2829 2a2b 2c2d 2e2f 3031 3233  $%&'()* ,-./0123
     0x0050:  3435 3637                                4567
13:10:07.612103 IP 220.181.38.148 > yifei: ICMP echo reply, id 7657, seq 1, length 64
     0x0000:  4500 0054 e26f 4000 2c01 b6ee dcb5 2694  E..T.o@.,.....&.
     0x0010:  ac15 05ec 0000 3a4a 1de9 0001 af58 195e  ......:J.....X.^
     0x0020:  0000 0000 1742 0900 0000 0000 1011 1213  .....B..........
     0x0030:  1415 1617 1819 1a1b 1c1d 1e1f 2021 2223  .............!"#
     0x0040:  2425 2627 2829 2a2b 2c2d 2e2f 3031 3233  $%&'()* ,-./0123
     0x0050:  3435 3637                                4567

可以看到本机先给baidu发送一个ICMP请求,然后baidu给回复了一条消息。 在第一个数据包中,用^标示的是ip首部,用*标示的是icmp首部,剩下的是icmp数据,具体每个字段的意义,在本文下一节中讲解。

ICMP协议数据报

下图是icmp报文被ip协议封装后的结构:

代码语言:javascript复制
----------------------------------------
| Ipv4头部  | Icmp头部 | Icmp数据      |
----------------------------------------

下图为icmp协议头部的结构:

代码语言:javascript复制
|------------------------------------------------|
| 类型(8位) | 代码(8位) |   校验和(16位)         |
|------------------------------------------------|
| 依赖类型和代码的内容                           |
|------------------------------------------------|
| 数据(可选)                                     |
|------------------------------------------------|

ping程序的回显请求中,类型为8,代码为0时,结构是下图这样,多了标识符和序列号两个字段:

代码语言:javascript复制
|------------------------------------------------|
| 类型(8位) | 代码(8位) |   校验和(16位)         |
|------------------------------------------------|
| 标识符(16位)          |   序列号(16位)         |
|------------------------------------------------|
| 数据(可选)                                     |
|------------------------------------------------|
  • 其中的类型和代码字段都是8位,这两个字段决定了Icmp数据报的用途,两个字段表示的组合如下:
  • 校验和16位,涵盖了icmp的报头和数据两部分。
  • 标识符可以设置为进程pid号,因为没有端口号来区分数据报了。
  • 序列号可以是从0开始的序号,用来标识icmp数据报。

ping程序原理

了解了icmp协议之后,ping程序的原理就很好理解了,可以分为以下几步。 1.将输入的域名转为ip地址。 2.填充icmp数据报。 3.创建原始套接字。 4.使用sendoto发送icmp报文。 5.使用recvfrom接受数据报。

Cpp代码实现

code

代码语言:javascript复制
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <netdb.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <ctime>
#include <signal.h>
using namespace std;

enum{ICMP_DATA_LEN=1024};
enum{IP_DATA_LEN=1024};
enum{RECV_MAX_LEN=1024};

/* 暂时用不着ip头部的结构体
//IP报头
typedef struct{
    unsigned char hdr_len;           //4位头部长度 & 4位版本号
    unsigned char tos;               //8位服务类型
    unsigned short total_len;        //16位总长度
    unsigned short identifier;       //16位标识符
    unsigned short frag_and_flags;   //3位标志加13位片偏移
    unsigned char ttl;               //8位生存时间
    unsigned char protocol;          //8位上层协议号
    unsigned short checksum;         //16位效验和
    unsigned int sourceIP;           //32位源IP地址
    unsigned int destIP;             //32位目的IP地址
}IpHeader;

//整个ip数据报
typedef struct{
    IpHeader ih;
    char data[IP_DATA_LEN];
}IpComplete;
*/

//ICMP报头
typedef struct{
    unsigned char type;     //8位类型字段
    unsigned char code;     //8位代码字段
    unsigned short cksum=0;  //16位效验和
    unsigned short id;     //16位标识符
    unsigned short seq;    //16位序列号
}IcmpHeader;

//整个icmp数据报
typedef struct{
    IcmpHeader icmph;
    char data[ICMP_DATA_LEN];
}IcmpComplete;

pid_t pid;  //当前进程pid
int seqnum=0;   //icmp头部中的序列号
struct sockaddr_in sai; //目的ip地址
char ipaddr[20];    //char类型ip地址
clock_t second,first;   //记录每个数据报发出~接受到回复的时间
int lostpacks=0,sumpacks=0;//记录数据报总数和丢包数

//计算校验和
unsigned short CheckSum(unsigned short *addr,int len){
    int nleft=len;
    unsigned int sum=0;
    unsigned short *w=addr;
    unsigned short answer=0;
    while(nleft>1){
        sum =*w  ;
        nleft-=2;
    }
    if(nleft==1){
        *(unsigned short *)(&answer)=*(unsigned char *)w;
        sum =answer;
    }
    sum=(sum>>16) (sum&0xffff);
    sum =(sum>>16);
    answer=~sum;
    return answer;
}

//填充整个icmp数据报
void FillIcmpCom(IcmpComplete *icmpcom){
    memset(icmpcom,0,8 sizeof(icmpcom->data));
    icmpcom->icmph.type=(unsigned char)8;
    icmpcom->icmph.code=0;
    icmpcom->icmph.cksum=0;
    pid=getpid();
    icmpcom->icmph.id=pid;
    icmpcom->icmph.seq=seqnum;
    icmpcom->icmph.cksum=CheckSum((unsigned short*)icmpcom,sizeof(icmpcom));
}

//根据域名获取ip地址
void GetIpByName(char *argv){
    struct addrinfo*res;
    if(getaddrinfo(argv,nullptr,nullptr,&res)!=0){
        cout<<"地址解析出错!"<<endl;
        return ;
    }else{
        //网络字节序的ip地址转为ascii字符数组中点分十进制的ip地址
        inet_ntop(AF_INET,(void*)&((sockaddr_in*)(res->ai_addr))->sin_addr,ipaddr,16);
        sai.sin_family=AF_INET;
        sai.sin_addr.s_addr=inet_addr(ipaddr);
        cout<<"PING "<<argv<<"("<<ipaddr<<")"<<endl;
    }
    freeaddrinfo(res);
}

//处理SIGINT信号
void handler(int data){
    cout<<endl<<"------------statistics-------------"<<endl;
    cout<<sumpacks<<" packets trasimitted,"<<lostpacks<<" packets loss"<<endl;
    exit(0);
}

int main(int argc,char **argv){
    GetIpByName(argv[1]);

    int sockfd;
    //创建icmp协议的原生套接字,需要自己构造icmp头部(ip头部系统自动填充,可以通过设置IP_HDRINCL套接字选项自己构造ip头部)
    sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);
    if(sockfd<0){
        cout<<"创建socket出错。"<<endl;
        return 0;
    }

    struct timeval time_out;
    time_out.tv_sec=1;
    time_out.tv_usec=0;
    //设置recvfrom函数的接收超时为1秒。
    if(setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,(void*)&time_out,sizeof(time_out))<0){
        cout<<"设置接收超时出错!"<<endl;
    }
    int errnum=0;
    char recbuf[RECV_MAX_LEN];
    IcmpComplete icmpcomplete;
    signal(SIGINT,handler); //设置ctrl c中断的处理函数
    for(;;){
        seqnum  ;
        FillIcmpCom(&icmpcomplete);

        //发送整个icmp数据报
        if((errnum=sendto(sockfd,(void*)&icmpcomplete,sizeof(icmpcomplete),0,(struct sockaddr *)&sai,sizeof(sai)))<0){
            cout<<"ICMP数据报发送失败!   "<<errno<<" "<<strerror(errno)<<endl;;
            return 0;
        }else{
            first=clock();
        }

        //收到的recbuf数据包括ip头部,icmp头部,icmp数据
        if(recvfrom(sockfd,recbuf,sizeof(recbuf),0,nullptr,nullptr)<0){
            cout<<"*"<<endl;
            lostpacks  ;
        }else{
            second=clock();
            cout<<sizeof(recbuf)<<" bytes from("<<ipaddr<<"): "
                <<"icmp_seq="<<seqnum<<" "
                <<"ttl="<<(int)recbuf[8]<<" "
                <<"time="<<(second-first)<<" ms"<<endl;
        }
        sleep(1);
        sumpacks  ;
    }
    return 0;
}

编译运行

代码语言:javascript复制
> gcc myping.cpp -o myping
> ./myping www.baidu.com
PING baidu.com(39.156.69.79)
1024 bytes from(39.156.69.79): icmp_seq=1 ttl=40 time=73 ms
1024 bytes from(39.156.69.79): icmp_seq=2 ttl=40 time=47 ms
1024 bytes from(39.156.69.79): icmp_seq=3 ttl=40 time=40 ms
1024 bytes from(39.156.69.79): icmp_seq=4 ttl=40 time=39 ms
1024 bytes from(39.156.69.79): icmp_seq=5 ttl=40 time=40 ms
^C
------------statistics-------------
4 packets trasimitted,0 packets loss

traceroute

traceroute工具可以查看数据报在传输过程中都是经过了哪些节点。这利用了ip协议中的ttl字段。 traceroute首先发送ttl字段为1的ip数据报,然后分别发送ttl字段为2,3,…的数据报。 当数据报经过一个节点,ttl变为0之后,就会给发送方回复一个icmp数据报。这样就能获得每个节点的ip地址了。 将之前的程序进行简单改动,就能实现traceroute的效果。

代码语言:javascript复制
1.在每次sendto发送icmp数据报之前,使用该函数可以直接设置IP数据报中的ttl字段,不用自己填充ip头部了。
setsockopt(sockfd,IPPROTO_IP,IP_TTL,(void*)&ittl,sizeof(ittl))
2.接受到返回的消息后,输出ip头部中的源ip地址。

问题记录

  • sendto函数的地址参数,直接传入getaddrinfo()得到的res->ai_addr,总是返回invalid argument错误,自己填充一个sockaddr_in传入就好了。
  • 自己的ping程序测试的time要比Linux自带的高一些。

参考

  • 《TCP/IP详解(卷1:协议)》第二版

欢迎与我分享你的看法。 转载请注明出处:http://taowusheng.cn/

0 人点赞