【雑記】Synology NASでお名前.comのダイナミックDNS(DDNS)を自動で更新する方法

ちょっと前にSynologyのNAS(DS923+)を購入して、自宅サーバーを構築していたんだけど、まあこれが思った以上に便利。自宅には4台の自作PCと3台のMacがある。そのどれからも必要なデータにアクセスできるだけじゃなくて、外出先からもFTP経由でデータが取り出せたり、なんならiPhoneでも動画が見られたりできちゃう。

私が知ってたNASってMacのTimeCapsule時代に使ってた激重のイメージしかなかったので、もやは内蔵ストレージと遜色ないレベルにまでなっている昨今のNAS事情にビックリ。

せっかくなので、屋号として掲げている(掲げようとしている)ドメインを取得して放置していた別サイトを試験的に運用しようと、NASにWordPressをインストールして動かしてみた。

これが安定すれば、レンタルサーバーを借りることなくサイトを運営できるし、容量も気にしなくて済むということで、のんびり経過観察していたんだけど、記事なるのはIPの変更。

基本的に、取得したドメインは自宅のIPアドレス(グローバルIP)に紐付けているので、ルーターを再起動したり、なんかの拍子でIPアドレスが変更になったら、その都度紐付けしているIPアドレスの記述を手動で修正しなければならない。

▲IPアドレスの登録・変更はお名前.com Naviのドメイン→ドメイン機能一覧→DNS設定/DNS設定/転送設定-機能一覧→DNSレコード設定から行える

一番手っ取り早い解決策は、プロバイダーなどが提供している固定IPサービスに加入すること。安いところなら月額1,000円以下のものもありお買い得だ。私も、仕事でどうしても固定IPにしなければならないことがあったときは利用してたけど、仕事でもない限りわざわざそのサービスに入るのはもったいない感じするよね。

そこでダイナミックDNS(DNSS)の登場である。ダイナミックDNSは、IPアドレスが何かの拍子で変わっても、自動で登録したドメインとの紐付けを更新してくれる便利な機能で、ご丁寧にお名前.comには「お名前.com ダイナミックDNSクライアント」というアプリも存在する。

