ま、そんなところで。

ニッチな技術系メモとか、車輪を再発明してみたりとか.

家庭内LANでIPv6のグローバルIP(GUA)とLAN内専用の固定IP(ULA)の両方を使う

プライベートLAN内専用の固定IPv6アドレスがほしい

IPv6対応の家庭用ルータが普及してきた

市販の家庭用ルータを使う我が家のネットワークでは、LAN内はIPv4固定IPアドレスを各マシンに割り当ててsshしたりプライベートなサーバ立てたりして使ってたんですが、昨今では、プロバイダからIPv4のアドレスだけでなくIPv6プレフィックスが当然のように割り当てられるようになりました.

いよいよIPv6対応が当然の前提とされるようになると、アプリなどでIPv6アドレスでのテストなどもしなきゃダメなので、IPv6の固定アドレスなんて欲しくなります.

IPv6のリンクローカルアドレスはどうにも使いづらい

通常、プロバイダの接続サービスと家庭用ルータがIPv6に対応していて、マシン側のIPv6が有効になっていれば、プロバイダからグローバルネットワーク識別子の払い出しを受けたルータからのRA(Router Advertisement; ルータ広告)によって、ローカルネットワーク内の各マシンでグローバルスコープのGUA(グローバルユニキャストアドレス)が自動で構成されます.
それと同時にローカルネットワーク内専用のリンクローカルアドレス(fe80::/64)が各マシンで自動的に生成されて、固定IPのように使えることになってるのですが、リンクローカルアドレスを通信で使うには、IPv6アドレスに加えてScopeIDなんてのが必要だったりします.
このScopeID、困ったことにツールやアプリごとに指定する方法が異なっている上に場合によっては対応していなかったり、どうにもこうにも使いづらいんですよね.

自動構成のGUAと固定のULAを同時に持たせれば・・?

幸いIPv6は一つのインタフェースに対してアドレスを複数持つことが出来る仕組みなので、自動構成によって割り当てられるグローバルIP(GUA)を持たせつつIPv4のプライベートアドレスっぽく使えるULA(ユニークローカルアドレス)を各マシンに固定IPとして割り当てて使うことは可能なはずです.

しかしながら、LinuxのNetworkManagerやmacOSでは、ネットワーク設定などでIPv6アドレスを 手動設定 とすると、ルータからのRAによるGUAの自動構成が行われなくなるため、ローカルネットワークの外とIPv6での通信が行えなくなり、自動構成のGUAを使うのか手動割当のULAかいずれかを選択する、といった状況になってしまいます.
さすがにこれでは本末転倒です.

そこで、もうちょっとなんとかできないかな〜、と色々調べてみました.

あとから追加でULAを割り当てる方法なら意外に簡単

GUAの自動構成はとりあえずそのままにして、ULAを自動構成させるにはルータ側でULAを自動構成させるための設定ができないといけませんが、さすがにそんな機能、安価な家庭用ルータに備わってる訳ありません.

しかし、あとから各マシンでコマンドを使ってULAを追加割当てする方法であれば、比較的に容易に実現できそうです.

ここではLinuxmacOSの手順を示します.

(準備) ULAのサブネットプレフィックスを準備する

ULAはプライベートネットワーク内での使用が想定されているとはいえ、グローバルスコープのUnicastAddressとして定義されてるものですから、SubnetPrefix部分はちゃんと RFC4193 に従ってEUI-64識別子を生成しなければなりません.

ただ、このEUI-64識別子の生成は手計算では到底無理なのでツールを使います.

こちらに生成用のpythonコードが公開されてますので、これを使いました.
www.jdoodle.com

ネットワーク内の代表マシンを適当に一つ選び、そのマシンが使用するネットワークインタフェースのMACアドレスから識別子を生成します.

$ ./ula_generator.py
Input MAC address: 12:34:56:78:ab:cd  # ←ここにMACアドレスを指定する

ULA Prefix        -> fd98:c3f9:eb41::/48
First Subnet      -> fd98:c3f9:eb41::/64
Last Subnet       -> fd98:c3f9:eb41:ffff::/64
First IPv6 Address-> fd98:c3f9:eb41::1/64

ULA Prefixが得られたので、サブネット識別部を 1 (0001) とし、fd98:c3f9:eb41:1::/64 をサブネットプレフィックスとします.

(Step1) とりあえず手動で割り当てて試してみる

