ネットワークアドレスの秘密

こんにちは、幅広い視野を持つエンジニアを目指しています田中と申します。

今日はネットワークアドレスの扱い方を見ていきます。

目的

特定のIPアドレスが、特定のネットワークの範囲内にあるかどうか調べたい。また、サブネットマスクやブロードキャストアドレスなどの算出や検証も併せて行いたい、何か良い方法はないか調べる

やってみた事

IPアドレスを整数(32bit unsigned int)0 ~ 4,294,967,295)に変換して値の範囲を調べる

きっかけ

疎通確認などでよく使うpingコマンドですが、

ping [IPアドレス]

のように使いますが、私はある日誤って以下のように入力してしまいました。

ping [正の整数]

結果は以下のようになります。

C:\Users\tanaka>ping 8.8.8.8

8.8.8.8 に ping を送信しています 32 バイトのデータ:
8.8.8.8 からの応答: バイト数 =32 時間 =3ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =3ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =3ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =3ms TTL=59

8.8.8.8 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 3ms、最大 = 3ms、平均 = 3ms

C:\Users\tanaka>ping 123456789

7.91.205.21 に ping を送信しています 32 バイトのデータ:
Ctrl+C
^C

10進数ドット区切りのIPアドレス以外をパラメータとして与えてもエラーにはなりませんでした。上記はWindowsのpingコマンドの結果ですが、Linuxなどのpingコマンドでも同様の結果となります。

以上のことから、pingコマンドは32bitのIPアドレスを10進数として解釈して処理しているのではないかと考え、自分のプログラムやスクリプトにも応用できるのではないかと考えました。

IPアドレスを10進数32bit unsigned intに変換してpingに与えてみた

それでは、Google Public DNSの”8.8.8.8″を10進数の32ビットの符号なし整数に変えてpingにパラメータとして与えてみます。

10進数のint型0を32bitで表すと以下のようになります。

0000 0000 0000 0000 0000 0000 0000 0000

10進数のint型8を2進数で表すと1000なので”8.8.8.8″は以下であると考えました。

0000 1000 0000 1000 0000 1000 0000 1000

これを10進数に直します。

2^27 + 2^19 + 2^11 + 2^3
= 134217728 + 524288 + 2048 + 8
= 134744072

これをpingのパラメータに与えてみます。

C:\Users\tanaka>ping 134744072

8.8.8.8 に ping を送信しています 32 バイトのデータ:
8.8.8.8 からの応答: バイト数 =32 時間 =4ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =3ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =3ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =3ms TTL=59

8.8.8.8 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 3ms、最大 = 4ms、平均 = 3ms

予想通りの結果となりました。

IPアドレスを10進数32bit unsigned intに変換して処理してみた

pingコマンドを真似てIPアドレスを32bit unsigned intとして扱い、TCP/IPネットワークの情報を算出してみます。

IPアドレスを32bit値に変換

IPアドレスの各オクテット(8bit)をint型の数字と見なし、4つ並べます。

0000 0000 0000 0000 0000 0000 0000 1000 // 8
0000 0000 0000 0000 0000 0000 0000 1000 // 8
0000 0000 0000 0000 0000 0000 0000 1000 // 8
0000 0000 0000 0000 0000 0000 0000 1000 // 8

これを第一オクテットから順に、24ビット左シフト、第二オクテットを16ビット左シフト、第三オクテットを8ビット左シフトしてORをとります。

0000 1000 0000 0000 0000 0000 0000 0000
0000 0000 0000 1000 0000 0000 0000 0000
0000 0000 0000 0000 0000 1000 0000 0000
0000 0000 0000 0000 0000 0000 0000 1000
==================================    [OR]
0000 1000 0000 1000 0000 1000 0000 1000 => 134744072

IPアドレスが正しいかどうか

10進数に変換したIPアドレスの範囲が0~4294967295の範囲に含まれるかどうか

CIDR(プレフィックス長)を32bitIPアドレスにする

2^((32 – プレフィックス長) – 1)を2進数にしたものと、0xFFFFFFFFでXORをとる

