ICMP协议与ping

ICMP协议

ICMP(Internet Control Message Protocol,Internet控制报文协议)是一种面向无连接的协议,是TCP/IP协议族的一个子协议,属于网络层协议,用于在IP主机、路由器之间传递控制消息(网络通不通、主机是否可达、路由是否可用等)。当遇到IP数据无法访问目标、IP路由器无法按当前的传输速率转发数据包等情况时,会自动发送ICMP消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。

ICMP类型的部分列表如下:

TYPE CODE Description Query Error
0 0 Echo Reply——回显应答(Ping应答) x
3 0 Network Unreachable——网络不可达 x
3 1 Host Unreachable——主机不可达 x
3 2 Protocol Unreachable——协议不可达 x
3 3 Port Unreachable——端口不可达 x
3 6 Destination network unknown——目的网络未知 x
3 7 Destination host unknown——目的主机未知 x
8 0 Echo request——回显请求(Ping请求) x

原始套接字

原始套接字提供普通TCP和UDP套接字所不提供的能力。具体如下:

  • 进程可以使用原始套接字读与写ICMPv4、ICMPv6和IGMPv4等分组。

    ping程序使用原始套接字发送ICMP回射请求并接收ICMP回射应答。
    
    #include 
    int sockfd; // 使用原始套接字创建ICMPv4套接字
    sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    
  • 大多数内核仅仅处理ICMP、IGMP、TCP和UDP的数据报,有了原始套接字,进程可以读写内核不处理其协议字段的IPv4数据报。

  • 使用原始套接字时,进程可使用IP_HDRINCL套接字选项自行构造IPv4首部。

    const int on = 1;
    if (setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0)
        printf("出错");
    
  • 原始套接字不存在端口号的概念。因此,在原始套接字上调用bind仅仅设置从这个原始套接字发送的所有数据报的源IP地址(未开启IP_HDRINCL选项的前提下)。在原始套接字上调用connect仅仅指定目的IP地址,调用connect之后可把sendto调用改为write或send调用。

原始套接字的输出规则

  • 普通输出调用sendto或sendmsg并指定目的IP地址完成。如果成功调用connect后也可调用
    write、writev或send。
  • 未开启IP_HDRINCL选项时,内核将根据socket函数第三个参数构造IPv4首部并把它置于来自进程的数据之前。
  • 开启IP_HDRINCL选项后,整个IPv4首部由进程构造,不过IPv4标识字段可置为0以让内核设置,而且IPv4首部校验和字段总是由内核计算并存储。另外,IPv4选项字段是可选的。
  • 内核会对超出外出接口MTU的原始分组执行分片。
  • 对于IPv4,进程必须负责IPv4首部之后数据报中包含的任何首部校验和的校验工作

原始套接字的输入规则

  • 接收到的UDP和TCP分组绝不传递到任何原始套接字。
  • 源自Berkeley的实现把不是回射请求、时间戳请求或地址掩码请求(这三类ICMP消息有内核处理)的所有ICMP分组传递到原始套接字。
  • 所有IGMP分组均传递到原始套接字。
  • 内核不认识其协议字段的IP数据报传递到原始套接字。
  • 内核不会传递分组中的单个或多个片段到原始套接字。
  • 内核将IP数据报传递到原始套接字时,因没有端口号的概念,内核会检查所有进程上的所有原始套接字,进行如下3个测试以寻找所匹配的原始套接字。

    • 接收到的数据报的协议字段是否匹配创建原始套接字时socket函数的第三个参数。
    • 如果在原始套接字上调用bind已经绑定了某个本地IP地址,接收到的数据报的目的IP地址是否匹配。
    • 如果此原始套接字已有connect调用指定了目的IP地址,接收到的数据报的源IP地址是否匹配。

注意,如果创建原始套接字socket函数的第三个参数为0值,且未调用过bind和connect,那么该原始套接字接收所有内核传递到原始套接字的数据报的一个副本。另外,内核传递到原始套接字的数据报一定是包括IPv4首部的完整数据报。

ping程序

此ping程序仅支持一个-v命令行选项,以打印详尽输出。为了追求最简化,也仅支持IPv4。ping程序的操作非常简单,往某个IP地址发送一个ICMP回射请求,该节点则以一个ICMP回射应答响应。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