サブネットプレフィックスができたら、まずは手動でULAを追加割り当てしてみます.
とりあえずこれでGUAとULAとリンクローカルアドレスの3タイプのIPv6アドレスを持つ状態にすることが出来ます.

(Linuxの場合) ip コマンドを使う

Linux系OSは ipコマンド で割り当てます.

# (コマンド)
$ sudo  ip -6 addr add {IPv6Addr}/{PrefixLength} dev {IntarfeceName}

# (例: eno1 に fd98:c3f9:eb41:1::2/64 を割り当て)
$ sudo ip -6 addr add fd98:c3f9:eb41:1::2/64 dev eno1

コマンドを実行したら、 ifconfig コマンドでネットワークの設定を確認します.

$ ifconfig eno1
eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.2  netmask 255.255.255.0  broadcast 192.168.1.255
        inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:db31  prefixlen 64  scopeid 0x0<global>
        inet6 fd98:c3f9:eb41:1::2  prefixlen 64  scopeid 0x0<global>  ★追加されている
        inet6 fe80::7404:eeb4:6b91:34b8  prefixlen 64  scopeid 0x20<link>
        inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:b06f  prefixlen 64  scopeid 0x0<global>
        ether 70:85:c2:7f:bb:7d  txqueuelen 1000  (イーサネット)
        RX packets 134402  bytes 68324237 (68.3 MB)
        RX errors 0  dropped 22346  overruns 0  frame 0
        TX packets 87596  bytes 20566693 (20.5 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        device interrupt 16  memory 0xa3200000-a3220000  

さらに、routeコマンドで静的ルーティングテーブルを確認します.

$ route -6
カーネルIPv6 経路テーブル
Destination                    Next Hop                   Flag Met Ref Use If
ip6-localhost/128              [::]                       U    256 2     0 lo
2405:xxxx:xxxx:xxxx::/64       [::]                       U    100 1     0 eno1
fd98:c3f9:eb41:1::/64          [::]                       U    256 3     0 eno1  ★追加されている
fe80::/64                      [::]                       U    100 2     0 eno1
[::]/0                         _gateway                   UG   20100 13     0 eno1
   :
(省略)
   :

(macOSの場合) ifconfig を使う

macOSでは ipコマンドは標準で組み込まれてないようです.
Homebrewを使って iproute2mac をインストールすれば ip コマンドが使えるのですが、 まずは標準の ifconfig で行うことにしました.

(コマンド)
$ sudo ifconfig {IntarfeceName} inet6 add {IPv6Addr} prefixlen {PrefixLength}

(例: en0 に fd98:c3f9:eb41:1::4/64 を割り当て)
$ sudo ifconfig en0 inet6 add fd98:c3f9:eb41:1::4 prefixlen 64

コマンド実行後、 ifconfig でインタフェースのアドレスを確認します.

$ ifconfig 
       :
    (省略)
       :
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=400<CHANNEL_IO>
    ether 88:e9:fe:4e:3d:19 
    inet6 fe80::8a6:a219:8c03:1cb6%en0 prefixlen 64 secured scopeid 0x4 
    inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:27ae prefixlen 64 autoconf secured 
    inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:7a3d prefixlen 64 autoconf temporary 
    inet6 fd98:c3f9:eb41:1::4 prefixlen 64  ★追加されている
    inet 192.168.1.4 netmask 0xffffff00 broadcast 192.168.1.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active

さらに、 netstat -rn で静的ルーティングテーブルを確認.

$ netstat -rn
Internet6:
Destination              Gateway                         Flags           Netif Expire
default                  fe80::8a6:a219:8c03:1cb6%en0    UGcg              en0       
::1                      ::1                             UHL               lo0       
     :  
   (省略)
     :
fd98:c3f9:eb41:1::/64    link#4                          UC                en0     ★追加されている
fd98:c3f9:eb41:1::4      88:e9:fe:4e:3d:19               UHL               lo0     ★追加されている

(Step2) マシンの起動時にコマンドで自動付与するように構成する

コマンドで付加したULAはマシンをシャットダウンするたびに消えてしまうので、このままでは毎回起動するたびにログインしてコマンドで付与しなければいけません.
ならば、マシンの起動時に自動的にULAを付加できるようになれば本格的に運用できそうです.

(Linuxの場合) NetworkManagerのフックスクリプトを使う

デスクトップ版のLinuxでは、Network関係の設定は通常 NetworkManager が行っているようですね.
NetworkManager には dispatcher の仕組みがあって、 /etc/NetworkManager/dispatcher.d/スクリプトや実行ファイルを配置すると、ネットワークデバイスの起動や状態変更のタイミングで呼び出してくれたりするので、これを使います.

以下のスクリプトを配置しました.
ファイルの所有者はrootにしておく必要があります.

#!/usr/bin/env bash

set -e

#
#  $1 = interface name. 
#  $2 = interface state. 
#

# $2をみて interface の 'up'と'down'のタイミングででコマンドを実行.

if [ "$2" = "connectivity-change" ]; then
    exit 0;
fi
 
if [ -z "$1" ]; then
    echo "$0: called with no interface" 1>&2
    exit 1;
fi

IP_CMD=$(which ip)

# インタフェース名
IFACE_NAME=eno1
# 追加で割当てるIPv6のULAとプレフィックス長
IP6_ULA=fd98:c3f9:eb41:1::2/64

if [ -n "${IP6_ULA}" ]; then 
    # 対象interfaceの呼び出しに絞り込み 
    if [ "$1" = "${IFACE_NAME}" ]; then
        # 'up' と 'down' の通知をトリガーに処理する
        case "$2" in
            up)
                # インタフェースの起動. ipを追加割当
                ${IP_CMD} -6 addr add ${IP6_ULA} dev $1 
                ;;
            down)
                # インタフェースのシャットダウン. ip割当を解除
                ${IP_CMD} -6 addr del ${IP6_ULA} dev $1
                ;;
            *)
                # Do nothing
                ;;
        esac
    fi