例: 24

0000 0000  0000 0000  0000 0000  1111 1111
1111 1111  1111 1111  1111 1111  1111 1111
=================================== [XOR]
1111 1111  1111 1111  1111 1111  0000 0000 => 255.255.255.0

ネットワークアドレスを求める

2進数に変換したIPアドレスとプレフィックス長付きアドレス(CIDR)を2進数にしたもののANDをとる(ホスト部のビットをすべて0にする)

例: 192.168.0.1 , サブネットマスク255.255.255.0(192.168.0.1/24)

1100 0000  1010 1000  0000 0000  0000 0001
1111 1111  1111 1111  1111 1111  0000 0000
=================================== [AND]
1100 0000  1010 1000  0000 0000  0000 0000 => 192.168.0.0

ブロードキャストアドレスを求める

プレフィックス長付きアドレス(CIDR)をネットワークアドレスを論理否定(NOT)したものとXORをとる(ホスト部のビットをすべて1にする)

例: 192.168.0.1, サブネットマスク255.255.255.0(192.168.0.1/24)

1100 0000  1010 1000  0000 0000  0000 0000
=================================== [NOT]
0011 1111  0101 0111  1111 1111  1111 1111
1111 1111  1111 1111  1111 1111  0000 0000
=================================== [XOR]
1100 0000  1010 1000  0000 0000  1111 1111 => 192.168.0.255

bashなどNOT演算ができない場合は以下で代用する(プレフィックス長付きアドレス(CIDR)と0xFFFFFFFFでXORをとったものとIPアドレスでORをとる)

1111 1111  1111 1111  1111 1111  0000 0000
1111 1111  1111 1111  1111 1111  1111 1111
=================================== [XOR]
0000 0000  0000 0000  0000 0000  1111 1111
1100 0000  1010 1000  0000 0000  0000 0001
=================================== [OR]
1100 0000  1010 1000  0000 0000  1111 1111 => 192.168.0.255

連続したIPアドレスを求める

IPアドレスを32bit unsigned intにしてインクリメントするだけ、オクテット境界は考える必要なし

あるIP アドレスが特定のネットワークの範囲にあるか調べる

IPアドレスがネットワークアドレスとブロードキャストアドレスの間にあるか調べればよい

例: 192.168.0.200がデフォルトサブネットマスク255.255.255.0で192.168.0.1の範囲に所属するか

192.168.0.255 => 3232235775
192.168.0.1       => 3232235521
192.168.0.200 => 3232235720

3232235521 < 3232235720 < 3232235775 よって所属する

実際のコード例

#!/bin/bash

# IPアドレス表記 -> 32bit値 に変換
function ip2decimal(){
    local IFS=.
    local c=($1)
    printf "%s\n" $(( (${c[0]} << 24) | (${c[1]} << 16) | (${c[2]} << 8) | ${c[3]} ))
}

# 32bit値 -> IPアドレス表記 に変換
function decimal2ip(){
    local n=$1
    printf "%d.%d.%d.%d\n" $(($n >> 24)) $(( ($n >> 16) & 0xFF)) $(( ($n >> 8) & 0xFF)) $(($n & 0xFF))
}

# CIDR 表記のネットワークアドレスを 32bit値に変換
function cidr2decimal(){
    printf "%s\n" $(( 0xFFFFFFFF ^ ((2 ** (32-$1))-1) ))
}

# 指定された個数だけ連続したIPアドレスの一覧を表示
function iplist(){
    local num=$(ip2decimal $1)
    local max=$(($num + $2 - 1))

    while :
    do
        decimal2ip $num
        [[ $num == $max ]] && break || num=$(($num+1))
    done
}

# 特定のIPアドレスが特定のネットワークに含まれるか調べる
function ipwith(){
   local addr=$1
   local mask=$2

   local num=$(ip2decimal $3)
   local net=$(( $(ip2decimal $addr) & $(ip2decimal $mask) ))
   local brd=$(( $(ip2decimal $addr) | (0xFFFFFFFF ^ $(ip2decimal $mask)) ))

   [ $net -le $num -a $num -le $brd ] && return 0 || return 1
}