/* C99中规定宏可以像函数一样带有可变参数 */
#define error_exit(format, ...) \
    do { fprintf(stderr, format"\n", ##__VA_ARGS__); exit(EXIT_FAILURE); } while (0)

#define BUFSIZE    1500

char     sendbuf[BUFSIZE];
int      datalen = 56;      // “可选数据”的长度
char    *host;              // 主机名或IP地址数串
int      nsent;             // 发送出去的分组“序列号” 
pid_t    pid;               // 进程ID
int      sockfd;            // 原始套接字描述符
int      verbose;           // "-v"命令行选项是否存在标记

struct proto {
  void (*fproc)(char *, ssize_t, struct msghdr *, struct timeval *);
  void (*fsend)(void);
  struct sockaddr *sasend;  // sockaddr{} for send, from getaddrinfo 
  struct sockaddr *sarecv;  // sockaddr{} for receiving 
  socklen_t salen;          // length of sockaddr{}s 
  int icmpproto;            // IPPROTO_xxx value for ICMP 
} *pr;

void* Signal(int signo, void (*func)(int)) {
    struct sigaction act, oact;

    act.sa_handler = func; 
    sigemptyset(&act.sa_mask); 
    act.sa_flags = 0;
#ifdef SA_INTERRUPT 
    if (signo == SIGALRM) act.sa_flags |= SA_INTERRUPT;
#endif
#ifdef SA_RESTART 
    if (signo != SIGALRM) act.sa_flags |= SA_RESTART; 
#endif
    if (sigaction(signo, &act, &oact) < 0) 
        return SIG_ERR;
    return oact.sa_handler; // 返回信号的旧行为
}

struct addrinfo* host_serv(const char *host, const char *serv, int family, int socktype) {
    int n;
    struct addrinfo hints, *res;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_flags = AI_CANONNAME;  // always return canonical name 
    hints.ai_family = family;       // 0, AF_INET, AF_INET6, etc
    hints.ai_socktype = socktype;   // 0, SOCK_STREAM, SOCK_DGRAM, etc
    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) {
        error_exit("host_serv error for %s, %s: %s",
            (host == NULL) ? "(no hostname)" : host,
            (serv == NULL) ? "(no service name)" : serv,
            gai_strerror(n));            
    }
    return(res);  // return pointer to first on linked list 
}

char* sock_ntop_host(const struct sockaddr *sa, socklen_t salen) {
    static char str[128];   // Unix domain is largest 

    switch (sa->sa_family) {
        case AF_INET: {
            struct sockaddr_in *sin = (struct sockaddr_in *)sa;
            if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
                return(NULL);
            return(str);            
        }
        default:
            snprintf(str, sizeof(str), "sock_ntop_host: unknown AF_xxx: %d, len %d", sa->sa_family, salen);
            return(str);
    }
    return (NULL);
}

/* 相减两个timeval时间,结果放到out中 */
static void tv_sub(struct timeval *out, struct timeval *in) {
    if ((out->tv_usec -= in->tv_usec) < 0) { // out -= in 
        --out->tv_sec;
        out->tv_usec += 1000000;
    }
    out->tv_sec -= in->tv_sec;
}

/* 处理所有接收到的ICMPv4消息。
 * (当一个ICMPv4消息由进程在原始套接字上收取时,内核已经证实它的IPv4首部和ICMPv4首部中的基本字段的有效性) 
 *
 *  |--------------------len-------------------|
 *  |--------hlen1--------|-------icmplen------|
 *  ++++++++++++++++++++++++++++++++++++++++++++
 *  + IPv4  +    IPv4     + ICMPv4 +    ICMP   +
 *  + 首部  +    选项     +  首部  +    数据   +
 *  ++++++++++++++++++++++++++++++++++++++++++++
 *  |--20B--|----0~40B----|---8B---|-----------|
 *  [ip]                  [icmp]
 *
 *  (图:处理ICMPv4应答涉及的首部、指针和长度)
 ************************************************/
void proc_v4(char *ptr, ssize_t len, struct msghdr *msg, struct timeval *tvrecv) {
    int hlen1, icmplen;
    double rtt;
    struct ip *ip;
    struct icmp *icmp;
    struct timeval *tvsend;

    ip = (struct ip *)ptr;        // start of IP header 
    hlen1 = ip->ip_hl << 2;       // length of IP header 
    if (ip->ip_p != IPPROTO_ICMP) // 检查ICMP标识符字段
        return; // not ICMP 

    icmp = (struct icmp *)(ptr + hlen1);  // start of ICMP header 
    if ((icmplen = len - hlen1) < 8)      // 是否为完整ICMP数据
        return; // malformed packet 

    if (icmp->icmp_type == ICMP_ECHOREPLY) { // ICMP消息的“类型”
        if (icmp->icmp_id != pid)            // ICMP消息的“标识符”
            return; // not a response to our ECHO_REQUEST 
        if (icmplen < 16)
            return; // not enough data to use 

        tvsend = (struct timeval *)icmp->icmp_data; // ICMP消息的“可选数据”
        tv_sub(tvrecv, tvsend);
        rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0; // 计算RTT时间
        printf("%d bytes from %s: seq=%u, ttl=%d, rtt=%.3f ms\n",
                icmplen, sock_ntop_host(pr->sarecv, pr->salen),
                icmp->icmp_seq, ip->ip_ttl, rtt);
    } 

    /* 若指定-v(详尽输出)则显示所有接收ICMP消息 */
    if (verbose) { 
        printf("  %d bytes from %s: type = %d, code = %d\n",
                icmplen, sock_ntop_host(pr->sarecv, pr->salen),
                icmp->icmp_type, icmp->icmp_code);
    }
}

/* 计算网际网校验和(具体是指被校验的各个16位值的二进制反码和) */
static uint16_t in_cksum(uint16_t *addr, int len) {
    int nleft = len;
    uint32_t sum = 0;
    uint16_t *w = addr;
    uint16_t answer = 0;

    /* Our algorithm is simple, using a 32 bit accumulator (sum), we add
     * sequential 16 bit words to it, and at the end, fold back all the
     * carry bits from the top 16 bits into the lower 16 bits. */
    while (nleft > 1)  {
        sum += *w++;
        nleft -= 2;
    }
    if (nleft == 1) {
        *(unsigned char *)(&answer) = *(unsigned char *)w ;
        sum += answer;
    }

    /* add back carry outs from top 16 bits to low 16 bits */
    sum = (sum >> 16) + (sum & 0xffff); // sum的高16位与低16位第一次相加
    sum += (sum >> 16);                 // 将上一步可能产生的高16位进位再次与低16位累加  
    answer = ~sum;                      // truncate to 16 bits 
    return(answer);
}


/* 发送ICMPv4回射请求消息。 
 *
 * 0_______7.8_____15.16_____________31
 * |__类型__|__代码__|_____校验和_____|
 * |______标识符_____|_____序列号_____|
 * |                                  |
 * .             可选数据             .
 * |__________________________________|
 *
 *        (图:ICMPv4消息的格式)
 ************************************************/
void send_v4(void) {
    int len;
    struct icmp *icmp;

    /* 构造ICMPv4消息 */
    icmp = (struct icmp *)sendbuf;
    icmp->icmp_type = ICMP_ECHO;    // 消息的“类型”
    icmp->icmp_code = 0;            // 消息的“代码”值为0
    icmp->icmp_id = pid;            // 消息的“标识符”使用ping进程的PID
    icmp->icmp_seq = nsent++;       // 消息的“序列号”递增
    memset(icmp->icmp_data, 0xa5, datalen);                // 消息的“可选数据”填充0xa5
    gettimeofday((struct timeval *)icmp->icmp_data, NULL); // 消息的“可选数据”开始处存放发送时刻的8字节时间戳

    /* 计算ICMP校验和 */
    len = 8 + datalen;              // ICMP“首部”和“可选数据”的长度和
    icmp->icmp_cksum = 0;           // 计算校验和之前,要将校验和字段置为0
    icmp->icmp_cksum = in_cksum((u_short *)icmp, len);

    /* 发送数据报(由于没有开启IP_HDRINCL选项,内核将构造IPv4首部并安置在上述ICMPv4消息缓冲区之前) */
    sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen);
}

