IPアドレスを決めるのにハッシュ値を使ってみた

本記事はqiitaに投降した記事と同一のものになります

経緯

サーバーを立てて1年ほどたつが、これまでさほどIPというものを気にしていなかった。自宅サーバーはルーターが192.168.1.1だったので特に悩まずに192.168.1.2で特に困らなかったし、VPSはsshくらいでしかIPでのアクセスを使わなかったので、最初に.ssh/configに設定してしまえばそれっきり意識して使う機会がなかった。

ところがこの度VPNでスマホやサーバを接続することになり、これまでサーバー側だけ設定しておけばよかったIPをクライアントにも設定する必要ができ、一気に管理するIPが片手で数えられないほどに増えてしまった。

自宅サーバー一つならともかく、3つ4つとなると連番にしてもどれがどれだか覚えられないし、リストで管理するのは避けられないとしても、リストがないとIPがわからなくなってしまうのはそれはそれで心配だし、不便。

ということで無駄に悩んだ結果、ハッシュ値からIPを生成することにした。

解説

本記事の手法は基本的にはIPv6向けとなる。

IPv4でもできなくはないが、例えばホスト部がデフォルトでは8bit255とおりしかないため、単に8bitのハッシュ値にしても機器に割り当てられない0や255になったり、衝突する可能性がそれなりに高いため、そうなった場合の例外的な処理が必要になるのでお勧めしない。今回自分がやった範囲では上記のようなエラーにはならなかったので予備的にIPv4も設定した。

IPv6について

IPv6の後半、インターフェース部は一般的には2種類のやり方(ネットワークデバイスに割り振られたMACアドレス1から生成する方法と、それだとプライバシーの問題があるということで、ランダムに一時アドレスを生成する方法)が使われている。

ここで重要なのは、

ということ

ハッシュ値

もちろん完全に乱数だと人間からは当然扱いづらい。しかし乱数がセーフならハッシュ値も問題ないはず。

例えばサーバーであればホスト名やサービス名、端末であれば機種名や持ち主など、わかりやすい、覚えやすい文字列をもとにIPアドレスを生成するということが可能だということ。 これならIPアドレスを覚えられなくても、リストが手元になくてもなんとかなる。

ということで文字列からハッシュ関数でIPv6アドレスを生成することにした。

具体的なやり方

ハッシュ関数

ハッシュ関数にはBLAKE2を使うことにした。

理由としては、たまたま今回導入したVPN Wireguardで使われていて知ったということもあるが、何より今回の目的で便利なのは出力するハッシュ値をこちらで指定できるということ。たとえば、IPV6アドレスのユニークローカルアドレスには固定である8bitのプレフィクスに40bitのグローバルネットワーク部、16bitのサブネット部、64ビットのinterface部と長さの違う3つのブロックがあるのだが、BLAKE2であれば40bitでも16ビットでも64ビットでも、8の倍数であればそのまま出力できる。もちろんgitみたいにハッシュ値のうえから必要な箇所だけ使ってあとは切り捨てるのでも問題ないけど、余らないほうが気分的にいい。まあ気分の問題といわれればそれまでだけど。

BLAKE2で簡単に任意の長さを生成するのにはb2sumというコマンドで簡単にできる。

$echo -n "Host" | b2sum -l 64
2f97016c73986f6e  -

こんな感じ。

8ビットで出して10進数に直せばipv4アドレスにも一応対応できる。衝突したり、0や255になったらその時はその時。

$ echo -n "VPS" | b2sum -l 8
e1  -

ネットワーク部とインターフェース部

ネットワーク部やサブネット、インターフェース部はランダムにとることで衝突のリスクを減らし、将来的に会社が合併したりしてもネットワークを振りなおす必要がないということで推奨されている。 まあ人付き合いのない個人じゃ関係ないけど。

ということでおおよそこのルールにのっとって行うことにした、

今回は1つのネットワーク、2つのサブネットを作った。

以下の例ではドメインから生成してみた。

$ echo -n "example.com" | b2sum -l 40
f9b0c7780e  -

これをローカルユニークアドレス用のプレフィクスfd00::/8につなげて

fdf9:b0c7:780e::/48

次にサブネット。 今回では外部から踏み台サーバをとおすルートと自宅LANで直接アクセスするルートを作るので、そんな感じに二つ作る。 前者を"Intra" 後者を"DMZ"ということで、サブネット部を生成する。

$ echo -n "Intra" | b2sum -l 16 
f97c  -
$ echo -n "DMZ" | b2sum -l 16 
e81f  -

fdf9:b0c7:780e:f97c:/64 fdf9:b0c7:780e:e81f:/64

そして最後にインターフェース部。

とりあえず踏み台サーバーBastionと自宅サーバーHomeクライアントLaptopひとつづつ設定してみる。

$ echo -n "Bastion" | b2sum -l 64  
a8165e7ca5a06d06  -
$ echo -n "Laptop" | b2sum -l 64 
6f6b4d31a9b0b119  -
$ echo -n "Home" | b2sum -l 64 
400e78b6b01da335  -

たとえばDMZネットワークであれば

踏み台サーバが fdf9:b0c7:780e:e81f:a816:5e7c:a5a0:6d06/64

クライアントのノートPCがfdf9:b0c7:780e:e81f:6f6b:4d31:a9b0:b119/64

自作サーバがfdf9:b0c7:780e:e81f:400e:78b6:b01d:a335/64

となる

自宅用LANで直接自宅サーバにアクセスするためのVPNが

クライアントのノートPCが fdf9:b0c7:780e:f97c:6f6b:4d31:a9b0:b119/64