▲使い方も簡単で常駐しておけばIPアドレスが変わったら勝手に書き換えてくれる(https://help.onamae.com/answer/7920)

Windows版しかないけどParallelsを使えばMacでも常駐可能。とはいえ、そのためにWindows機を常時起動していたり、MacでParallelsを起動しっぱなしにするのかとなるとこれまた不便だよね。

そこで、このクライアントの挙動をNAS内で実行できるプログラムにできないかと考えていた——。前置きが長い(笑)。

もくじ

先人の力を借りてどうにか運用可能に

結論からいうと大成功だった!

というのも同じように考えていた先人(https://zenn.dev/tom_tan/articles/90394887c3f6c3)がいて、同じように試行錯誤していたので参考にさせてもらった。

しかし、ここで大きな壁にぶち当たる。記事を確認すると、なんと公開は約4年前。当時のSynology環境で動作していたPythonは2.x系で、現在のNASでは3.x系が標準だ。つまり、スクリプトをそのまま拝借しても動作しない。そこでChatGPTに協力してもらい、Python 3.x環境でも問題なく動くようにコードを修正してもらった。下記がその最新版スクリプトである。

#!/usr/bin/env python3

import os
import ssl
import socket
from urllib.request import urlopen

# ----------------------------------------------------------
# 設定(必要に応じて修正)
# ----------------------------------------------------------
DDNS_HOST = "ddnsclient.onamae.com"  # お名前.com DDNSサーバー
DDNS_PORT_TLS = 65010                # 通信ポート
ENV_PATH = "/volume1/homes/【ユーザーネーム】/.env"     # .envファイルの場所
LOG_PATH = "/var/log/onamae-ddns.log"                   # ログ出力先

# ----------------------------------------------------------
# ログ出力
# ----------------------------------------------------------
def log(msg):
    with open(LOG_PATH, "a") as f:
        f.write(msg + "\n")
    print(msg)

# ----------------------------------------------------------
# .env 読み込み(空欄も保持)
# ----------------------------------------------------------
def read_env(path):
    env = {}
    with open(path) as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            k, v = line.split("=", 1)
            env[k] = v
    return env

# ----------------------------------------------------------
# グローバルIPを取得
# ----------------------------------------------------------
def get_global_ipv4():
    with urlopen("https://ifconfig.me", timeout=10) as r:
        return r.read().decode().strip()

# ----------------------------------------------------------
# DDNS更新コマンド送信
# ----------------------------------------------------------
def ddns_update(user, pw, host, dom, ip):
    host = "" if host == "@" else host  # @は空文字に変換
    ctx = ssl.create_default_context()

    with socket.create_connection((DDNS_HOST, DDNS_PORT_TLS), timeout=15) as s:
        with ctx.wrap_socket(s, server_hostname=DDNS_HOST) as ss:
            def send(cmd):
                ss.sendall(cmd.encode())
                return ss.recv(4096).decode()

            send("")  # バナー読み捨て

            # ログイン
            send(f"LOGIN\nUSERID:{user}\nPASSWORD:{pw}\n.\n")

            # IPアドレス更新
            res = send(f"MODIP\nHOSTNAME:{host}\nDOMNAME:{dom}\nIPV4:{ip}\n.\n")
            if "000 COMMAND SUCCESSFUL" not in res:
                raise RuntimeError(res)

            # ログアウト
            send("LOGOUT\n.\n")

# ----------------------------------------------------------
# メイン処理
# ----------------------------------------------------------
def main():
    env = read_env(ENV_PATH)
    ip = get_global_ipv4()
    ddns_update(
        env["USERID"],
        env["PASSWORD"],
        env.get("HOSTNAME", ""),
        env["DOMNAME"],
        ip,
    )
    log(f"DNS records updated successfully. IPv4: {ip}")

if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        log(f"Failed to update DNS records. Exception: {e}")
USERID=お名前.comのユーザーID
PASSWORD=お名前.comのパスワード
HOSTNAME=
DOMNAME=ドメイン

用意するものは上記のスクリプトを記述したファイル「onamae-ddns-client.py」と、お名前.comのログイン情報などを記載する「.env」というファイルだ。どちらも適当なエディターで編集して保存の際にこの名前にすればOK。「onamae-ddns-client.py」は11〜14行目を自分の環境のものに書き換えるのを忘れずに。「.env」のHOSTNAMEは空欄でOK。DOMNAMEは登録しているドメインを入れる。

ちなみに.envのようにファイル名の頭に.(ドット)が付くと、不可視ファイル扱いになって、WindowsのExplorerやMacのFinderからは見えなくなる。不可視ファイルが見えるようにしておけば、半透明で見えるようになるので覚えておこう。MacならCommand+Shift+.(ドット)のショートカットで不可視ファイルが見えるようになる。

ファイルを用意したら、NASの適当な場所に保存しておく。おすすめはユーザーフォルダ。セキュリティー的にも管理のしやすさ的にも扱いやすいんじゃないかなぁと。ちなみにユーザーフォルダがない場合は、DSMのコントロールパネル→ユーザーとグループから設定できる。

▲ユーザーとグループで表示されるメニューから詳細のタブを開き、下部にある「ユーザーホームサービスを有効にする」チェックを入れると、指定したストレージにhomesフォルダが作成される
▲ユーザーフォルダにこんな感じで入れておけば問題ない

ここまでくれば準備完了。いよいよ実際にスクリプトを実行して、NAS内でお名前.comのDNSレコードが自動更新されるか確認してみよう。Macユーザーなら「ターミナル」、Windowsユーザーなら「PowerShell」から操作できる。

まあ大前提条件だけど、お名前.comでドメインのDNSレコードは記述しておくこと。もしくは、Windowsで「お名前.com ダイナミックDNSクライアント」を一度実行しておくこと。そしてSynologyのDSMにPythonがインストールしておくこと。というかWordPressが入っている時点でその作業はやっていると思うので割愛。

SSH接続を有効にする

まずSynologyのDSM内にターミナルからアクセスできるように設定する。DSMのコントロールパネル→端末とSNMPから設定できる。

▲端末タブにある「SSHサービスを有効にする。」にチェックを入れて適用すればOK

Synology NASにSSH接続

ssh 【ユーザー名】@【NASのローカルIPアドレス】

続けてターミナル経由でNASにアクセスする。なんか本当にいいの? みたいなこと聞かれるのでyesと入力してEnterを入力する。

スクリプトを実行する

/bin/python3 /volume1/homes/【ユーザー名】/onamae-ddns-client.py

うまく動作していれば、下記のような表記が表示される。
DNS records updated successfully. IPv4: xxx.xxx.xxx.xxx

最初にありがちなエラーが下記のエラー
Failed to update DNS records. Exception: Failed to modify IP address: 003 DBERROR

これは .env の設定に誤りがある場合(HOSTNAMEの空欄など)に発生した。特に「HOSTNAME=@」は使えないので、空欄で登録できるようスクリプトを修正しておくのがポイント。最初これに気づかず四苦八苦していたが、上記の公開コードはその辺も修正されているので、恐らくミスはでないかと。

タスクスケジューラで自動化

/bin/python3 /volume1/homes/【ユーザー名】/onamae-ddns-client.py >> /var/log/onamae-ddns.log 2>&1

動作が確認できたら、あとはDSM内で上記のスクリプトを定期的に自動で実行できるようにしておけばOK。コントロールパネル→タスク スケジューラーを開いたら、作成で新規タスクを作成しよう。

▲タスク設定にある「ユーザー指定のスクリプト」に上記のコードを入力する。ユーザー名はご自身のものに書き換えるのを忘れずに

スケジュールは1時間ごととかその辺はお好みで。ちなみにタブの上部にある「実行」をクリックすれば即時時効できる。実行後、スクリプトを入れたフォルダに「onamae-ddns.log」というファイルが作成され、エディターで開くと結果が表示されるようになっている。

▲適当なエディターで開けばちゃんと動作しているかも確認できる

記事化してみればなんてことないんだけど、実際はずっとエラーが出ててなんでエラーが出ているのかの原因究明が大変だった。ログファイル見るとずっとエラーが表示されてたんだけど、それはクイックルックで表示していたせいで、ずっと昔の記述が表示されていただけだったとか、HOSTNAMEを空欄で動作する方法が分からず、なんでできないのか悩んでいたりとか……。

ということで今後のための備忘録として最新版スクリプトを残しておこうと記事化してみた。どこかの誰かの手助けになれば幸いだー。

ちなみにNASでWDのHDD買うならRed一択。以前は生産終了となったGreenを使っていたんだけど、マジでゴミというレベルで壊れた。今まで攻略本作成の際にゲーム動画をしこたま保存しては削除というので、Greenは壊れることはあったけどRedやBlackならピンピンしたいたので、個人的にGreenと同じ立ち位置であるBlueは信用してないw

あとSeagateは2009年にHDD内部にアクセス不能になる致命的なバグ(https://xtech.nikkei.com/it/pc/article/news/20090119/1011485/)を起こし、COMポートかなんかを使って無理矢理ロックを解除するといった荒技で治療したことがあって、それがトラウマで買ってない……。

Synology NASキット 4ベイ DS923+/G
Synology
WESTERN DIGITAL WD120EFBX
ウエスタンデジタル(Western Digital)
もくじ