RAWソケットを使ったICMPの送受信
(pingもどきプログラム)
RAWソケットからデータを送信してみます、今回はRAWソケットからICMPメッセージを送信するpingもどきプログラムを作成します。(ちょっとソースも説明も長くなってしまいましたが) |
/*
* sample program
* ping test program
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <netdb.h>
#include <errno.h>
int sequence;
pid_t pid;
struct in_addr addr;
unsigned short calc_checksum(int , void *);
int send_icmp_echo_request(int);
int recv_icmp_echo_reply(int, struct timeval*);
int main(int argc, char *argv[])
{
char host[256] = {0};
int cnt = 0, ret = 0;
struct addrinfo hints, *res;
int recvfd = 0, sendfd = 0;
struct timeval tv;
if(argc != 2){
fprintf(stdout, "Usage: myping distination\n");
return -1;
}
strncpy(host, argv[1], 256);
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
ret = getaddrinfo(host, NULL, &hints, &res);
if((ret != 0) || (res == NULL)){
fprintf(stdout, "distination not found!\n");
return -1;
}
addr.s_addr = ((struct sockaddr_in *)(res->ai_addr))->sin_addr.s_addr;
pid = getpid();
/* create recv socket */
recvfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP));
if(recvfd < 0){
perror("socket error");
return -1;
}
/* create send socket */
sendfd = socket(PF_INET, SOCK_RAW, IPPROTO_RAW);
if(sendfd < 0){
perror("socket error");
return -1;
}
for(cnt = 0; cnt < 3; cnt++){
if(send_icmp_echo_request(sendfd) < 0){
break;
}
gettimeofday(&tv, NULL);
if(recv_icmp_echo_reply(recvfd, &tv) < 0){
fprintf(stdout, "Destination Host Unreachable\n");
}
sleep(1);
}
close(recvfd);
close(sendfd);
return 0;
}
unsigned short calc_checksum(int len, void *start)
{
unsigned short *p;
unsigned long sum = 0;
p = (unsigned short *)start;
while(len > 1){
sum += *p;
p++;
len -= sizeof(unsigned short);
}
if(len){
sum += *(uint8_t *)p;
}
sum = (sum & 0xffff) + (sum >> 16);
sum += (sum >> 16);
return (unsigned short)(~sum & 0xffff);
}
int send_icmp_echo_request(int sockfd)
{
struct packet {
struct iphdr ip;
struct icmphdr icmp;
char data[64];
}packet;
int datalen = 0, len = 0;
struct sockaddr_in to;
/* set icmp data */
memset(&packet, 0, sizeof(struct packet));
strcpy(packet.data, "1234567890qwertyuiopasdfghjklzxcvbnm");
datalen = strlen(packet.data);
packet.icmp.type = ICMP_ECHO;
packet.icmp.un.echo.id = htons(pid);
packet.icmp.un.echo.sequence = htons(++sequence);
datalen += sizeof(struct icmphdr);
packet.icmp.checksum = calc_checksum(datalen, &(packet.icmp));
/* set ip header */
/* (id, tot_len, saddr and check are set automatically.) */
packet.ip.version = 4;
packet.ip.ihl = 5;
packet.ip.tos = 0;
datalen += sizeof(struct iphdr);
packet.ip.tot_len = 0;
packet.ip.id = 0;
packet.ip.frag_off = htons(0x02 << 13);
packet.ip.ttl = 64;
packet.ip.protocol = IPPROTO_ICMP;
packet.ip.daddr = *(uint32_t *)&addr;
packet.ip.saddr = 0;
packet.ip.check = 0;
/* send icmp */
memset(&to, 0, sizeof(struct sockaddr_in));
to.sin_family = PF_INET;
to.sin_addr = addr;
len = sendto(sockfd, &packet, datalen, 0,
(struct sockaddr *)&to, sizeof(to));
if(len < 0){
perror("sendto error");
}
return len;
}
int recv_icmp_echo_reply(int sockfd, struct timeval *tv)
{
struct iphdr *ip;
struct icmphdr *icmp;
char buf[ETHER_MAX_LEN] = {0};
char sip[16] = {0};
unsigned short icmpid, icmpseq;
int len = 0, icmplen = 0, usec = 0;
double msec;
struct timeval now, tout;
fd_set readfd;
while(1){
gettimeofday(&now, NULL);
if(now.tv_sec >= (tv->tv_sec + 2)){
return -1; /* timeout */
}
memset(buf, 0, sizeof(buf));
FD_ZERO(&readfd);
FD_SET(sockfd, &readfd);
tout.tv_sec = 0;
tout.tv_usec = 100000;
if(select(sockfd +1, &readfd, NULL, NULL, &tout) <= 0){
continue;
}
if(FD_ISSET(sockfd, &readfd) == 0){
continue;
}
len = recv(sockfd, buf, sizeof(buf), 0);
if(len < 0){
perror("recv error");
return -1;
}
gettimeofday(&now, NULL);
ip = (struct iphdr *)(buf + sizeof(struct ether_header));
if((ip->protocol != IPPROTO_ICMP) || (ip->saddr != addr.s_addr)){
continue;
}
icmp = (struct icmphdr *)((char *)ip + sizeof(struct iphdr));
icmpid = ntohs(icmp->un.echo.id);
if((icmp->type != ICMP_ECHOREPLY) || (icmpid != pid)){
continue;
}
icmpseq = ntohs(icmp->un.echo.sequence);
icmplen = ntohs(ip->tot_len) - sizeof(struct iphdr);
sprintf(sip, "%s", inet_ntoa(*(struct in_addr *)&(ip->saddr)));
usec = ((now.tv_sec)&0x03) * 1000000 + now.tv_usec;
usec = usec - ((tv->tv_sec)&0x03) * 1000000 - tv->tv_usec;
msec = (double)usec / 1000;
fprintf(stdout, "%d bytes from %s: icmp_seq=%d ttl=%d time=%.3f ms\n",
icmplen, sip, icmpseq, ip->ttl, msec);
break;
}
return len;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
(プログラムの概要)
pingコマンドのような動きを実現する為に以下の操作を行います。1.コマンド引数から送信先アドレスの取得。
2.送信用と受信用のソケットを作成。
3.ICMPエコーリクエストメッセージを送信。
4.ICMPの返信を待ちます。(約3秒でタイムアウト) <main処理>
34L-47L:宛先アドレスの取得
コマンドの引数から送信先のアドレスを取得します。getaddrinfo()にて引数の文字列からIPアドレスに変換しています。 48L:プロセスIDの取得
ICMPメッセージのIDとして使う為にプロセスIDを取得しています。 51L-55L:受信用ソケット作成
ICMPの返信を受信する為のソケットを作成しています。ICMPが受信できるようにETH_P_IP指定をしています。IP以外のプロトコルは必要ないのでETH_P_ALLではありません。 57L-61L:送信用ソケット作成
ICMP送信用のソケットを作成しています。IPPROTO_RAWプロトコルを指定しています。このIPPROTO_RAWでソケットを作成すると、送信時にIPヘッダーの一部を自動的に作成して くれます。詳しくはRAWのmanpageを参照してください。 63L-72L:ICMP送受信ループ
ICMPを3回送受信します。送信に失敗した場合はエラーメッセージを出力します。 <ICMP送信処理:send_icmp_echo_request()>
108L-115L:ICMPメッセージ作成
iphdr構造体についてはnetinet/ip.hを、icmphdr構造体についてはnetinet/ip_icmp.hを参照してください。ICMPタイプをICMP_ECHO、IDとしてmain()で取得したプロセスIDを使用しています。チェックサムはcalc_checksum()サブルーチンで計算しています、1の補数演算ですが、詳しく解説しているサイトが色々ありますので探してみてください。IPヘッダーやUDPヘッダーのチェックサムも同じように計算します。(UDPはダミーヘッダーを使用するのでちょっと違いがありますが計算方法は一緒です。) 118L-129L:IPヘッダー作成
IPヘッダーを作成しています。id(パケット識別子),tot_len(トータルのデータ長),saddr(送信元IPアドレス),check(IPヘッダーのチェックサム)に関しては、IPPROTO_RAWプロトコルで作成したRAWソケットから送信するので、送信時に自動的に設定されます。saddrとidは値が0の時だけ設定されるので、例えばsaddrに適当なアドレスを設定すると送信元を偽ることもできてしまいます。 132L-138L:送信
作成したICMPメッセージを送信しています。 <ICMP受信処理:recv_icmp_echo_reply()>
select()を使用して、100msに1回タイムアウトさせながら、RAWソケットで到着するIPパケット全てを受信しています。受信したパケットのプロトコルや送信元アドレス、ICMPのデータをチェックして、自分が送信したICMPリクエストの返信である場合に表示を行います。gettimeofday()で現在時刻と比較しているだけなので、タイマーの精度は良くありません。とりあえず1秒に1回程度送信させる為に作成したということで。
(コンパイルと動作)
gcc -Wall -o myping myping.c
スーパーユーザーで実行してください。RAWソケットを使うのでroot権限が必要になります。送信先を引数にして実行すると、pingコマンドのように返信までにかかった時間などを出力します。
ちなみに余談ですが、本物のpingはユーザー権限で実行できるように見えますが、ls -lでみると-rwsr-xr-x 1 root rootとなっており、rootにset uidされていることが分かります。