ICMPを使ったネットワークプログラム


今回はネットワークとプログラミング両方の学習に役立つ簡単なtracerouteを実装します。
ネットワークプログラミングのいい勉強になりますし、そこそこ格好いいのでいろいろと意味があると思います。

このツールを利用すると通信先マシンへの経路上にあるマシンのIPアドレスなどの情報を取得することができます。

原理をまとめそれを実装してパケットがどのようにネットワークを移動するのか見ていきます。



そもそもどのように対象のマシンへパケットが送られるか

僕も普段何気なくインターネットを利用していますが、
中を覗いてみるとそこそこ面倒なことをやっているようです。

インターネットのどこかのマシンと通信する際は直接そのマシンとやり取りしているわけではなく、ルーターを経由してパケットを送りあっています。

通信先との間にルーターがあるわけですが何台あるかはわかりません。
例えば5台あるとすれば5回ルーターを経由して対象のマシンへとパケットが送られます

time to live

しかしIPパケットにはtime to live(IPv6ではhop limit)という寿命のようなものがあります。
このtime to liveというのは簡単に言うとルーターを何台超えることができるかを定めた値です。

たとえば対象マシンとの間にルーターが5台あり、time to liveが3だとするとこのパケットは対象のマシンへと到達する前に破棄されてしまいます。
それはtime to liveが3であるため3回しかルーターを超えることができないからです。
ルーター(ルートのホスト)を通るたびにtime to liveはデクリメントされていきます。
そしてゼロになるとTimeExceededを知らせるICMPパケットが送信者に向けて返却されます。

ルートスキャンというものはこの原理を利用しています。

原理

time to liveがゼロになるとTime Exceededを知らせるICMPパケットが送信者に向けて返却されると書きました。

このICMPパケットにはICMPパケットを発行したルーターのIPアドレスが格納されています。
とういことはルート上にルーターが5台あるとして、
time to liveを1にすれば、自分のマシンの、一つ先のマシンの情報が取得できるということです。

一つ先のマシンからICMPパケットを受信したら、次はtime to liveを2に設定して、通信したい相手へとパケットを送信します。
そうすると2つ先のマシンがICMPパケットを返却してきます。(time to liveが2だから)
そして2つ先のマシンの情報も取得できます。

これを最終到達地点のマシンへパケットが届くまで、time to liveをインクリメントして繰り返すのです。

こうすることで、ルート上のマシンの情報が取得できるということです。

実装


冷静になって実装します。

まずはソケットを作成する部分です。
今回はicmpパケットを送受信します。
特にこれといったことはやっていません。
ソケットの作成に成功したらディスクリプタを、失敗したら-1を返却します。

// ICMPV ソケット作成
int make_socket() {
  int soc;

  if ( (soc = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) == -1) {
    perror("socket");
    return -1;
  }

  int opt = 1;
  if (setsockopt(soc, IPPROTO_IP, IP_HDRINCL, &opt, sizeof(opt)) == -1) {
    perror("setsockopt");
    close(soc);
    return -1;
  }

  return soc;
}


次は早速送受信です。

ipヘッダーとicmpヘッダーに諸々値を設定します。
今回はicmp echo requestパケットを送信するためicmp_typeにICMP_ECHOを設定します。

Time to liveが0になれば時間超過を知らせるtime exceededパケットが返却され、
ターゲットにパケットが到達すればecho replyが返却されます。

その解析をanalyze関数にお任せします。
echo replyが返却された場合は1、
time exceededが返却された場合0、
それ以外は一応2を返却します。

analyze関数から「1」が返却された場合は最終目標に到達したわけですから、
パケット送信ループを抜けます。
「0」の場合はTime to liveをインクリメントして送信処理を続行します。


