シェルスクリプトでIPアドレスの計算

IPAM や DHCP サーバーを実装するとき、IP アドレスを機械的に生成するために、IP アドレスの計算をする事があります。 例えば IPAM がラックやデータセンターから機械的に IP アドレスを割り当てたり、DHCP サーバーが連番の IP アドレスを割り当てたりします。 この記事ではシェルスクリプトで IP アドレスを計算する方法を紹介します。

IP アドレスと数値の変換

IP アドレスを計算する上で欠かせない操作が、IP アドレスと数値の相互変換です。 IPv4 アドレスは長さ 4 のバイト列に過ぎませんが、通常は192.168.0.1 のように人間の扱いやすい文字列で表現します。 一方 IP アドレスを計算するには、IP アドレスをバイト列や 32 ビット数値などの、計算機で扱いやすい形に変換します。 IP アドレスを数値として扱えると、数値演算で IP アドレスを計算したりネットワークアドレスを求めやすくなります。

実は Python は標準で、IP アドレスと数値の相互変換を簡単にできます。

import ipaddress
ipaddress.ip_address('192.168.1.200') + 100
# => IPv4Address('192.168.2.44')

ではシェルスクリプトではどうするか。 IP アドレスを数値に変換するip4_to_int関数と、数値から IP アドレスに変換するint_to_ip4関数を記述してゆきます。

ip4_to_intA.B.C.D という IPv4 アドレスを整数に変換します。

# converts IPv4 as "A.B.C.D" to integer
ip4_to_int() {
  IFS=. read -r i j k l <<EOF
$1
EOF
  echo $(( (i << 24) + (j << 16) + (k << 8) + l ))
}

A.B.C.Dという文字列から、各オクテットを抽出して、変数i, j, k, lに格納します。 環境変数IFSを設定して、read 関数の区切り文字を指定します。 i, j, k, l に格納したら、それぞれビットシフトして加算することで、各オクテットから 32 ビット数値に変換します。 シェルでは $(( ... ))で囲むと、数値計算できます。

int_to_ip4 は IPv4 アドレスの整数から A.B.C.D という形の文字列に変換します。

# converts interger to IPv4 as "A.B.C.D"
int_to_ip4() {
  echo "$(( ($1 >> 24) % 256 )).$(( ($1 >> 16) % 256 )).$(( ($1 >> 8) % 256 )).$(( $1 % 256 ))"
}

こちらは、数値をビットシフトして、256 の剰余を求めます。そして各オクテットの値を取得して、再び文字列に変換します。

使い方はこんな感じです。

ip4_to_int 192.168.0.1
# => 3232235521

int_to_ip4 3232235521
# => 192.168.0.1

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

この例だけでは退屈なので、ネットワークアドレスを取得してみます。 ネットワークアドレスは、IP アドレスとネットマスクの各ビットで論理和によって求まります。 論理和を計算するために A.B.C.D という文字列から一度整数に変換します。 次の例は、IP アドレス 172.16.10.20 とネットマスク 255.255.252.0 から、ネットワークアドレス172.16.8.0 を求めます。 IP アドレスとネットマスクをip4_to_int を使って一度数値に変換して、その論理和を再び int_to_ip4 で人間が読みやすい形式に変換します。

ip=$(ip4_to_int 172.16.10.20)
netmask=$(ip4_to_int 255.255.252.0)
int_to_ip4 $((ip & netmask))
# => 172.16.8.0

ネットワークアドレスが求まれば、「ネットワークアドレス + 1」のアドレス(つまりはよくあるデフォルトゲートウェイ)を計算することもできます。

ip=$(ip4_to_int 172.16.10.20)
netmask=$(ip4_to_int 255.255.252.0)
int_to_ip4 $(((ip & netmask) + 1))
# => 172.16.8.1

CIDR と組み合わせる

CIDR(Classless Inter-Domain Routing)とは、IP アドレスとネットマスクを A.B.C.D/E で表したものです。 CIDR も非常によく使う表現なので、CIDR を扱うためのユーティリティ関数があると便利です。 まずは CIDR を IP アドレス部とプレフィクスに分割する関数を定義します。

# returns the ip part of an CIDR
cidr_ip() {
  IFS=/ read -r ip _ <<EOF
$1
EOF
  echo $ip
}

# returns the prefix part of an CIDR
cidr_prefix() {
  IFS=/ read -r _ prefix <<EOF
$1
EOF
  echo $prefix
}
cidr_ip "172.16.0.10/22"
# => 172.16.0.10
cidr_prefix "172.16.0.10/22"
# => 22

CIDR のプレフィクスはそのままだと扱いにくいので、ネットマスクの数値に変換する関数も定義します。

# returns net mask in numberic from prefix size
netmask_of_prefix() {
  echo $((4294967295 ^ (1 << (32 - $1)) - 1))
}
netmask_of_prefix 8
# => 4278190080

ここまでの関数を組み合わせると、CIDR から「ネットワークアドレス + 1」のアドレスを計算する関数を定義できます。

# returns default gateway address (network address + 1) from CIDR
cidr_default_gw() {
  ip=$(ip4_to_int $(cidr_ip $1))
  prefix=$(cidr_prefix $1)
  netmask=$(netmask_of_prefix $prefix)
  gw=$((ip & netmask + 1))
  int_to_ip4 $gw
}
cidr_default_gw 192.168.10.1/24
# => 192.168.10.1
cidr_default_gw 192.168.10.1/16
# => 192.168.0.1
cidr_default_gw 172.17.18.19/20
# => 172.17.16.1

また「ブロードキャストアドレス - 1」のパターンのデフォルトゲートウェイも簡単に計算できます。

# returns default gateway address (broadcast address - 1) from CIDR
cidr_default_gw_2() {
  ip=$(ip4_to_int $(cidr_ip $1))
  prefix=$(cidr_prefix $1)
  netmask=$(netmask_of_prefix $prefix)
  broadcast=$(((4294967295 - netmask) | ip))
  int_to_ip4 $((broadcast - 1))
}

cidr_default_gw_2 192.168.10.1/24
# => 192.168.10.254
cidr_default_gw_2 192.168.10.1/16
# => 192.168.255.254
cidr_default_gw_2 172.17.18.19/20
# => 172.17.31.254

#a おわりに

シェルスクリプトで IP アドレスが計算できると、たとえばプロビジョニングスクリプトをシェルで記述して、IP アドレスの設定などを自動で行うなどができます。 今回のコードはパースエラーなどはチェックしてませんが、ユーザーからの入力をこれらの関数に渡す場合は、厳密にチェックすべきでしょう。 今回のコード例は Gist に置いておきます。


Profile picture

Shin'ya Ueoka

B2B向けSaaSを提供する会社の、元Webエンジニア。今はエンジニアリング組織のマネジメントをしている。