Edited at

CでIPヘッダを理解する

More than 1 year has passed since last update.


概要

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ヘッダ内容と照らし合わせて見るとなんとなくわかる気がしませんか。


netinet/ip.h

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パケットデータへの参照アドレスを取得します。


get_iphdr.c

#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用の構造体に入れる必要があります。


ipver.c

#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)で割り切れるようになっています。


hdr_len.c

#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が最強。


show_dscp.c

#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)


show_flag.c

#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パケット(チェック無し、早い)


show_prot.c

#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の補数を取るかというと、受信側の確認処理が楽になるからっぽいです。


ip_chksum.c

#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文字列を取得できちゃったりします。


ipaddr.c

#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;
}


オプションは割愛させて頂きます。

以上になります。