はじめに
自宅のネットワークでは ISP から動的にグローバル IPアドレスが割り当てられるため、外部からの接続には DDNS が欠かせません。AWS が公開しているサーバーレス DDNS ソリューション(Lambda + Route 53 + DynamoDB)を使って、EdgeRouter X から定期的に DNS レコードを更新する仕組みを構築しました。
この記事では、AWS 環境の構築から EdgeRouter X 上でのクライアント設定、動作確認までの手順を扱います。
DDNS を利用して EdgeRouter X にリモートアクセス VPN で接続することを目指していますが、まず DDNS を運用する環境を構築します。
EdgeRoter Xにリモートアクセス VPNで接続する方法を紹介した記事はこちらです。
詳細な仕組みは「AWS でサーバーレスダイナミック DNS システムを構築する」を参照してください。
参考情報
動作確認環境
EdgeRouter X(ER-X)のファームウェア 3.0.1 で動作確認を行いました。
フォークリポジトリについて
AWS 公式リポジトリ awslabs/route53-dynamic-dns-with-lambda をフォークし、revsystem/route53-dynamic-dns-with-lambda を作成しました。
フォーク側では以下の修正をブランチごとに行っています。
- fix/lambda-security: Lambda 関数のセキュリティ脆弱性を修正(タイミング攻撃、入力バリデーション、例外処理、情報漏洩)。コード品質の改善(関数名衝突解消、戻り値型統一、boto3 モジュールレベル化、Python 2 互換コード除去)
- fix/cdk-improvements: IAM ポリシーのリソース制限を強化(Route53, CloudWatch Logs)。DynamoDB の RemovalPolicy を RETAIN に変更し、課金モードを PAY_PER_REQUEST に変更。Lambda のログ保持期間と reserved concurrency を設定。未使用インポートの削除、CDK-Nag サプレッションの改善
- fix/client-scripts: dyndns.sh の変数クォート、演算子修正、非推奨コマンド置換、JSON 組み立て安全化。newrecord.py の JSON インジェクション防止、例外処理改善、シークレット非表示化
- test/add-tests: テスト未実装(アサーション全コメントアウト)を全面書き直し。CDK スタックテスト 7 件 + Lambda ユニットテスト 15 件の計 22 テスト。GET/SET 正常系、バリデーション、認証、エラーハンドリングをカバー
- docs/fix-typos-and-docs: README.md, invocation.md のタイポ修正。invocation.md のレスポンス例を Lambda 実装にあわせて更新。requirements.txt のバージョン範囲指定。cdk.json のフィーチャーフラグ最新化
- fix/cdk-nag-log-retention:
log_retention設定で CDK が自動生成するカスタムリソース Lambda に対する CDK-Nag サプレッションを追加。CDK 内部リソースのため変更不可、理由を明記した applies_to 付きサプレッションで対応 - feat/manage-record-script: managerecord.py を新規作成し、設定の確認・TTL の即時変更・レコードの削除をサブコマンドで実行できるようにした
CDK-Nag は CDK アプリケーションに対してセキュリティやコンプライアンスのベストプラクティスを自動チェックするツールです。エラーレベルの違反が検出されると cdk synth が失敗し、デプロイがブロックされます。警告レベルの違反はデプロイをブロックしません。意図的にルールを適用除外する場合は、理由を明記したサプレッション(抑制)を設定します。
AWS 環境の構築
前提条件
- Route 53 でホストされているドメインがある
- Lambda、DynamoDB、Route 53 を操作する権限がある
- AWS CLI が使える環境がある
アーキテクチャ概要
[操作者]
|
|-- newrecord.py (create/update) --> DynamoDB (hostname -> JSON)
|
[EdgeRouter X]
|-- dyndns.sh --> Lambda URL --> Lambda --> DynamoDB + Route 53
CDK スタックのデプロイ
このソリューションは AWS CDK でデプロイします。Lambda 関数、DynamoDB テーブル、IAM ロールが一括で作成されます。
リポジトリをクローンします。
git clone https://github.com/revsystem/route53-dynamic-dns-with-lambda.git
cd route53-dynamic-dns-with-lambda
Python の依存パッケージをインストールします。
pip install -r requirements.txt
CDK を初めてデプロイするアカウントとリージョンの組み合わせ(環境)ではブートストラップが必要です。同一アカウントでもリージョンが異なる場合は、各リージョンで個別に実行します。
cdk bootstrap
CDK CLI のバージョンが古いと言われた場合は更新します。
sudo npm install -g aws-cdk
スタックをデプロイします。
cdk deploy
デプロイが完了すると、Lambda 関数 URL が出力されます。この URL は後のクライアント設定で使います。
DNS レコードの設定
newrecord.py を実行すると、対話形式で Route 53 のホストゾーンとレコードセットを設定できます。設定内容は DynamoDB テーブルに保存されます。
python3 newrecord.py
AWS プロファイルやリージョンがデフォルトと異なる場合は、環境変数を指定して実行します。
AWS_PROFILE=production AWS_DEFAULT_REGION=us-east-1 python3 newrecord.py
ホストゾーン名(例: example.jp)、ホスト名(例: router.example.jp)、TTL(デフォルト 60)、shared secret(共有シークレット)を順に入力します。ホストゾーンが存在しない場合は新規作成するか確認されます。
shared secret は Lambda 関数 URL とあわせて認証の要となるため、十分に複雑な文字列を設定してください。openssl rand -hex 32 などで生成したランダム文字列の使用を推奨します。
入力が完了すると、設定内容の確認が表示されます。
##############################################
# #
# The following configuration will be saved: #
# #
Host name: router.example.jp
Hosted zone id: ZXXXXXXXXXXXXXXXXXXXXXXX
Record set TTL: 60
Secret: ********
# #
# do you want to continue? (y/n) #
# #
##############################################
設定が完了すると、dyndns.sh の実行に必要なパラメータが表示されます。
./dyndns.sh -m set -u https://XXXXXXXXXX.lambda-url.REGION.on.aws/ -h router.example.jp -s <YOUR_SECRET>
この Lambda 関数 URL と shared secret を控えておきます。EdgeRouter X 側の設定で使用します。
動作確認
dyndns.sh をテスト実行して、Lambda 関数と Route 53 の連携を確認します。shasum が使える環境では perl-Digest-SHA のインストールが必要な場合があります。
sudo yum install perl-Digest-SHA
IPアドレス取得のテストを行います。
./dyndns.sh -m get -u "https://XXXXXXXXXX.lambda-url.REGION.on.aws/"
パブリック IPアドレスが返れば、Lambda 関数は正常に動作しています。
DDNS クライアントの実装方式
EdgeRouter X には set service dns dynamic で設定できる DDNS 機能が組み込まれています。内部的には ddclient が動いており、dyndns2 や cloudflare など標準的なプロトコルに対応しています。
しかし、AWS の DDNS ソリューションは Lambda 関数 URL に対して JSON 形式の HTTP POST を送り、アプリケーションレベルで SHA-256 ハッシュによる独自認証を行うプロトコルを採用しています。Lambda 関数 URL 自体は認証タイプ NONE(パブリックアクセス)で公開されており、Lambda 関数内でリクエストに含まれるハッシュ値を検証することで認証を実現しています。ddclient にこのプロトコルの定義は存在しないため、組み込み DDNS 機能では対応できません。
代わりに、リポジトリで提供されているシェルスクリプト dyndns.sh を EdgeRouter X のタスクスケジューラで定期実行する方式をとります。
以降の手順は、EdgeRouter X の CLI または SSH 接続上で実行します。
dyndns.sh の配置
SSH で EdgeRouter X にログインし、スクリプトをダウンロードします。/config/scripts/ 配下に置いたファイルはファームウェアアップグレード後も保持されます。
curl -o /config/scripts/dyndns.sh https://raw.githubusercontent.com/revsystem/route53-dynamic-dns-with-lambda/master/dyndns.sh
chmod +x /config/scripts/dyndns.sh
dyndns.sh は内部で shasum -a 256 を使って認証用ハッシュを生成しています。ファームウェア 3.0.1 では shasum コマンドが含まれているため、スクリプトの修正は不要でした。古いファームウェアで shasum が見つからない場合は、該当行を openssl dgst -sha256 に書き換えることで対応できます。
dyndns.sh の動作確認
IPアドレス取得の確認
まず -m get モードで Lambda 関数 URL への接続と IPアドレス取得が成功するかを確認します。
/config/scripts/dyndns.sh -m get -u "https://XXXXXXXXXX.lambda-url.REGION.on.aws/"
グローバル IPアドレスが返れば、ネットワーク接続と Lambda 関数は正常に動作しています。ここで何も返らない場合は、Lambda 関数 URL の誤り、DNS 解決の問題、またはファイアウォールルールを疑ってください。
DNS レコード更新の確認
次に -m set モードで実際に Route 53 のレコードを更新します。
/config/scripts/dyndns.sh -m set \
-u "https://XXXXXXXXXX.lambda-url.REGION.on.aws/" \
-h "router.example.jp" \
-s "<YOUR_SECRET>"
成功すると以下のようなレスポンスが返ります。
{
"return_status": "success",
"return_message": "router.example.jp has been updated to XXX.XXX.XXX.XXX",
"status_code": "201"
}
IPアドレスに変更がない場合は以下のレスポンスになります。これも正常な動作です。
{
"return_status": "success",
"return_message": "Your IP address matches the current Route53 DNS record.",
"status_code": "200"
}
ラッパースクリプトの作成
dyndns.sh は引数で動作するため、タスクスケジューラから呼び出すためのラッパースクリプトを用意します。
cat << 'EOF' > /config/scripts/run-ddns.sh
#!/bin/bash
/config/scripts/dyndns.sh \
-m set \
-u "https://XXXXXXXXXX.lambda-url.REGION.on.aws/" \
-h "router.example.jp" \
-s "<YOUR_SECRET>" \
>> /var/log/aws-ddns.log 2>&1
EOF
chmod +x /config/scripts/run-ddns.sh
タスクスケジューラへの登録
EdgeOS CLI のコンフィグレーションモードでタスクスケジューラを設定します。
configure
set system task-scheduler task aws-ddns interval 300m
set system task-scheduler task aws-ddns executable path /config/scripts/run-ddns.sh
commit
save
exit
これで 300 分(5 時間)ごとに run-ddns.sh が実行されます。interval の値は ISP による IPアドレス変更の頻度に応じて調整してください。
設定の確認
タスクスケジューラの登録状態は以下で確認できます。
configure
show system task-scheduler
exit
DNS レスポンスの確認
Route 53 にレコードが正しく反映されたかを確認します。ここで一つ注意点があります。EdgeRouter X の CLI は Vyatta ベースであり、? をヘルプ呼び出しキーとして解釈します。operational mode($ プロンプト)でもこの動作は変わりません。そのため ? を含む URL を直接 CLI に貼り付けることができません。
この問題を回避するには、vi でスクリプトファイルを作成して実行します。echo を使っても CLI が ? を解釈してしまうため、エディタで直接記述する必要があります。
vi /tmp/dnscheck.sh
以下の内容を記述して保存します。
curl -s "https://dns.google/resolve?name=router.example.jp&type=A"
作成したスクリプトを実行します。
sh /tmp/dnscheck.sh
正常であれば以下のような JSON が返ります。
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": false,
"CD": false,
"Question": [{"name": "router.example.jp.", "type": 1}],
"Answer": [{"name": "router.example.jp.", "type": 1, "TTL": 60, "data": "XXX.XXX.XXX.XXX"}],
"Comment": "Response from 205.251.196.248."
}
Answer 内の data フィールドに表示される IPアドレスが、WAN 側インターフェースの IPアドレスと一致していれば、DDNS の構成は正常に動作しています。WAN IPアドレスは show interfaces で確認できます。Comment に含まれる 205.251.196.248 は Route 53 のネームサーバーの IPアドレスであり、AWS 側でレコードが応答していることを示しています。
dig や nslookup が使いたい場合は dnsutils パッケージをインストールできますが、EdgeRouter X はストレージが 256MB NAND と限られています。DNS の確認程度であれば上記の方法で十分であり、不要なパッケージの追加は避けたほうがよいでしょう。
? を含まない確認方法としては ping も使えます。
ping router.example.jp
1 行目に PING router.example.jp (XXX.XXX.XXX.XXX) と表示され、解決された IPアドレスを確認できます。ルーター配下の LAN から実行した場合、ルーター自身の WAN IPアドレスに対する ping となるため応答は返りません。Ctrl+C で停止します。名前解決の成否だけを確認する用途で使います。
ログの確認
run-ddns.sh の実行結果は /var/log/aws-ddns.log に蓄積されます。
cat /var/log/aws-ddns.log
タスクスケジューラの実行状況はシステムログに記録されない場合があります。スケジューラの動作確認は /var/log/aws-ddns.log へのログ出力で判断してください。
DNS レコードの管理
newrecord.py で設定した内容の確認・変更・削除には managerecord.py を使います。DynamoDB のテーブル名は CloudFormation から自動で解決されるため、手動で調べる必要はありません。
設定のやり直し・shared secret の変更
newrecord.py を再実行すると、同じホスト名の設定を上書きできます。TTL や shared secret を変更したい場合もこの方法で対応できます。
現在の設定確認
python3 managerecord.py show router.example.jp
DynamoDB に保存された設定と、Route 53 の現在の IP・TTL をあわせて表示します。
Hostname : router.example.jp
Hosted zone ID : ZXXXXXXXXXXXXXXXXXXXXXXX
TTL : 60
Secret : ********
Current IP : XXX.XXX.XXX.XXX
Route 53 TTL : 60
TTL の即時反映
Lambda 関数は IPアドレスの変化のみを判定しており、TTL の比較は行いません。そのため IPアドレスが変わらない限り、Route 53 側の TTL は更新されません。TTL の変更を即座に反映するには update-ttl サブコマンドを使います。
python3 managerecord.py update-ttl router.example.jp 300
DynamoDB の TTL と Route 53 のレコードを同時に更新します。次回 IPアドレスが変わったタイミングからは、更新後の TTL が自動で適用されます。
レコードの削除
DynamoDB のレコードのみ削除する場合は以下を実行します。
python3 managerecord.py delete router.example.jp
Route 53 の DNS レコードもあわせて削除する場合は --also-route53 を付けます。
python3 managerecord.py delete --also-route53 router.example.jp
いずれの場合も実行前に確認プロンプトが表示されます。
まとめ
Route 53 の DNS レコードを EdgeRouter X で更新するための DDNS クライアントを AWS Lambda + Route 53 + DynamoDB で構築しました。これにより、外部の DDNS サービスに依存せず、AWS 環境だけで DDNS を運用できます。
この仕組みは、シェルスクリプト内の curl コマンドで IPアドレスの取得と Lambda 関数 URL の呼び出しを行っているため、curl コマンドや cron 等のスケジューラーが利用できる他のネットワーク機器やサーバーでも応用可能です。
AWS のリポジトリでは設定の変更や削除を行う仕組みが提供されていないため、managerecord.py を作成して対応しました。設定の確認・TTL の即時変更・レコードの削除をサブコマンドで実行できます。