文章目录
- 1. 抓包
- 2. ICMP协议数据报
- 3. ping程序原理
- 4. Cpp代码实现
- 5. traceroute
- 6. 问题记录
- 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/