// パケット送受信
void send_recv_packet(int soc, char *dst) {
  // 受信パケット解析用
  struct ip *ip;
  struct icmp *icmp;

  struct sockaddr_in sock_in;
  memset(&sock_in, 0, sizeof(sock_in));

  struct ip_icmp ip_icmp;
  memset(&ip_icmp, 0, sizeof(ip_icmp));

  // パケット設定
  ip_icmp.ip.ip_v = 4;
  ip_icmp.ip.ip_hl = 5;
  ip_icmp.ip.ip_id = htons(1);
  ip_icmp.ip.ip_off = 0;
  ip_icmp.ip.ip_len = htons(sizeof(struct ip_icmp));
  ip_icmp.ip.ip_p = IPPROTO_ICMP;
  inet_pton(AF_INET, dst, &ip_icmp.ip.ip_dst);

  ip_icmp.icmp.icmp_type = ICMP_ECHO;
  ip_icmp.icmp.icmp_code = 0;
  ip_icmp.icmp.icmp_cksum = 0;
  ip_icmp.icmp.icmp_id = 1;
  ip_icmp.icmp.icmp_seq = 1;
  ip_icmp.icmp.icmp_cksum = checksum((uint16_t *)&ip_icmp.icmp,
                                                 sizeof(ip_icmp.icmp));

  // アドレス情報
  sock_in.sin_family = AF_INET;
  inet_pton(AF_INET, dst, &sock_in.sin_addr);

  int reach = 0;

  // 一応255まで
  for (int i=1; i<256; i++) {
    char res[512], *p;

    ip_icmp.ip.ip_ttl = i;
    // 3回
    for (int j=0; j<3; j++) {

      fd_set ready;
      struct timeval tv;

      FD_ZERO(&ready);
      FD_SET(soc, &ready);

      tv.tv_sec = 3;
      tv.tv_usec = 0;

      int r = sendto(
                  soc,
                  (char *)&ip_icmp,
                  sizeof(ip_icmp),
                  0,
                  (struct sockaddr *)&sock_in,
                  sizeof(struct sockaddr_in)
                );
      if (r == -1) {
        perror("sendto");
        return;
      }

      int sel_res = select( (soc+1), (fd_set *)&ready, NULL, NULL, &tv);
      if (sel_res == 0) {
        if (j == 0) {
          printf("- Time to live %d ? ", i);
        } else if (j == 2) {
          printf("?\n");
        } else {
          printf("? ");
        }
        fflush(stdout);
      } else if (sel_res == -1){

      } else if (sel_res > 0) {
        if (FD_ISSET(soc, &ready)) {
          if (recvfrom(soc, res, sizeof(res), 0, NULL, NULL) == -1) {
            perror("recvfrom");
          }

          p = res;
          ip = (struct ip *)p;
          p += sizeof(struct ip);
          if (ip->ip_p == IPPROTO_ICMP) {
            char ip_addr[128];
            icmp = (struct icmp *)p;
            int result = analyze(icmp);
            inet_ntop(AF_INET, &ip->ip_src, ip_addr, sizeof(ip_addr));
            if (result == 1) {
              printf("- Reach Time to live %d from %s\n",i, ip_addr);
              reach = 1;
              break;
            } else if (result == 0) {
              printf("- Time to live %d from %s\n",i, ip_addr);
              break;
            }
          }
        }
      }

    }
    if (reach == 1) {
      break;
    }
  }
}

ソースコード


最終的にこのようなプログラムになります。

#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

enum {
  COMMAND, DST
};

struct ip_icmp {
  struct ip ip;
  struct icmp icmp;
};

int analyze(struct icmp *icmp);
uint16_t checksum(uint16_t *data, int size);
int make_socket();
void send_recv_packet(int soc, char *dst);

uint16_t checksum(uint16_t *data, int size) {
  uint32_t sum = 0;
  for (;size > 1; size -= 2) {
    sum += *data++;
    if (sum & 0x80000000) {
      sum = (sum >> 16) + (sum & 0xffff);
    }
  }

  if (size == 1) {
    uint8_t odd;
    odd = *(uint8_t *)data;
    sum += odd;
  }

  while(sum >> 16) {
    sum = (sum >> 16) + (sum & 0xffff);
  }

  return ~sum;
}