/* 每隔1s发送一个ICMP回射请求 */
static void sig_alrm(int signo) {
    (*pr->fsend)();

    alarm(1); 
    return;
}

void readloop(void) {
    int size;
    char recvbuf[BUFSIZE];
    char controlbuf[BUFSIZE];
    struct msghdr msg;
    struct iovec iov;
    ssize_t n;
    struct timeval tval;

    /* 创建原始套接字(需超级用户特权) */
    sockfd = socket(pr->sasend->sa_family, SOCK_RAW, pr->icmpproto); // 进程必须拥有超级用户特权才能创建原始套接字
    setuid(getuid()); // 把进程的有效用户ID设置为实际用户ID,使得进程放弃对超级用户特权的拥有(防攻击)

    /* 设置套接字接收缓冲区的大小,防止接收缓冲区溢出 */
    size = 60 * 1024; 
    setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));

    /* 启动回射请求,由SIGALRM信号每秒钟驱动一次 */
    sig_alrm(SIGALRM);  

    /* 在一个原始套接字上读入收到的每个分组,显示ICMP回射应答 */
    iov.iov_base = recvbuf;
    iov.iov_len = sizeof(recvbuf);
    msg.msg_name = pr->sarecv;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = controlbuf;
    for ( ; ; ) {
        msg.msg_namelen = pr->salen;
        msg.msg_controllen = sizeof(controlbuf);
        n = recvmsg(sockfd, &msg, 0);  // 读入回射到原始ICMP套接字的每个分组
        if (n < 0) {
            if (errno == EINTR)
                continue;
            else 
                error_exit("recvmsg(%d)=%d", sockfd, n);
        }

        gettimeofday(&tval, NULL);    // 记录分组收取时刻,用于计算RTT
        (*pr->fproc)(recvbuf, n, &msg, &tval);
    }
}

