#概要
IPパケットはOSI参照モデルのレイヤー3(ネットワーク層)に該当します。
Etherヘッダの後に続くパケットになります。
この記事ではC言語によるIPパケットの解析を行い、IPパケットについて理解を深めてみます。
#環境構築
CentOSを想定しています。
gccなどのコンパイラとlibpcapというパケット取得のためのライブラリを使います。
yum -y update
yum install -y gcc vim wget tar flex byacc
wget http://www.tcpdump.org/release/libpcap-1.8.1.tar.gz
tar zxvf libpcap-1.8.1.tar.gz
cd libpcap-1.8.1
./configure
make -j 8
make install
#IPヘッダについて
##IPヘッダ内容
ヘッダの内容をざっくり言いますと
IPアドレスが割り当てられた機器間でデータ(パケット)をやり取りする為の情報が入っています。
以下は各領域のざっくりとした説明になります。
概要 | サイズ | 説明 |
---|---|---|
Version | 4bit | v4かv6か |
Header length | 4bit | ヘッダのサイズ |
Type of service | 8bit | パケットの優先度情報 |
Total length | 16bit | IPパケット全体のサイズ |
Identification | 16bit | パケットを分割した時、後で戻せるように割り振った目印の値 |
Flag | 3bit | IPパケット分割に関する制御用(分割して欲しくない時などに使用) |
Flagment Offset | 13bit | 元データでの位置。 分割したパケットを元に戻す用 |
Time to Live | 8bit | パケットの寿命(0になると廃棄されます) |
Protcol | 8bit | IPヘッダに続くデータ部の中身は何であるかを示す値 |
Checksum | 16bit | 伝送エラーがあるかをチェックする為の値 |
Source IP | 32bit | 送信元IPアドレス |
Destination IP | 32bit | 宛先IPアドレス |
Options | 可変 | オプション |
Padding | 可変 | Optionsが32bit単位の区切りでない場合、 残りを0で埋めて32bit単位にします |
##ヘッダ構造体
CでIPヘッダを扱う場合、netinet/ip.hに構造体が定義されています。
上のIPヘッダ内容と照らし合わせて見るとなんとなくわかる気がしませんか。
struct iphdr
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ihl:4;
unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
unsigned int version:4;
unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
u_int8_t tos;
u_int16_t tot_len;
u_int16_t id;
u_int16_t frag_off;
u_int8_t ttl;
u_int8_t protocol;
u_int16_t check;
u_int32_t saddr;
u_int32_t daddr;
/*The options start here. */
};
#ヘッダ解析
##IPヘッダ取り出し
まずはIFからフレームを受信しないとなりません。
受信後、フレームの参照アドレスからEtherヘッダサイズ分だけ移動させて、
IPパケットデータへの参照アドレスを取得します。
#include <stdio.h>
#include <netinet/ip.h>
void start_pktfunc( u_char *user, // pcap_loop関数の第4引数
const struct pcap_pkthdr *h , // 受信したPacketの補足情報
const u_char *p // 受信したpacketへのポインタ
){
struct ether_header *eth_hdr = (struct ether_header *) p;
struct iphdr *ip_hdr = NULL;
if( ETHERTYPE_IP != ntohs(eth_hdr->ether_type) ){
// IPパケットでない場合は無視
return;
}
// Etherフレームデータの次がIPパケットデータなので、ポインタを移動させる。
ip_hdr = (struct iphdr *) (p + sizeof(struct ether_header));
printf("IP Packet Receive\n");
return;
}
##Versionのdump
IPパケットのversionをダンプするソースになります。
恐らく4か6になるかと思いますが、6が入って来た場合、ver6用の構造体に入れる必要があります。
#include <stdio.h>
#include <netinet/ip.h>
#define DPCP_IPV4_PKT 0x04
#define DPCP_IPV6_PKT 0x06
void show_ipver(struct iphdr *ip_hdr) {
printf("version : \n");
if( DPCP_IPV4_PKT != ip_hdr->version ) {
printf("ipv4\n");
} else if( DPCP_IPV6_PKT != ip_hdr->version ) {
printf("ipv6\n");
} else {
printf("unknown version\n");
}
}
##Header lengthのdump
ヘッダ領域の節約かと思いますが、ihlにはヘッダのbyteサイズが格納されるのではなく、
ヘッダサイズのbyte値から4を割った値が格納されます。
IPヘッダのサイズは可変ですが必ず32bit単位で可変するはずですので、
ヘッダサイズは必ず4(byte)で割り切れるようになっています。
#include <stdio.h>
#include <netinet/ip.h>
void show_hdrlen(struct iphdr *ip_hdr){
printf("ip header length : %ubyte" , ip_hdr->ihl*4 );
}
###Type of serviceのDump
####IP Precedence
IP Precedenceという方式の優先制御では8bitあるうちの先頭3bitを使用しています。
先頭から4bit目は予約領域となっていて通常は使用しないかと思います。
以下はざっくり各値の説明になります。
名前 | 説明 |
---|---|
routine priority immediate flash flash-override critical |
routineをデフォルト値としてcriticalに行く程優先度が上昇 criticalは音声用(Voice RTP)等で利用 |
internet network |
上記よりも更に優先度が高い。ネットワーク制御用に使用 |
#include <stdio.h>
#include <netinet/ip.h>
void show_ipprd( struct iphdr *ip_hdr ){
printf( "ip precedence : " );
if( 0 == ip_hdr->tos & (0xFF & ~IPTOS_CLASS_MASK) ){
switch( IPTOS_PREC( ip_hdr->tos ) ){
case IPTOS_PREC_ROUTINE:
printf("routine\n");
break;
case IPTOS_PREC_PRIORITY:
printf("priority\n");
break;
case IPTOS_PREC_IMMEDIATE:
printf("immediate\n");
break;
case IPTOS_PREC_FLASH:
printf("flash\n");
break;
case IPTOS_PREC_FLASHOVERRIDE:
printf("flash-override\n");
break;
case IPTOS_PREC_CRITIC_ECP:
printf("critical\n");
break;
case IPTOS_PREC_INTERNETCONTROL:
printf("internet\n");
break;
case IPTOS_PREC_NETCONTROL:
printf("network\n");
break;
}
}
}
####DSCP
DSCPという方式の優先制御では8bitあるうちの先頭6bit使用していて
IP PrecedenceとDSCPのClass Selector領域は互換性があります。
ただ、IP Precedenceは後半3bitの値は決められていないのに対して、
Class Selectorでは後半3bitは0と決められています。
AF43に行く程優先度が高くなり、expedited forwardingが最強。
#include <stdio.h>
#include <netinet/ip.h>
void show_dscp( struct iphdr *ip_hdr ){
int dscp = IPTOS_DSCP(p_hdr->tos);
printf("differentiated services code point\n");
printf("----------------------------------\n");
if( 0 != dscp ){
if( 0 == IPTOS_CLASS(dscp) ){
// class selector
show_ipprd( ip_hdr );
}else if( IPTOS_DSCP_EF == dscp ){
printf("expedited forwarding : true");
}else{
printf("assured forwarding : ");
switch( dscp ){
case IPTOS_DSCP_AF11:
printf("af11\n");
break;
case IPTOS_DSCP_AF12:
printf("af12\n");
break;
case IPTOS_DSCP_AF13:
printf("af13\n");
break;
case IPTOS_DSCP_AF21:
printf("af21\n");
break;
case IPTOS_DSCP_AF22:
printf("af22\n");
break;
case IPTOS_DSCP_AF23:
printf("af23\n");
break;
case IPTOS_DSCP_AF31:
printf("af31\n");
break;
case IPTOS_DSCP_AF32:
printf("af32\n");
break;
case IPTOS_DSCP_AF33:
printf("af33\n");
break;
case IPTOS_DSCP_AF41:
printf("af41\n");
break;
case IPTOS_DSCP_AF42:
printf("af42\n");
break;
case IPTOS_DSCP_AF43:
printf("af43\n");
break;
default:
printf("unknown\n");
break;
}
}
}
}
###Total length , IDのDump
tot_lenの値はIPヘッダ+データのサイズが格納されます。
16bitの領域を使用しているので65535byteが(理論上は)最大値になります。
printf("total length : %ubyte\n", ntohs( ip_hdr->tot_len ) );
printf("identification : %u\n", ntohs( ip_hdr->id ) );
###Flag
bit列 | 説明 |
---|---|
ビット0 | 使用してない。 |
ビット1 | 分割を許可しない事を示す(Don't Fragment) |
ビット2 | 分割したパケットがまだ続くかを示す(More Fragment) |
#include <stdio.h>
#include <netinet/ip.h>
void show_flag( struct iphdr *ip_hdr ){
int flag = ntohs(ip_hdr->frag_off);
printf("flag : ");
if( IP_DF == ip_hdr->frag_off ){
printf("don't fragment\n");
}else if( IP_MF == ip_hdr->frag_off ){
printf("more fragment\n");
}else{
printf("finish fragment\n");
}
}
###Flagment OffsetのDump
IPパケットの分割は常に8byte単位で行われます。
領域の節約かと思われますがflagment offsetの値は位置を表すbyte値を8で割った値が格納されます。
なので値を取り出す際はその逆をします。
printf("flagment offset : %ubyte\n" , (IP_OFFMASK & (ntohs(ip_hdr->frag_off)))*8 );
###Time to LiveのDump
パケットが経由可能なデバイス数。ルータなどを通過する度に1ずつ減っていきます。
printf("time to live : %u\n" , ip_hdr->ttl );
###ProtocolのDump
IPパケットヘッダに続くデータの中身は何かを示す値になります。
以下はその値の一部です。
値 | 説明 |
---|---|
0x01 | ICMPパケット(pingなどで使用) |
0x06 | TCPパケット(送信チェックなどがある) |
0x17 | UDPパケット(チェック無し、早い) |
#include <stdio.h>
#include <netinet/ip.h>
#define DPCP_PROT_ICMP 0x01
#define DPCP_PROT_TCP 0x06
#define DPCP_PROT_UDP 0x17
void show_prot( struct iphdr *ip_hdr ){
printf("protocol : ");
if( DPCP_PROT_ICMP == ip_hdr->protocol ){
printf("icmp\n");
}else if( DPCP_PROT_TCP == ip_hdr->protocol ){
printf("tcp\n");
}else if( DPCP_PROT_UDP == ip_hdr->protocol ){
printf("udp\n");
}else{
printf("0x%x\n" , ip_hdr->protocol );
}
}
###チェックサム計算
エラーがないかをチェックするための領域。
対象はヘッダ領域をとし、ヘッダの各値を加算して1の補数を取った値がチェックサムになる。
なんで1の補数を取るかというと、受信側の確認処理が楽になるからっぽいです。
#include <netinet/ip.h>
#include <sys/types.h>
#include <string.h>
int ip_chksum( struct iphdr *ip_hdr ){
int hdr_len = 0;
int sum = 0;
u_int16_t *buf = (u_int16_t *)ip_hdr;
ip_hdr->check = 0;
hdr_len = ip_hdr->ihl * 4;
// 加算開始
while( hdr_len > 0 ){
sum += *buf++;
if( sum & 0x80000000 ){
// sumが桁あふれしてしまったらエラー
exit(-1);
}
// sumは32bit(4byte)に対してhdr_lenは16bit()
hdr_len -= 2;
}
// 桁溢れ分も加算する。
sum = (sum & 0xffff) + (sum >> 16);
// ↑の加算で桁溢れする場合もあり得るから念の為もう一回
sum = (sum & 0xffff) + (sum >> 16);
// 1の補数
return ~sum;
}
###Source , Destination IPのDump
この領域には送信元IPアドレスと宛先IPアドレスが格納されています。
なんか色々変換するとIP文字列を取得できちゃったりします。
#include <netinet/ip.h>
#include <stdio.h>
void show_ipaddr( struct iphdr *ip_hdr ){
char ip_str[18] = {0};
struct in_addr *addr = NULL;
struct in_addr *daddr = NULL;
printf("source ip : ");
addr = (struct in_addr *) &(ip_hdr->saddr);
inet_ntop( AF_INET , saddr , &ip_str[0] , (socklen_t) sizeof(ip_str) );
printf("%s¥n" , ip_str );
memset( &ip_str[0] , 0x00 , sizeof(ip_str) );
printf("destination ip : ");
addr = (struct in_addr *) &(ip_hdr->daddr);
inet_ntop( AF_INET , addr , &ip_str[0] , (socklen_t) sizeof(ip_str) );
printf("%s¥n" , ip_str );
return;
}
オプションは割愛させて頂きます。
以上になります。