自作サーバが fdf9:b0c7:780e:f97c:400e:78b6:b01d:a335/64

となる

結局最終的にできるIPアドレスは案の定覚えにくいので実際に運用するには結局リストに頼ることになるのだが、 それでもいざとなったら忘れても生成できるのはメリットなのではなかろうか。 結局のところipv4で連番のほうが楽な気はするが…。

生成用のスクリプト

ここまでやってきて今更ではあるが、手動でつなぎ合わたりコロン挿入するのしんどい…。

ということでツール化した

あとついでにIPv4も同じルールで生成できるようにした。 はじめはIPv6オンリーで設定しようとおもっていたのだが、用意していたキーワードでたまたまIPv4問題なかったので設定した。

コード

#!/bin/bash
get_ipv4(){
    local IPV4_LENGTH=8
    local KEYWORD=$1
    echo -n $KEYWORD | b2sum -l $IPV4_LENGTH | echo $(read v; v=${v:0:$((IPV4_LENGTH/4))}; echo -n "ibase=16;${v^^}")| bc;
}
split_by_colon(){
    local RAW=$1
    local REMAINDER=$((${#RAW}%4))
    if [ $REMAINDER -ne 0 ]; then    
        echo -n ${RAW:0:REMAINDER}
    fi
    for ((i=0; i < $((${#1}/4)); i++)); do
        if [ $REMAINDER -ne 0 -o $i -ne 0 ]; then
            echo -n ":"
        fi
        echo -n  "${RAW:$((i*4+${REMAINDER}%4)):4}"
    done
    return
}
get_ipv6(){
    local IPV6_LENGTH=64
    local OPT 
    local OPTARG
    local KEYWORD=$1
    if [[ $# -eq 2 ]]; then
        local LENGTH=$2
    else
        local LENGTH=$IPV6_LENGTH
    fi
    echo -n $KEYWORD  | b2sum -l $LENGTH | (read v; v=${v:0:$((LENGTH/4))}; echo $v)
}
while getopts 46l:n:s: OPT; do
    case $OPT in
        4)
            IPV4=true
            ;;
        6)
            IPV6=true
            ;;
        l)
            IPV6_LENGTH=$OPTARG
            ;;
        n)
            NET_KEYWORD=$OPTARG
            ;;
        s)
            SUBNET_KEYWORD=$OPTARG
            ;;        
    esac
done
shift `expr $OPTIND - 1`
KEYWORD=$*
if  ! ( [ -n "${IPV4+1}" ] || [ -n "${IPV6+1}" ] ) ; then
    IPV4=true
    IPV6=true
fi
if [ -n "${NET_KEYWORD+1}" ] && [ -n "${SUBNET_KEYWORD+1}" ] ; then
    if [ -n "${IPV4+1}" ] ; then
        echo "10.$(get_ipv4 $NET_KEYWORD).$(get_ipv4 $SUBNET_KEYWORD).$(get_ipv4 $KEYWORD)"
    fi
    if [ -n "${IPV6+1}" ] ; then
        split_by_colon "fd$(get_ipv6 $NET_KEYWORD 40)$(get_ipv6 $SUBNET_KEYWORD 16)$(get_ipv6 $KEYWORD)"
    fi
elif [ -n "${NET_KEYWORD+1}" ] || [ -n "${SUBNET_KEYWORD+1}" ] ; then
    if ! [ -n "${NET_KEYWORD+1}" ] ; then
        NET_KEYWORD=$SUBNET_KEYWORD
    fi
    if [ -n "${IPV4+1}" ] ; then
        echo "192.168.$(get_ipv4 $NET_KEYWORD).$(get_ipv4 $KEYWORD)"
    fi
    if [ -n "${IPV6+1}" ] ; then
        split_by_colon "fd$(get_ipv6 $NET_KEYWORD 56)$(get_ipv6 $KEYWORD)"
    fi
else
    if [ -n "${IPV4+1}" ] ; then
        get_ipv4 $KEYWORD
    fi
    if [ -n "${IPV6+1}" ] ; then
        split_by_colon "$(get_ipv6 $KEYWORD  $IPV6_LENGTH)" 
   fi
fi

使い方

デフォルトではipv4用の8bit10進数と、ipv6用の64bit16進数を出力する

$ text2ip.sh "Home" 
83
400e:78b6:b01d:a335

-4-6オプションでipv4とipv6どちらかにできる。 ipv6に限り-lオプションで長さを指定できる。(8の倍数のみ)IPv4は8bitで固定。

$ text2ip.sh -4 "Home"
83

$ text2ip.sh -6 "Home"
400e:78b6:b01d:a335

$ text2ip.sh -6 -l 24 "Home"
e3:5591

-n オプションでネットワーク部を、-sオプションでサブネット部のキーワードを指定できる。この場合通常のキーワードはインターフェース部に割り当てられる。

$ text2ip.sh -n "example.com" "Home"
192.168.114.83
fd20:741e:2015:2520:400e:78b6:b01d:a335

$ text2ip.sh -n "example.com" -s "Intra" "Home"
10.114.80.83
fdf9:b0c7:780e:f97c:400e:78b6:b01d:a335

ちなみにネットワーク部とサブネット部とインターフェース部(ホスト部)の長さについては以下のようになっている。

結果

簡単に覚えやすい文字列からIPアドレスを生成できるようになった。

とはいえリストがないときでも生成できるようにという目的を考えるとツールに頼るのもどうなのかという… 最初に一通り設定したらもうツールの出番なさそう。

参考


  1. ネットワーク端子にそれぞれ割り振られている固有の値。使いまわされているものの基本的にはユニークで重複しないと考えていいらしい ↩︎