IPv6ネットワークプログラミング ICMPV6編


ICMPV6を使ったプログラムを作ります。
作るものは以下の記事に書いたようなtracerouteをIPV6で書き直したものです。


IPv4とIPv6は仕様的に異なるところがそこそこあります。
そのためプログラムの書き方にも相違点がありIPv4で通用していた書き方ではどうにもならない部分もあります。


実装

#include <sys/socket.h>
#include <netinet/icmp6.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>

enum {
  COMMAND, DST
};

int extract(struct icmp6_hdr *icmp6);
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 icmp6_hdr *icmp6, send_icmp6;

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

  memset(&send_icmp6, 0, sizeof(send_icmp6));

  send_icmp6.icmp6_type = ICMP6_ECHO_REQUEST;
  send_icmp6.icmp6_code = 0;
  send_icmp6.icmp6_cksum = 0;
  send_icmp6.icmp6_id = htons(666);
  send_icmp6.icmp6_seq = htons(888);

  // アドレス情報
  sock_in.sin6_family = AF_INET6;
  
  inet_pton(AF_INET6, dst, &sock_in.sin6_addr);

  int reach = 0;

  // 一応255まで
  for (int i=1; i<256; i++) {

    char  res[512], *p;

    // 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 hop = i;
      if (setsockopt(soc, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &hop, sizeof(hop)) == -1) {
        perror("setsockopt");
      }

      int r = sendto(
                  soc,
                  (char *)&send_icmp6,
                  sizeof(send_icmp6),
                  0,
                  (struct sockaddr *)&sock_in,
                  sizeof(sock_in)
                );

      if (r == -1) {
        perror("sendto");
        return;
      }

      int sel_res = select( (soc+1), (fd_set *)&ready, NULL, NULL, &tv);
      if (sel_res == 0 ) {
        break;
      } else if (sel_res == -1) {
        break;
      } else {
        if (FD_ISSET(soc, &ready)) {
          struct sockaddr_in6 recv_soc;
          socklen_t from = sizeof(recv_soc);
          if (recvfrom(soc, res, sizeof(res), 0, (struct sockaddr *)&recv_soc, &from) == -1) {
            perror("recvfrom");
          }

          p = res;
          char ip6_addr[128];
          icmp6 = (struct icmp6_hdr *)p;
          int result = extract(icmp6);
          inet_ntop(AF_INET6, &recv_soc.sin6_addr, ip6_addr, sizeof(ip6_addr));
          if (result == 1) {
            printf("- Reach Hop Limit %d from %s\n",i, ip6_addr);
            reach = 1;
            break;
          } else if (result == 0) {
            printf("- Hop Limit %d from %s\n",i, ip6_addr);
            break;
          }
        }
      }
    }
    if (reach == 1) {
      break;
    }
  }
}

int extract(struct icmp6_hdr *icmp6) {
  switch(icmp6->icmp6_type) {
    case ICMP6_ECHO_REPLY:
      return 1;
      break;
    case ICMP6_TIME_EXCEEDED:
      if (icmp6->icmp6_code == ICMP6_TIME_EXCEED_TRANSIT) {
        return 0;
      }
      break;
    default:
      break;
      printf("%d\n", icmp6->icmp6_type);
  }

  return 2;
}

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

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

  return soc;
}

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

  if (argc < 2) {
    printf("Arg [Destination IPV6 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;
}

IPv4との相違点


IPv4でプログラミングしていた時との相違点を可能な限りまとめます。

ソケット


ICMPV6パケットを送受信するためのソケットを作成します。

socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6)


IPv4ではここでIPヘッダーを自分で作成したい場合、setsockoptでIP_HDRINCLを指定することによってそれを実現できます。

しかしIPv6ではIP_HDRINCLを指定できませんでした。
そもそも「IPv6ヘッダーを自分で作る」ということができないようになっているみたいです。

さらに上のようにソケットを作成しても受信するのはIPv6ヘッダーではなくICMPV6パケットです。

tracerouteはHop limit値を更新しながらパケットの送信を繰り返していくわけですが、
IPv6ヘッダーを自分作ることができないとなるとどうすればいいのか。

そこでsetsockoptを使います。

int hop = 1;
setsockopt(soc, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &hop, sizeof(hop));


このようにHop limitを指定することができます。

アドレス情報構造体


IPv4ではパケットを送信する際にsockaddr_in構造体にアドレスファミリと宛先IPアドレスを指定してsendtoに構造体を渡します。

IPv6でもこれは同じです。
構造体がsockaddr_in6に変わっただけです。
アドレスファミリに指定するのはAF_INET6です。

アドレスの文字列からの変換はIpv4の場合と同じようにinet_ptonを使用することができます。

struct sockaddr_in6 sock_in;
sock_in.sin6_family = AF_INET6;
inet_pton(AF_INET6, ip6address, &sock_in.sin6_addr);

実行結果


実行するとこのようになります。

$ sudo ./RouteScanV6 2001:db8:0:3:f8b9:c0da:6aa8:e5c9
- Hop Limit 1 from 2001:db8:0:1::1
- Hop Limit 2 from 2001:db8:0:2::2
- Reach Hop Limit 3 from 2001:db8:0:3:f8b9:c0da:6aa8:e5c9


IPv6はまだまだ底がありそうなのでIPv4との相違点を中心に触れていきたいと思います。