Minami Ueda

EdgeRouter X (ER-X) についての記事 3 本目です。リモートアクセス VPN をセットアップする準備として、今回は ER-X から CloudFlare の DDNS (Dynamic DNS) を更新する設定をしていきます。ER-X は標準で DDNS 更新機能を持っているのですが、何度か試してみたところ、ルータの WAN 側にグローバル IP アドレスが振られていないと使えないようでした。私の場合 ER-X が NAT 配下にあり、グローバル IPv4 アドレスを直接持たない環境のため、シェルスクリプトで DDNS を更新することにしました。

これまでの記事はこちら。

  1. EdgeRouter X を NURO 光で使う ― その1: 初期設定
  2. EdgeRouter X を NURO 光で使う ― その2: IPv6 with ndppd

変動型 IP アドレスと DDNS

NURO 光を含め、家庭用として提供されているインターネット回線のほとんどは、変動型グローバル IP アドレスを持ちます。家の中からインターネットにアクセスする分には変動型でも特に問題ないのですが、外部から VPN などでアクセスしたい場合、IP アドレスが変わってしまうと困ってしまいます。例えるならば、外出先から家に電話をかけたいのに、家の電話番号がいつの間にか変わってしまっているようなものです。

このような課題を解決するのが DDNS (Dynamic DNS / ダイナミックDNS ) です。DNS 自体は ドメイン名(厳密には FQDN )と IP アドレスを紐付けて名前解決を行う仕組み(例えば www.sfc.keio.ac.jp133.27.4.220)ですが、DDNS ではこの IP アドレスを動的に変更します。

具体的な使い方としては、DDNS のサービス(今回は CloudFlare)に事前にホスト名を登録しておき、そこに対して自宅ネットワークのグローバル IP アドレスを定期的に通知することによって、DNS レコードを更新します。こうすることによって、グローバル IP アドレスが変わったとしても、常に同じホスト名を使って自宅ネットワークに到達できるようになるわけです。

CloudFlare で DNS レコードを設定する

DDNS サービスは無料のものを含めていろいろありますが、今回は日頃から minami.me ドメインの DNS サーバとして使用している CloudFlare を使用します。UI がシンプルでわかりやすいほか、CDN のプロキシ機能と設定画面が統合されていて使いやすいため、とてもおすすめです。CloudFlare を普段から使っていないという方は、Duck DNS など別の DDNS サービスを使うのが良いでしょう。

ここでは設定の例として、hogehoge.minami.meというサブドメインを DDNS 用に設定するとします。

CloudFlare の DNS 設定画面を開き、Add record ボタンから DNS レコードを追加します。

Type: A
Name: hogehoge
IPv4 address: 8.8.8.8 (いずれ更新するので適当なIPアドレス)
TTL: Auto
Proxy status: DNS only

CloudFlare で Zone ID / Global API key を取得

今回紹介するスクリプトでは CloudFlare の API を用いることによって、DNS レコードを更新します。そのために、まず CloudFlare 管理画面から Zone ID と Global API key を取得しておく必要があります。

まずは、管理画面の Overview ページの一番下に API というセクションがあるので、そこに書いてあるZone IDを控えておきましょう。また、同じ場所に Get your API token というリンクがあるので、遷移先のページでGlobal API Keyを取得します。こちらもあとで使うようにメモしておきます。

DDNS 更新スクリプト

冒頭で書いたように、ER-X は標準で DDNS 更新機能を持っています。しかし ER-X が NAT 配下にあって、グローバル IPv4 アドレスを直接持たない環境だとこれが使えないようなので、今回はシェルスクリプトで DDNS を更新することにしました。グローバル IPv4 アドレス が ER-X に振られている場合は、標準の DDNS クライアントを使用した方が良いかと思います。

では、DDNS を更新するためのシェルスクリプトを準備していきます。コードをテキストエディタにコピーした上で、変数を書き換えます。編集が完了したらddns-cloudflare.shなど、適当な名前で保存します。

  • auth_email: CloudFlare で使用しているメールアドレス
  • auth_key: 上で取得した Global API Key
  • zone_identifier: 上で取得した Zone ID
  • record_name: 設定する DDNS 用サブドメイン(今回の例では hogehoge.minami.me )

GitHub にもリポジトリを作ってあるので、こちらも適宜ご確認ください。
mu373/cloudflare-ddns: Update Cloudflare DNS record with shell script (on EdgeRouter X)

#!/bin/bash

# Forked from lifehome/systemd-cfddns/src/cfupdater-v4