fi

exit 0;

(macOSの場合) launchd を使う

macOSの場合、非ログイン状態でNetworkManagerのようにネットワークデバイス起動/切断時のフックを行う仕組みはちょっと見当たりませんでした.
なにか良い方法があればと思いますが、まずは設定だけでも・・・と、launchdを使って起動時にコマンドを実行させるようにしました.

ただし、コマンドの実行はNetworkデバイスのSetupが完了した後でなければ正しくルーティングテーブルが更新されません.
そこで、launchd のスクリプト起動タイミングには RunAtLoad ではなく WatchPaths を使い、マシンの起動後にネットワークサービスが DNS情報/etc/resolv.conf を変更したタイミングで実行させるようにします.
(※ 今回macOSでは起動後のip付加のみ. downイベント相当のフックに使えそうなものはちょっと見当たらず・・要検討...)

以下の設定ファイルを所有者をrootにして /Library/LaunchDaemons/ に配置します.
呼び出すスクリプトファイルは、 /Users/hoge/bin/reg_ipv6_ula.bash としました.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<!-- jp.or.regv6ula.plist. -->
<!-- /Library/LaunchDaemons/ に配置 -->

<plist version="1.0">
<dict>
    <key>Label</key>
    <string>reg_ipv6_ula</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/env</string>
        <string>bash</string>
        <string>/Users/hoge/bin/reg_ipv6_ula.bash</string>
        <string>add</string>
    </array>
    <key>Disabled</key>
    <false/>
    <key>WatchPaths</key>
    <array>
        <string>/etc/resolv.conf</string>
    </array>
    <key>KeepAlive</key>
    <false/>
    <key>StandardErrorPath</key>
    <string>/Users/hoge/log/reg_ula_err.log</string>
</dict>
</plist>

呼び出すスクリプトファイル(reg_ipv6_ula.bash)の内容は以下の通り.

#!/usr/bin/env bash

#
# 登録/削除用Script
#

IFCFG_CMD=$(which ifconfig)

# インタフェース名
IFACE_NAME=en0
# 割り当てるULA
IPV6_ULA=fd98:c3f9:eb41:1::4

if [ -n "$1" ]; then
    case "$1" in
    remove)
        echo "unregister ULA.. ${IPV6_ULA}"
        ${IFCFG_CMD} ${IFACE_NAME} inet6 remove ${IPV6_ULA} prefixlen 64
        echo "Done."
        ;;
    add)
        echo "register ULA.. ${IPV6_ULA}"
        ${IFCFG_CMD} ${IFACE_NAME} inet6 add ${IPV6_ULA} prefixlen 64
        echo "Done."
        ;;
    *)
        ;;
    esac
fi

(参考) ところで、Windowsではどうするのだろう?

Windowsの場合はネットワークのプロパティからIPv6手動割当て とした場合でも、手動割り当てしたULAに加えてGUAもちゃんと自動構成されるようでした.
この動作だと「ULAだけ使う」という選択肢はなさそうですが、こちらは設定が簡単で良いですね.

参考サイト