// パケット送受信
void send_recv_packet(int soc, char *dst) {
  // 受信パケット解析用
  struct ip *ip;
  struct icmp *icmp;

  struct sockaddr_in sock_in;
  memset(&sock_in, 0, sizeof(sock_in));

  struct ip_icmp ip_icmp;
  memset(&ip_icmp, 0, sizeof(ip_icmp));

  // パケット設定
  ip_icmp.ip.ip_v = 4;
  ip_icmp.ip.ip_hl = 5;
  ip_icmp.ip.ip_id = htons(1);
  ip_icmp.ip.ip_off = 0;
  ip_icmp.ip.ip_len = htons(sizeof(struct ip_icmp));
  ip_icmp.ip.ip_p = IPPROTO_ICMP;
  inet_pton(AF_INET, dst, &ip_icmp.ip.ip_dst);

  ip_icmp.icmp.icmp_type = ICMP_ECHO;
  ip_icmp.icmp.icmp_code = 0;
  ip_icmp.icmp.icmp_cksum = 0;
  ip_icmp.icmp.icmp_id = 1;
  ip_icmp.icmp.icmp_seq = 1;
  ip_icmp.icmp.icmp_cksum = checksum((uint16_t *)&ip_icmp.icmp,
                                                 sizeof(ip_icmp.icmp));

  // アドレス情報
  sock_in.sin_family = AF_INET;
  inet_pton(AF_INET, dst, &sock_in.sin_addr);

  int reach = 0;

  // 一応255まで
  for (int i=1; i<256; i++) {
    char res[512], *p;

    ip_icmp.ip.ip_ttl = i;
    // 3回
    for (int j=0; j<3; j++) {

      fd_set ready;
      struct timeval tv;

      FD_ZERO(&ready);
      FD_SET(soc, &ready);

      tv.tv_sec = 3;
      tv.tv_usec = 0;

      int r = sendto(
                  soc,
                  (char *)&ip_icmp,
                  sizeof(ip_icmp),
                  0,
                  (struct sockaddr *)&sock_in,
                  sizeof(struct sockaddr_in)
                );
      if (r == -1) {
        perror("sendto");
        return;
      }

      int sel_res = select( (soc+1), (fd_set *)&ready, NULL, NULL, &tv);
      if (sel_res == 0) {
        if (j == 0) {
          printf("- Time to live %d ? ", i);
        } else if (j == 2) {
          printf("?\n");
        } else {
          printf("? ");
        }
        fflush(stdout);
      } else if (sel_res == -1){

      } else if (sel_res > 0) {
        if (FD_ISSET(soc, &ready)) {
          if (recvfrom(soc, res, sizeof(res), 0, NULL, NULL) == -1) {
            perror("recvfrom");
          }

          // 一応パケットを調べる
          p = res;
          ip = (struct ip *)p;
          p += sizeof(struct ip);
          if (ip->ip_p == IPPROTO_ICMP) {
            char ip_addr[128];
            icmp = (struct icmp *)p;
            int result = analyze(icmp);
            inet_ntop(AF_INET, &ip->ip_src, ip_addr, sizeof(ip_addr));
            if (result == 1) {
              printf("- Reach Time to live %d from %s\n",i, ip_addr);
              reach = 1;
              break;
            } else if (result == 0) {
              printf("- Time to live %d from %s\n",i, ip_addr);
              break;
            }
          }
        }
      }

    }
    if (reach == 1) {
      break;
    }
  }
}

int analyze(struct icmp *icmp) {
  switch(icmp->icmp_type) {
    case ICMP_ECHOREPLY:
      return 1;
      break;
    case ICMP_TIME_EXCEEDED:
      if (icmp->icmp_code == ICMP_EXC_TTL) {
        return 0;
      }
      break;
    default:
      printf("%d\n", icmp->icmp_type);
  }

  return 2;
}

// ICMPV ソケット作成
int make_socket() {
  int soc;

  if ( (soc = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) == -1) {
    perror("socket");
    return -1;
  }

  int opt = 1;
  if (setsockopt(soc, IPPROTO_IP, IP_HDRINCL, &opt, sizeof(opt)) == -1) {
    perror("setsockopt");
    close(soc);
    return -1;
  }

  return soc;
}


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

  if (argc < 2) {
    printf("Arg [Destination IP Address]\n");
    exit(1);
  }

  int soc;
  if ((soc = make_socket()) == -1) {
    printf("[*] Error in make_socket\n");
    exit(1);
  }

  send_recv_packet(soc, argv[DST]);
  return 0;
}