実行例

# IPアドレスを32bit値に変換
$ ip2decimal 192.168.0.1
3232235521

# 32bit値をIPアドレスに変換
$ decimal2ip 3232235521
192.168.0.1

# CIDR表記のネットワークアドレスを32bit値に変換
$ cidr2decimal 24
4294967040

# 特定のIPアドレスのネットワークアドレスを求める
$ IP=192.168.0.1
$ CIDR=24
$ decimal2ip $(( $(ip2decimal $IP) & $(cidr2decimal $CIDR) ))
192.168.0.0

# 特定のIPアドレスのブロードキャストアドレスを求める
$ MASK=255.255.255.0
$ decimal2ip $(( $(ip2decimal $IP) | (0xFFFFFFFF ^ $(ip2decimal $MASK)) ))
192.168.0.255

# 指定された数だけ特定のIPアドレスと連続したIPアドレスを求める
$ iplist 192.168.0.253 5
192.168.0.253
192.168.0.254
192.168.0.255
192.168.1.0
192.168.1.1

# 特定のIPアドレスが特定のネットワークに含まれるか調べる
$ ipwith 192.168.0.1 255.255.255.0 192.168.0.200
$ echo $?
0

# 特定のIPアドレスが特定のネットワークに含まれるか調べる
$ ipwith 192.168.0.1 $(decimal2ip $(cidr2decimal 24)) 192.168.1.200
$ echo $?
1

IPアドレスを10進数32bit unsigned intとしてIPアドレスの範囲を調べることができました!

pingのソースコードを読んでみた

Linuxのpingのソースコードが見つからなかったのでFreeBSDのソースコードを読んでみます。

https://svnweb.freebsd.org/base/head/sbin/ping/ping.c

553 target = argv[optind]; // コマンドラインパラメータに渡された値を読んでいる
.
.
.
/* to->sin_addrにtargetの値をnetwork byte order形式で入れる。パラメータの値がIPアドレスとして解釈できるかどうか */
606 if (inet_aton(target, &to->sin_addr) != 0) {
607         hostname = target; // 解釈できる(32bit10進数値の場合もここを通る)
608 }
.
.
.
841 (void)printf("PING %s (%s)", hostname, inet_ntoa(to->sin_addr)); // PING 134744072 (8.8.8.8)...と表示している

inet_ntoa()とinet_aton()が32bit数値とASCII文字列(10進数ドット区切りIPアドレス)の変換をしていそうです。

オンラインマニュアルのinet(3)にありました。

inet_aton() converts the Internet host address cp from the IPv4
       numbers-and-dots notation into binary form (in network byte order)
       and stores it in the structure that inp points to.  inet_aton()
       returns nonzero if the address is valid, zero if not.
.
.
.

マニュアルによると16進数でも解釈できるとのことなのでやってみます。

C:\Users\tanaka>ping 0x08080808

8.8.8.8 に ping を送信しています 32 バイトのデータ:
8.8.8.8 からの応答: バイト数 =32 時間 =3ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =3ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =3ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =4ms TTL=59

8.8.8.8 の ping 統計:
 パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
 最小 = 3ms、最大 = 4ms、平均 = 3ms

C:\Users\tanaka>ping 0x08.0x08.0x08.0x08

8.8.8.8 に ping を送信しています 32 バイトのデータ:
8.8.8.8 からの応答: バイト数 =32 時間 =8ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =5ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =8ms TTL=59
8.8.8.8 からの応答: バイト数 =32 時間 =8ms TTL=59

8.8.8.8 の ping 統計:
 パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
 最小 = 5ms、最大 = 8ms、平均 = 7ms

16進数でもできました!

参考にさせていただいたサイト様(TCP/IP情報取得シェルスクリプトをそのまま使わせて頂いています)

http://qiita.com/harasou/items/5c14c335388f70e178f5

記事は以上になります。

このたびはご覧戴き、ありがとうございました。