ちょっと前にSynologyのNAS(DS923+)を購入して、自宅サーバーを構築していたんだけど、まあこれが思った以上に便利。自宅には4台の自作PCと3台のMacがある。そのどれからも必要なデータにアクセスできるだけじゃなくて、外出先からもFTP経由でデータが取り出せたり、なんならiPhoneでも動画が見られたりできちゃう。
私が知ってたNASってMacのTimeCapsule時代に使ってた激重のイメージしかなかったので、もやは内蔵ストレージと遜色ないレベルにまでなっている昨今のNAS事情にビックリ。
せっかくなので、屋号として掲げている(掲げようとしている)ドメインを取得して放置していた別サイトを試験的に運用しようと、NASにWordPressをインストールして動かしてみた。
これが安定すれば、レンタルサーバーを借りることなくサイトを運営できるし、容量も気にしなくて済むということで、のんびり経過観察していたんだけど、記事なるのはIPの変更。
基本的に、取得したドメインは自宅のIPアドレス(グローバルIP)に紐付けているので、ルーターを再起動したり、なんかの拍子でIPアドレスが変更になったら、その都度紐付けしているIPアドレスの記述を手動で修正しなければならない。

一番手っ取り早い解決策は、プロバイダーなどが提供している固定IPサービスに加入すること。安いところなら月額1,000円以下のものもありお買い得だ。私も、仕事でどうしても固定IPにしなければならないことがあったときは利用してたけど、仕事でもない限りわざわざそのサービスに入るのはもったいない感じするよね。
そこでダイナミックDNS(DNSS)の登場である。ダイナミックDNSは、IPアドレスが何かの拍子で変わっても、自動で登録したドメインとの紐付けを更新してくれる便利な機能で、ご丁寧にお名前.comには「お名前.com ダイナミックDNSクライアント」というアプリも存在する。

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のコントロールパネル→ユーザーとグループから設定できる。


ここまでくれば準備完了。いよいよ実際にスクリプトを実行して、NAS内でお名前.comのDNSレコードが自動更新されるか確認してみよう。Macユーザーなら「ターミナル」、Windowsユーザーなら「PowerShell」から操作できる。
まあ大前提条件だけど、お名前.comでドメインのDNSレコードは記述しておくこと。もしくは、Windowsで「お名前.com ダイナミックDNSクライアント」を一度実行しておくこと。そしてSynologyのDSMにPythonがインストールしておくこと。というかWordPressが入っている時点でその作業はやっていると思うので割愛。
SSH接続を有効にする
まずSynologyのDSM内にターミナルからアクセスできるように設定する。DSMのコントロールパネル→端末とSNMPから設定できる。

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ポートかなんかを使って無理矢理ロックを解除するといった荒技で治療したことがあって、それがトラウマで買ってない……。