# CHANGE THESE
auth_email="mail@hogehoge.com"     # The email used to login 'https://dash.cloudflare.com'
auth_key="xxxxx"     # Top right corner, "My profile" > "Global API Key"
zone_identifier="xxxxx"     # Can be found in the "Overview" tab of your domain
record_name="fuga.hoge.com"     # Which record you want to be synced

# Fetch Global IP address
ip=$(curl -s inet-ip.info)

# Seek for the current record on Cloudflare
record=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records?name=$record_name" \
    -H "X-Auth-Email: $auth_email" \
    -H "X-Auth-Key: $auth_key" \
    -H "Content-Type: application/json")

# Can't do anything without the record
if [[ $record == *"\"count\":0"* ]]; then
    >&2 echo -e "[Cloudflare DDNS] Record does not exist, perhaps create one first?"
    exit 1
fi

# Extract existing IP address from the fetched record
old_ip=$(echo $record | grep -o '"content": "[^"]*' | grep -o '[^"]*$' | head -1)
echo "[Cloudflare DDNS] Currently set to $old_ip"

# Compare if they're the same
if [ "$ip" == "$old_ip" ]; then
    echo "[Cloudflare DDNS] IP has not changed."
    exit 0
fi

# Extract record identifier from result
record_identifier=$(echo $record | grep -o '"id": "[^"]*' | grep -o '[^"]*$' | head -1)

# Update IP address through Cloudflare API
update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_identifier" \
    -H "X-Auth-Email: $auth_email" \
    -H "X-Auth-Key: $auth_key" \
    -H "Content-Type: application/json" \
    --data "{\"type\":\"A\",\"name\":\"$record_name\",\"content\":\"$ip\",\"ttl\":1}")

# Log IP address if it has changed
currenttime=$(date '+%Y-%m-%dT%H:%M%z')
echo "$currenttime, $ip" >> ip-address.txt

# Result
success=$(echo $update | grep -o '"success": .*, "'| awk '{sub(",.*$",""); print $0 }' | awk '{sub("\"success\": ",""); print $0 }')

case "$success" in
"true")
    echo "[Cloudflare DDNS] IPv4 context '$ip' has been synced to Cloudflare.";;
*)
    >&2 echo -e "[Cloudflare DDNS] Update failed for $record_identifier. DUMPING RESULTS:\n$update"
    exit 1;;
esac

スクリプトの流れとしては、以下のようなことをしています。

  • グローバル IP アドレスを外部サイトから取得(inet-ip.info
  • CloudFlare に登録されているレコードを確認
  • 登録されている IP アドレスと現在の IP アドレスを比較
  • 変わっていた場合は以下の処理を行う
    • CloudFlare API で DNS レコードの IP アドレスを新しいものに更新
    • ip-address.txt に日時と新しい IP アドレスをログする

フォーク元のコードはgrep -Poを使って JSON の値を抽出しているのですが、ER-X では-Pオプションが使用できないため、grep -oを駆使して抽出を行うようにしました。
参考:shell – How to retrieve single value with grep from Json? – Stack Overflow

EdgeRouter X での設定

作成したシェルスクリプトを scp などで ER-X の/config/user-data/ディレクトリにコピーします。コピーが完了したら、ER-X 上でchmod 755 ddns-cloudflare.shとし、スクリプトに実行権限を付与しておきます。

試しにsh ddns-cloudflare.shを実行してみましょう。うまく行っていれば、CloudFlare の DNS 管理画面に表示される IP アドレスが、最初に設定した 8.8.8.8 から別のものに差し替わっているはずです。Mac など任意の端末からdig hogehoge.minami.meを実行して、実際に DNS レコードが更新されているかも確認してみましょう。

定期的にスクリプトを実行するように cron を設定します。今回は 15 分間隔にしていますが、DNS サーバに負荷がかかりすぎない程度に間隔を調整してもいいでしょう。とはいえ、実際 IP アドレスはそう頻繁に更新されるものでもないので、30 分間隔でも十分だと思います。

$ configure
# set system task-scheduler task ddns-update executable path /config/user-data/ddns-cloudflare.sh
# set system task-scheduler task ddns-update interval 15m
# commit; save; exit;

まとめ

これで NAT 配下に配置されていて グローバル IPv4 アドレスを持たない EdgeRouter X からでも、CloudFlare の DDNS を更新することができるようになりました。CloudFlare API から返された JSON の値の抽出を grep で行っているので、API の出力フォーマットが少しでも変わると動かなくなってしまいますが、今のところは安定して動いています。

VPN をセットアップするための事前準備はこれで完了です。次回はいよいよ EdgeRouter X で L2TP / IPSec の VPN を設定していきます。