Pingをrawソケットを利用して実装したプログラムを解析します。
解析するプログラムは、リチャードスティーブンスのwebサイト
http://www.kohala.com/start/unpv12e.html
からダウンロード出来るプログラム群の中で、pingフォルダに格納されているソース類になります。
動作を確認したのは、
ubuntu 12.04LTS
gcc version:4.6.3になります。
Pingの動作は簡単で、指定したIPアドレスにICMPエコー要求をが送られ、そのノードが
ICMPエコー応答で反応します。これらは2種類のICMPメッセージは、IPv4/IPv6の両方でサポートされています。
下図は、ICMPメッセージの形式を示しています。
ICMPv4およびICMPv6のエコー要求およびエコー応答メッセージの形式
上図、識別子にはPingプログラムのプロセスIDを設定し、シーケンス番号はパケットを受信するたびに
増加させることとします。送信時には、オプショナルデータに、8バイトのタイムスタンプを付加することにします。
ICMPの規則から、識別子、シーケンス番号、オプショナルデータがそのままエコー応答で返されることを期待します。
Pingプログラムの関数構成
Pingプログラム mainで、シグナルハンドラ(sig_alrm)を定義し、
1秒毎にエコー要求送信(send_v4/send_v6)を指定したIPアドレスへ送信します。
関数readloopより、パケット読み出し関数recvfromをコールし、パケット到着を待ち合わせます。
パケットを読み出した後、proc_v4/proc_v6に受信データを引き渡し、到着したICMPデータを出力します。
各関数の解説
main関数
#include "ping.h"
struct proto proto_v4 = { proc_v4, send_v4, NULL, NULL, NULL, 0, IPPROTO_ICMP };
int datalen = 56; /* data that goes with ICMP echo request */
int
main(int argc, char **argv)
{
int c;
struct addrinfo *ai;
char *h;
opterr = 0; /* don't want getopt() writing to stderr */
while ( (c = getopt(argc, argv, "v")) != -1) {
switch (c) {
case 'v':
verbose++;
break;
case '?':
err_quit("unrecognized option: %c", c);
}
}
if (optind != argc-1)
err_quit("usage: ping [ -v ] <hostname>");
host = argv[optind];
pid = getpid() & 0xffff; /* ICMP ID field is 16 bits */
Signal(SIGALRM, sig_alrm);
ai = Host_serv(host, NULL, 0, 0);
h = Sock_ntop_host(ai->ai_addr, ai->ai_addrlen);
printf("PING %s (%s): %d data bytes\n",
ai->ai_canonname ? ai->ai_canonname : h,
h, datalen);
/* 4initialize according to protocol */
if (ai->ai_family == AF_INET) {
pr = &proto_v4;
} else
err_quit("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();
exit(0);
}
関数mainは、プログラム引数の処理、シグナルハンドラの定義、無限受信ループ関数のコール
といったことを主にやっています。
関数readloop
#include "ping.h"
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()); /* don't need special permissions any more */
if (pr->finit)
(*pr->finit)();
size = 60 * 1024; /* OK if setsockopt fails */
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
sig_alrm(SIGALRM); /* send first packet */
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);
if (n < 0) {
if (errno == EINTR)
continue;
else
err_sys("recvmsg error");
}
Gettimeofday(&tval, NULL);
(*pr->fproc)(recvbuf, n, &msg, &tval);
}
}
関数readloopでは、処理先頭部で、rawソケットの作成、sig_alrmで最初のパケットを発行した後、
無限ループ内で、
パケット受信処理recvfrom、関数ポインタを利用して、受信パケット解析関数のコールを行います。
関数proc_v4
#include "ping.h"
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)
return; /* not ICMP */
icmp = (struct icmp *) (ptr + hlen1); /* start of ICMP header */
if ( (icmplen = len - hlen1) < 8)
return; /* malformed packet */
if (icmp->icmp_type == ICMP_ECHOREPLY) {
if (icmp->icmp_id != pid)
return; /* not a response to our ECHO_REQUEST */
if (icmplen < 16)
return; /* not enough data to use */
tvsend = (struct timeval *) icmp->icmp_data;
tv_sub(tvrecv, tvsend);
rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;
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);
} else 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);
}
}
関数readloopから、関数ポインタ経由で本関数proc_v4がコールされます。
パケットのヘッダタイプを検証(ICMP_ECHOREPLY)し、pingプロセスIDと同じ識別子を持つ
パケットのみを解析、端末表示の対象としています。
⬆ICMPv4応答を処理するためのヘッダ構成、ポインタ、長さ
IPv6の場合、proc_v6がコールされますが、内容自体は、v4とそれほど変わらず、
解析対象となるパケット構成は下記のようになります。
⬆ICMPv6応答を処理するためのヘッダ構成、ポインタ、長さ
関数sig_alrm
#include "ping.h"
void
sig_alrm(int signo)
{
(*pr->fsend)();
alarm(1);
return;
}
シグナルハンドラ。
1秒毎に、このハンドラがOSによって呼び出され、関数ポインタ経由でsend_v4/send_v6が
呼び出されます。
関数send_v4
#include "ping.h"
void
send_v4(void)
{
int len;
struct icmp *icmp;
icmp = (struct icmp *) sendbuf;
icmp->icmp_type = ICMP_ECHO;
icmp->icmp_code = 0;
icmp->icmp_id = pid;
icmp->icmp_seq = nsent++;
memset(icmp->icmp_data, 0xa5, datalen); /* fill with pattern */
Gettimeofday((struct timeval *) icmp->icmp_data, NULL);
len = 8 + datalen; /* checksum ICMP header and data */
icmp->icmp_cksum = 0;
icmp->icmp_cksum = in_cksum((u_short *) icmp, len);
Sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen);
}
ターゲットに送信するICMPメッセージを作成します。ちなみにIP_HDRINCLソケットオプションは
設定しないため、カーネルによってこのICMPメッセージの前にIPv4ヘッダが付与されます。
send_v6は、IPv6の場合動作しますが、大きく動作は変わらないので割愛します。
非常にシンプルな形で、pingプログラムが作成出来ます。
ただ、rawソケットを作成する形になるので、sudo ./ping ターゲットアドレス
としないと、pingが発信されません。
またOS付属のpingプログラムは、オプションを充実させてるので、前処理部分、出力部分が
それなりに複雑です。ただ、機能自体は、シンプルなのものです。