ICMP报文的介绍和使用

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)。

  1. 回音(Echo)属于资讯信息。ping命令就是利用了该类型的ICMP包。当使用ping命令的时候,将向目标主机发送Echo-询问类型的ICMP包,而目标主机在接收到该ICMP包之后,会回复Echo-回答类型的ICMP包,并将询问ICMP包包含在数据部分。ping命令是我们进行网络排查的一个重要工具。例如一个IP地址可以通过ping命令收到回复,那么其他的网络协议通信方式也很有可能成功。

  2. 源头冷却(source quench)属于错误消息。如果某个主机快速的向目的传送数据,而目的地主机没有匹配的处理能力,目的主机可以向出发主机发出给类型的ICMP包,提醒出发主机放慢发送速度。

  3. 目的地无法到达(Destination Uncreachale)属于错误信息。如果一个路由器接收到一个没办法进一步接力的IP包,它会想出发主机发送该类型的ICMP包。比如当IP包到达最后一个路由器,路由器发现目的地主机down机,就会向出发主机发送目的地无法到达(Destinaton Unreacherable)类型的ICMP包。目的地无法到达还有其他可能的原因,比如不存在接力路径,比如不被接收的端口号等等。

  4. 超时(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包的信息打印在屏幕上,就是接力路径的信息了。

  5. 重新定向(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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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

你可能感兴趣的:(ICMP报文的介绍和使用)