ICMP协议是做什么的
ICMP是"Internet Control Message Protocol"(Internet控制消息协议)的缩写。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。
ICMP协议是一种面向无连接的协议,用于传输出错报告控制信息。它是一个非常重要的协议,它对于网络安全具有极其重要的意义。
主要作用:主机探测、路由维护、路由选择、流量控制。我们常用的ping命令和traceroute命令都是通过ICMP协议来实现的。
ICMP使用的是什么端口(一个面试陷阱)?
Linux下端口的划分和使用是由IANA(Internet Assigned Numbers Authority,因特网已分配数值权威机构)维护的,端口号被划分为3个段:
0~1023,这些端口由IANA分配和控制,可能的话,相同端口号就分配给TCP、UDP和SCTP的同一给定服务。如80端口被赋予web服务
102449151,这些端口不受IANA控制,不过由IANA登记并提供他们的使用情况清单,已方便整个群体。相同端口号也分配给TCP和UDP的同一给定服务。如60006003端口分配给这两种协议的X Window服务器。
49152~65535,动态端口。IANA不管这些端口,就是我们所说的临时端口。(49152这个魔数是65536的四分之三)。
IANA端口号查询地址如下:
http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml
常用端口号:
服务名 | 端口号 |
---|---|
FTP | 21 |
SSH (Secure Shell) | 22 |
TELNET | 23 |
MAIL(smtp\pop3) | 25\110 |
DNS | 53 |
DHCP | 67 |
HTTP | 80 |
windows远程终端 | 3389 |
tomcat | 8080 |
mysql | 3306 |
ORACLE | 1521、1526 |
ETH,IP,TCP层的报文格式
我们先来回顾一下ETH,IP,TCP层的报文格式
ETH层记录了MAC地址,IP层记录了IP地址,TCP层记录了PORT。这样TCP协议就可以通过Socket(IP:PORT)来识别对方了。
/*
* 1. eth level[14Bytes]
* EthHeader{dst:6, src:6, type:2}
* EthData(RFC894)
* ----------------------------------------------------------------------
* | 6Bytes | 6Bytes | 2Bytes | 46~1500Bytes | 4Bytes |
* ----------------------------------------------------------------------
* | DstMac | SrcMac | Type | Data | CRC | : ETH
* | 0806 | ARP(28) | PAD(18) |
* | 0835 | PARP(28) | PAD(18) |
* | 0800 | IP(4~1500) |
* | IPHeader | IPData | : IP
* | TCPHeader | TCPData | : TCP
* | APPData | : APP
*
* 2. IP level[20Bytes]
* IPPacket:{IPHeader(20) + TCPHeader(20+) + [TCPData]}
* ====================================================
* 0---------8---------15-------23--------31
* | v_hl(1) | tos(1) | len(2) |
* | id(2) | offset(2) |
* | ttl(1) | proto(1) | checksum(2) |
* | src(4) |
* | dst(4) |
* $ option $
* $ data $
* ====================================================
* IPHeader(20){v_hl:1, tos:1, len:2, id:2, offset:2, ttl:1, proto:1, checksum:2, src:4, dst:4}
*
* 3. TCP level[20+Bytes]
* ====================================================
* 0---------8---------15-------23--------31
* | src port(2) | dst port(2) |
* | seq(4) |
* | ack(4) |
* |hdlen(1) |flags(1) | window size(2) |
* | checksum(2) | urgent pointer(2) |
* $ option $
* $ data $
* TCPHeader{srcport:2, dstport:2, seq:4, ack:4, hdr_len:1, flags:1, winsize:2, checksum:2, urgent:2}
*/
ICMP报文格式
详细信息:https://www.rfc-editor.org/info/rfc792
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unused |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Internet Header + 64 bits of Original Data Datagram |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct icmphdr
{
u_int8_t type; /* message type */
u_int8_t code; /* type sub-code */
u_int16_t checksum;
union
{
struct
{
u_int16_t id;
u_int16_t sequence;
} echo; /* echo datagram */
u_int32_t gateway; /* gateway address */
struct
{
u_int16_t __unused;
u_int16_t mtu;
} frag; /* path mtu discovery */
} un;
};
type:一个 8 位类型字段,表示 ICMP 数据包类型;
code:一个 8 位代码域,表示指定类型中的一个功能,如果一个类型中只有一种功能,代码域置为 0;
checksum:数据包中 ICMP 部分上的一个 16 位检验和;
ICMP报文中没有PORT,其是根据id+sequence来进行目标判断的。
常见的ICMP报文类型
ICMP提供多种类型的消息为源端节点提供网络层的故障信息反馈,它的报文类型可以归纳为以下5个大类:
诊断报文(类型8,代码0;类型0,代码0);
目的不可达报文(类型3,代码0-15);
重定向报文(类型5,代码0-4);
超时报文(类型11,代码0-1);
信息报文(类型12-18)。
回音(Echo)属于资讯信息。ping命令就是利用了该类型的ICMP包。当使用ping命令的时候,将向目标主机发送Echo-询问类型的ICMP包,而目标主机在接收到该ICMP包之后,会回复Echo-回答类型的ICMP包,并将询问ICMP包包含在数据部分。ping命令是我们进行网络排查的一个重要工具。例如一个IP地址可以通过ping命令收到回复,那么其他的网络协议通信方式也很有可能成功。
源头冷却(source quench)属于错误消息。如果某个主机快速的向目的传送数据,而目的地主机没有匹配的处理能力,目的主机可以向出发主机发出给类型的ICMP包,提醒出发主机放慢发送速度。
目的地无法到达(Destination Uncreachale)属于错误信息。如果一个路由器接收到一个没办法进一步接力的IP包,它会想出发主机发送该类型的ICMP包。比如当IP包到达最后一个路由器,路由器发现目的地主机down机,就会向出发主机发送目的地无法到达(Destinaton Unreacherable)类型的ICMP包。目的地无法到达还有其他可能的原因,比如不存在接力路径,比如不被接收的端口号等等。
超时(Time Exceded)属于错误信息。IPV4中的Time to Live(TTL)和IPV6中的Hop Limit会随着经过的路由器而递减,当这个区域值减为0时,就认为该IP包超时(Time Exceeded)。Time Exceeded就是TTL减为0时路由器发给出发地主机的ICMP包,通知它发生了超时错误。 traceroute就利用了这种类型的ICMP包。traceroute命令用来发现IP接力路径上的各个路由器。它向目的地发送IP包,第一次的时候,将TTL设为1,引发第一个路由器的Time Exceeded错误。这样,第一个路由器恢复ICMP包,从而让出发主机知道途径的第一个路由器的信息。随后TTL被设置为2,,3,4。。。直到到达目的主机。这样,沿途的每个路由器都会向出发主机发送ICMP包来汇报错误。traceroute将ICMP包的信息打印在屏幕上,就是接力路径的信息了。
重新定向(redirect)属于错误信息。当一个路由器收到一个IP包,对照其routing table,发现自己不应该受到该IP包,它会向出发主机发送重新定向类型的ICMP,提醒出发主机修改自己的routing table。比如下面的网络:
参见:https://www.cnblogs.com/jingmoxukong/p/3811262.html
这是一个完整的ICMP类型的列表:
TYPE | CODE | Description |
---|---|---|
0 | 0 | Echo Reply--回显应答(Ping应答) |
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--优先中止生效 |
4 | 0 | Source quench--源端被关闭(基本流控制) |
5 | 0 | Redirect for network--对网络重定向 |
5 | 1 | Redirect for host--对主机重定向 |
5 | 2 | Redirect for TOS and network--对服务类型和网络重定向 |
5 | 3 | Redirect for TOS and host--对服务类型和主机重定向 |
8 | 0 | Echo request--回显请求(Ping请求) |
9 | 0 | Router advertisement--路由器通告 |
10 | 0 | Route solicitation--路由器请求 |
11 | 0 | TTL equals 0 during transit--传输期间生存时间为0 |
11 | 1 | TTL equals 0 during reassembly--在数据报组装期间生存时间为0 |
12 | 0 | IP header bad (catchall error)--坏的IP首部(包括各种差错) |
12 | 1 | Required options missing--缺少必需的选项 |
13 | 0 | Timestamp request (obsolete)--时间戳请求(作废不用) |
14 | 0 | Timestamp reply (obsolete)--时间戳应答(作废不用) |
15 | 0 | Information request (obsolete)--信息请求(作废不用) |
16 | 0 | Information reply (obsolete)--信息应答(作废不用) |
17 | 0 | Address mask request--地址掩码请求 |
18 | 0 | Address mask reply--地址掩码应答 |
如何判断目标主机是否可达
源主机向目标主机发送ICMP回显请求数据包,然后等待目标主机的回答。
目标主机在收到一个ICMP回显请求数据包后,它会交换源、目的主机的地址,然后将收到的ICMP回显请求数据包中的数据部分原封不动地封装在自己的ICMP回显应答数据包中,然后发回给发送ICMP回显请求的一方。
源主机收到回显应答报文核对id和seq后,便认为目标主机的回显服务正常,也即物理连接畅通。
简单来说:向目标主机发送ECHO报文,接收ECHO_REPLY报文,则认为目标主机可达。
需要注意
ICMP协议是IP协议的排错助手,它可以帮助人们及时发现IP通信中出现的故障。基于ICMP的ping和traceroute也构成了重要的网络诊断工具。
然而需要注意的是,尽管ICMP的设计是出于好的意图,但ICMP却经常被黑客借用进行网络攻击,比如利用伪造的IP包引发大量的ICMP回复,并将这些ICMP包导向受害主机,从而形成DoS攻击。
而redirect类型的ICMP包可以引起某个主机更改自己的routing table,所以也被用做攻击工具。许多站点选择忽视某些类型的ICMP包来提供自身的安全性。
C语言使用ICMP报文实现ping命令
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/ip_icmp.h>
#define ICMP_PACKET_SIZE 16
#define TIMEOUT_SECONDS 2
uint16_t ident=0;
const char *ping_desc(uint8_t type, uint8_t code)
{
switch(type)
{
case ICMP_ECHOREPLY:
return "Echo Reply";
case ICMP_ECHO:
return "Echo Request";
case ICMP_PARAMPROB:
return "Bad Parameter";
case ICMP_SOURCEQUENCH:
return "Packet lost, slow down";
case ICMP_TSTAMP:
return "Timestamp Request";
case ICMP_TSTAMPREPLY:
return "Timestamp Reply";
case ICMP_IREQ:
return "Information Request";
case ICMP_IREQREPLY:
return "Information Reply";
case ICMP_UNREACH:
switch(code)
{
case ICMP_UNREACH_NET:
return "Unreachable Network";
case ICMP_UNREACH_HOST:
return "Unreachable Host";
case ICMP_UNREACH_PROTOCOL:
return "Unreachable Protocol";
case ICMP_UNREACH_PORT:
return "Unreachable Port";
case ICMP_UNREACH_NEEDFRAG:
return "Unreachable: Fragmentation needed";
case ICMP_UNREACH_SRCFAIL:
return "Unreachable Source Route";
case ICMP_UNREACH_NET_UNKNOWN:
return "Unknown Network";
case ICMP_UNREACH_HOST_UNKNOWN:
return "Unknown Host";
case ICMP_UNREACH_ISOLATED:
return "Unreachable: Isolated";
case ICMP_UNREACH_NET_PROHIB:
return "Prohibited network";
case ICMP_UNREACH_HOST_PROHIB:
return "Prohibited host";
case ICMP_UNREACH_FILTER_PROHIB:
return "Unreachable: Prohibited filter";
case ICMP_UNREACH_TOSNET:
return "Unreachable: Type of Service and Network";
case ICMP_UNREACH_TOSHOST:
return "Unreachable: Type of Service and Host";
case ICMP_UNREACH_HOST_PRECEDENCE:
return "Unreachable: Prec vio";
case ICMP_UNREACH_PRECEDENCE_CUTOFF:
return "Unreachable: Prec cutoff";
default:
return "Unreachable: Unknown Subtype";
}
break;
case ICMP_REDIRECT:
switch(code)
{
case ICMP_REDIRECT_NET:
return "Redirect: Network";
case ICMP_REDIRECT_HOST:
return "Redirect: Host";
case ICMP_REDIRECT_TOSNET:
return "Redirect: Type of Service and Network";
case ICMP_REDIRECT_TOSHOST:
return "Redirect: Type of Service and Host";
default:
return "Redirect: Unknown Subtype";
}
case ICMP_TIMXCEED:
switch(code)
{
case ICMP_TIMXCEED_INTRANS:
return "Timeout: TTL";
case ICMP_TIMXCEED_REASS:
return "Timeout: Fragmentation reassembly";
default:
return "Timeout: Unknown Subtype";
}
break;
default:
return "Unknown type";
}
}
uint16_t chksum(unsigned short *buf, int len)
{
uint32_t sum = 0;
while(len > 1) {
sum += *buf;
buf++;
len -= 2;
}
if(len)
sum += (*(uint8_t *)buf);
sum = (sum >> 16) + (sum & 0xFFFF);
sum = (sum >> 16) + sum;
return ~sum;
}
int ping_write(char *buf, int seq)
{
struct timeval tv;
struct icmp *icmp = (struct icmp *)buf;
icmp->icmp_type = ICMP_ECHO;
icmp->icmp_code = 0;
icmp->icmp_id = ident;
icmp->icmp_seq = seq;
icmp->icmp_cksum = 0;
gettimeofday(&tv, NULL);
memcpy(buf + 8, &tv, sizeof(tv));
icmp->icmp_cksum = chksum((unsigned short *)icmp, ICMP_PACKET_SIZE);
// printf("send ICMP packet(type:%d, code:%d, id:%d, seq:%d, checksum:%d\n",
// icmp->icmp_type, icmp->icmp_code, icmp->icmp_id, icmp->icmp_seq, icmp->icmp_cksum);
}
int ping_read(void *buf, struct sockaddr_in answer)
{
uint16_t chk_sum;
struct timeval tv_send, tv_now;
unsigned int mini_sec;
struct ip *ip = (struct ip *)buf;
struct icmp *icmp = (struct icmp *)(buf + (ip->ip_hl << 2));
// printf("recv ICMP packet(type:%d, code:%d, id:%d, seq:%d, checksum:%d\n",
// icmp->icmp_type, icmp->icmp_code, icmp->icmp_id, icmp->icmp_seq, icmp->icmp_cksum);
if(icmp->icmp_id != ident) {
// printf("ERROR: bad icmp_id %d\n", icmp->icmp_id);
return -1;
}
if(icmp->icmp_type != ICMP_ECHOREPLY) {
printf("ERROR: %s\n", ping_desc(icmp->icmp_type, icmp->icmp_code));
return -1;
}
chk_sum = icmp->icmp_cksum;
icmp->icmp_cksum = 0;
if(chk_sum != chksum((unsigned short *)icmp, ICMP_PACKET_SIZE)) {
printf("ERROR: bad chksum\n");
return -1;
}
gettimeofday(&tv_now, NULL);
memcpy(&tv_send, ((char *)icmp + 8), sizeof(tv_send));
mini_sec = (tv_now.tv_sec - tv_send.tv_sec) * 1000000 + (tv_now.tv_usec - tv_send.tv_usec);
printf("%d bytes from %s: icmp_seq=%d, ttl=%d, time=%.3f ms\n",
(ip->ip_hl << 2) + 16,
inet_ntoa(answer.sin_addr),
icmp->icmp_seq, ip->ip_ttl, mini_sec/1000.0);
return 0;
}
int main(int argc, char *argv[])
{
struct hostent *host;
struct sockaddr_in dest;
struct sockaddr_in answer;
int answer_len = sizeof(answer);
int sock_raw_fd, seq=0;
char send_buf[ICMP_PACKET_SIZE];
char recv_buf[50];
if(argc != 2) {
printf("Usage: %s ipaddr/hostname\n", argv[0]);
exit(1);
}
if ((host = gethostbyname(argv[1])) == NULL) {
printf("ping: unknow host %s \n", argv[1]);
exit(1);
}
ident = getpid() & 0xFFFF;
bzero(&dest, sizeof(dest));
dest.sin_family = AF_INET;
memcpy(&dest.sin_addr, host->h_addr, sizeof(int));
dest.sin_port = ntohs(0);
if ((sock_raw_fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) == -1) {
perror("socket");
exit(1);
}
while(seq < 5) {
seq++;
// printf("\nid:%d\n", seq);
ping_write(send_buf, seq);
if (sendto(sock_raw_fd, (const char*)send_buf, ICMP_PACKET_SIZE, 0, (struct sockaddr *)&dest, sizeof(dest)) == -1) {
perror("sendto");
exit(1);
}
while(1) {
int ret = 0;
struct timeval tv;
fd_set readset;
FD_ZERO(&readset);
FD_SET(sock_raw_fd, &readset);
tv.tv_sec = TIMEOUT_SECONDS;
tv.tv_usec = 0;
if ((ret = select(sock_raw_fd+1, &readset, NULL, NULL, &tv)) == -1) {
perror("select");
exit(1);
}
if(ret == 0) {
printf("time out.\n");
break;
} else {
if(recvfrom(sock_raw_fd, recv_buf, 36, 0, (struct sockaddr *)&answer, &answer_len) == -1) {
perror("select");
exit(1);
} else {
if (ping_read(recv_buf, answer) == 0)
break;
}
}
}
sleep(1);
}
return 0;
}