int main(int argc, char **argv) {
    struct proto proto_v4 = { proc_v4, send_v4, NULL, NULL, 0, IPPROTO_ICMP };  // IPv4的proto结构
    int c;
    struct addrinfo *ai;
    char *h;

    /* getopt被用来解析命令行选项参数。调用一次,返回一个选项。
     * extern int opterr;   //当opterr=0时,getopt不向stderr输出错误信息
     * extern int optopt;   //未知选项存储在optopt中,并且getopt返回'?'
     * extern int optind;   //getopt要处理的argv中的下一个字符选项的索引
     * extern char *optarg; //选项的参数指针    
     * 当再也检查不到包含的选项时,getopt返回-1,同时optind储存第一个不包含选项的命令行参数的索引。*/
    opterr = 0; 
    while ((c = getopt(argc, argv, "v")) != -1) { // 此程序只有-v选项
        switch (c) {
            case 'v':        // -v选项
                verbose++;
                break;
            case '?':        // 未知选项
                error_exit("unrecognized option: %c", optopt);
        }
    }
    if (optind != argc-1) 
        error_exit("usage: ping [ -v ] ");    

    host = argv[optind];       //  主机名或IP地址数串
    pid = getpid() & 0xffff;   // ICMP ID field is 16 bits 
    Signal(SIGALRM, sig_alrm);

    ai = host_serv(host, NULL, 0, 0);                // 处理主机名或IP地址数串,返回addrinfo结构中含有协议族AF_INET等信息
    h = sock_ntop_host(ai->ai_addr, ai->ai_addrlen); // 从套接字地址结构中得到主机IP信息
    printf("PING %s (%s): %d data bytes\n", ai->ai_canonname ? ai->ai_canonname : h, h, datalen);       
    if (ai->ai_family == AF_INET) 
        pr = &proto_v4;
    else 
        error_exit("unknown address family %d", ai->ai_family);     

    pr->sasend = ai->ai_addr;
    pr->sarecv = calloc(1, ai->ai_addrlen);
    pr->salen = ai->ai_addrlen;

    readloop();

    free(pr->sarecv);
    freeaddrinfo(ai);
    exit(0);
}

测试:

:~$ sudo ./ping www.baidu.com
PING www.baidu.com (123.125.114.144): 56 data bytes
64 bytes from 123.125.114.144: seq=0, ttl=52, rtt=37.399 ms
64 bytes from 123.125.114.144: seq=1, ttl=52, rtt=36.838 ms
64 bytes from 123.125.114.144: seq=2, ttl=52, rtt=62.450 ms

:~$ sudo ./ping -v www.baidu.com
PING www.baidu.com (123.125.114.144): 56 data bytes
64 bytes from 123.125.114.144: seq=0, ttl=52, rtt=52.391 ms
  64 bytes from 123.125.114.144: type = 0, code = 0
64 bytes from 123.125.114.144: seq=1, ttl=52, rtt=37.155 ms
  64 bytes from 123.125.114.144: type = 0, code = 0
64 bytes from 123.125.114.144: seq=2, ttl=52, rtt=36.360 ms
  64 bytes from 123.125.114.144: type = 0, code = 0

:~$ sudo ./ping -v 123.3.4.5
PING 123.3.4.5 (123.3.4.5): 56 data bytes
  36 bytes from 203.134.25.118: type = 3, code = 1
  36 bytes from 203.134.25.118: type = 3, code = 1

你可能感兴趣的:(▷--○,socket,▷,Protocol)