「お名前.com」DDNSサービスの IPアドレス自動更新 - Aレコード情報を最新に保つ

はじめに

@niftyのサブドメインではDKIM認証とDMARC認証に対応できない

これまでは @niftyドメインサービスで取得したサブドメイン(xxx.atnifty.com)を使用して自宅サーバを運用してきました。

ところが最近では自宅メールサーバからメールを送信しようとしても、DMARC認証を採用するドメイン宛てのメール(例えばGmail 宛てのメール)では、SPF認証とDKIM認証の双方でfailしてしまい、したがってDMARC認証にもfailしてしまい、メール送信できないということが増えてきました。

Envelop-fromヘッダ情報に基づくSPF認証については「OCNメールサーバへのリレー設定」を行っていれば、OCNサーバが信頼できる送信元であるとして問題なくpassしますが、DMARC認証のベースとなるFromヘッダ情報に基づくSPF認証では DNSレコードを細かく設定することの出来ない@niftyサブドメインでは対応しきれません。

@niftyサブドメイン(xxx.atnifty.com)のDDNSサービスで設定可能なレコードは「Aレコードが1つ」と「CNAMEが2つ(「www. ~」と「ftp. ~」のみ)」だけです。

これではDKIM認証にもDMARC認証にも対応できません。
そこで今回は自ドメインのDNS回りを見直すことにします。

 

結局「お名前.com」に乗り換えることにしました

レコード設定がある程度自由に行えるドメイン管理サービス業者であればどこでも良かったのですが、価格や機能を比較して「お名前.com」に乗り換えることにしました。

@niftyで取得したドメインの管理を「お名前.com」に移譲する選択肢もあるようですが、「お名前.com」のドメイン取得サービスを利用してしまうのが一番シンプルで価格も安上がりのようです。そんなわけで思い切って新ドメインを取得することにしました。

これでAレコード・CNAMEレコード・MXレコード・TXTレコードの設定が可能になり、メール送信で悩まされることもなくなります。

 

IPアドレス更新をどうする!?

私は固定IPアドレスを持っておりませんので、@niftyサブドメインを利用していたときは、こちらのPerlスクリプトを利用させて頂いてDDNSサービスの IPアドレス更新をしていました。

「お名前.com」でもWindows版 DDNSクライアントが配布されているようです。
ただ私のWindows環境ではなぜかエラーが出てしまい、インストールできませんでした。(その後トライしていませんので現在の動作状況は分かりません。)

またできれば常時稼働させているLinuxマシン上でIPアドレスの自動更新を実現したかったので、何とか自動更新用のスクリプトを作れないものか調査してみることにしました。

※実は私が使用しているNECのルーター(Aterm)はDDNS更新機能を搭載しており、「お名前.com」とも連携することができます。それを使う手もあるのですが、複数のAレコード設定や他のレコード設定には対応していないので、万全ではありません。

Python 3.x をインストールする

今回基にさせて頂くスクリプトは こちらのスクリプトです。
このスクリプトはPython3で記述されていますので、デフォルトで2.x系を採用しているCentOSの場合は自分で3.xをインストールします。

ちなみに普段私が使っているPython環境は「Windows+VS Code+Python3.x」です。VS Codeは初期設定こそ少し戸惑いますが、一度設定してしまえばとても使いやすいIDEです。
今回CentOSにPython3.xをインストールするのはあくまで今回のスクリプトを動かすためだけですので、CentOSが利用するPythonバージョンを3.xに変更したい訳ではありません。3.xにしてしまうとシステムにどんな影響が及ぶか分からないのと、3.xを使いたい場合は “python3 ~” とコマンドすれば良いだけなので、今回はPythonコマンドのパス変更等は行わないことにします。

というわけで今回は単純に3.x系のインストールとpipのアップデートのみを行います。CentOS7.8ではリポジトリを追加しなくてもインストールすることが可能でした。

# dnf install python3
# python3 -V
Python 3.6.8

# pip3 install --upgrade pip
# pip3 -V
pip 20.1 from /usr/local/lib/python3.6/site-packages/pip (python 3.6)


これでPython 3.6.8とpip 20.1がインストールされました。
なお今回基にさせて頂くPythonスクリプトはPython3の標準モジュールだけで動作しますので、追加のモジュールをインストールする必要はありません。

 

「お名前.com」のAレコードを更新する

更新スクリプト本体

オリジナルのスクリプトを少しだけ私の環境に合わせてアレンジさせて頂きながら使用します。アレンジの内容は、

    • グローバルIPアドレスの取得は別のスクリプトに分けて記述し、その結果は別のテキストファイルに保存しておくことにします。

      更新したいAレコードが複数ある場合、Aレコードごとにチェックサイトにアクセスしているとその分アクセス回数が増えてしまいます。それを回避するためにアクセスは一回のみ行い、その結果を用いてすべてのAレコードを更新するようにします。

    • DNS情報の取得についても別スクリプト(bashスクリプト)に分けて記述し、その結果をテキストファイルに保存しておくことにします。

      というのもオリジナルのスクリプトでは GoogleのDNSチェックサイトを用いてドメインの正引き情報を取得しています。
      でも何度か実験した限りでは、(「お名前.com」の Aレコード更新後も GoogleのDNSサーバーに反映されるまでにタイムラグがありますから)「お名前.com」のDNS設定を無事に書き換え完了した場合であっても、スクリプト側はまだ書き換えを完了していないものと思い込み、再び書き換え処理に入ろうとしてしまいます。特にスクリプトの起動間隔を短く設定した場合は、無駄な更新処理が何回も繰り返されてしまいました。
      そこでDNS情報については直接「お名前.com」のDNSサーバーに問い合わせることでタイムラグを気にしなくて済むようにしました。
      ただ私の場合 Python関数でDNSサーバーを指定しながらレコード検索を行う方法が見つけられませんでしたので、bashスクリプトで普通にdigコマンドを実行することでレコード検索を行うことにしました。bashスクリプトとなったことで、必然的に本体とは別に分けて記述することになりました。

    • 「お名前.com」のDNSサーバーとのソケット通信確立後、recv() を実施してからその後の通信を行うようにします。

      私のCentOSのPython環境では「お名前.com」のDNSサーバーとのソケット通信確立後、先方からの応答メッセージを受信してからでなければその後の通信処理が正常に行われませんでした。
      不思議なことにWindowsのPython環境では応答メッセージの受信を気にしなくても通信が正常に行われます。生成されるSocketオブジェクトに何か違いでもあるのでしょうか??
      いずれにしてもサーバー側の送信メッセージに残りがないことを保証してから通信を続けたりソケットを閉じたりすのが本来のやり方なのかもしれません。


これらを考慮に入れたスクリプトは以下のとおりです。

</usr/local/bin/onamaeddns/onamaeddns.py>

#!/usr/bin/python3
# coding: utf-8

import ssl
import socket
import sys
import re


# 作業用ディレクトリ
basedir = "/usr/local/bin/onamaeddns"

#「お名前.com」の基本情報
userid = "********"
password = "********"
domainname = "example.com"

# ホスト名(hostname)はここでは定義せず、スクリプト実行時に引数として渡すことにする
# ex) ./onamaeddns.py       =>「example.com」のAレコードを更新
# ex) ./onamaeddns.py host  =>「host.example.com」のAレコードを更新


# グローバルIPアドレスを確認するスクリプトを別途用意し、その結果を
#「basedir/ip.txt」に保管しておく

#「お名前.com」のDNSサーバーに直接問い合わせるスクリプトを別途用意し、その結果を
#「basedir/dns_(ホスト名.)example.com.txt」に保管しておく
# ex)「basedir/dns_example.com.txt」,「basedir/dns_host1.example.com.txt」 


# グローバルIPアドレスをファイルから読み込む
def getip():
    try:
        with open(basedir + '/ip.txt', encoding="utf-8") as f:
            line = f.read().rstrip('\n')
            if re.fullmatch(\
r'(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])',\
line):
                return line
            else:
                print("グローバルIPを正常に読み込めませんでした")
                return "0.0.0.0"
    except:
        print("グローバルIPを正常に読み込めませんでした")
        return "0.0.0.0"


#「お名前.com」のDNSサーバーへの問い合わせ結果をファイルから読み込む
def getdns(fqdn):
    try:
        with open(basedir + '/dns_' + fqdn + '.txt', encoding="utf-8") as f:
            line = f.read().rstrip('\n')
            if re.fullmatch(\
r'(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])',\
line):
                return line
            else:
                print("DNS情報を正常に読み込めませんでした")
                return "0.0.0.0"
    except:
        print("DNS情報を正常に読み込めませんでした")
        return "0.0.0.0"


# Aレコードを更新する
def updateip(ip):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        try:
            sock.settimeout(3)   # データ送信の合間にもrecv()を設けることにしたので短めに設定
            s = ssl.wrap_socket(sock)
            s.connect(("ddnsclient.onamae.com", 65010))
            s.recv(1024)         # 私のWindows環境では不要だったが、CentOS7では必要だった

            data = """LOGIN
USERID:{userid}
PASSWORD:{password}
.
MODIP
HOSTNAME:{hostname}
DOMNAME:{domainname}
IPV4:{ip}
.
LOGOUT
.""".format(userid=userid, password=password, hostname=hostname,\
domainname=domainname, ip=ip)

            for line in data.split("\n"):
                s.sendall(line.encode() + b"\r\n")
                if line == ".":   # オリジナルではコメントアウトされていたが、一応インする
                    s.recv(1024)  # 実際はコメントアウトしても通信は正常に行われた

        except:
            print('"お名前.com" のDNSサーバーとのソケット通信が正常に終了しませんでした')


if __name__ == "__main__":
    if len(sys.argv) <= 1:
        hostname = ""
        fullname = domainname
    else:
        hostname = sys.argv[1]
        fullname = hostname + '.' + domainname 

    new_ip = getip()
    old_ip = getdns(fullname)

    if new_ip != "0.0.0.0" and old_ip != "0.0.0.0" and new_ip != old_ip:
        updateip(new_ip)
        print('"' + fullname + '" のIPアドレスを更新しました')
        print(' 更新前IPアドレス: ', old_ip)
        print(' 更新後IPアドレス: ', new_ip)


一々テキストファイルを介して情報を取得しているので何だか回りくどいスクリプトになってしまいましたが、Aレコードが複数個ある場合にはそれほど悪くないのかなと思います。

またAレコードが複数個あることを想定し、このスクリプトの実行時にはホスト名を引数として渡せるようにしました。

ついでですが、入手した情報が正しいIPアドレス形式であるかどうかも確認するようにしています。

グローバルIPを確認する Pythonスクリプト

内容はオリジナルのスクリプトを踏襲しつつ、あとはファイルへの書き込み処理を加えます。

</usr/local/bin/onamaeddns/getip.py>

#!/usr/bin/python3
# coding: utf-8

import urllib.request
import re


# 作業用ベースディレクトリ
basedir = "/usr/local/bin/onamaeddns"


def getip():
    url = "http://inet-ip.info/ip"
    req = urllib.request.Request(url)
    try:
        with urllib.request.urlopen(req) as res:
            ip = res.read().decode()
            if re.fullmatch(r'(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])', ip):
                return ip
            else:
                print("グローバルIPを正常に取得できませんでした")
                return "0.0.0.0"
    except:
        print("グローバルIPを正常に取得できませんでした")
        return "0.0.0.0"


def saveip(ip):
    try:
        with open(basedir + '/ip.txt', mode="w", encoding="utf-8") as f:
            f.write(ip)
    except Exception as e:
        print("グローバルIPの書き込みに失敗しました")


if __name__ == "__main__":
    new_ip = getip()
    saveip(new_ip)

 

DNSサーバーへの問い合わせを行う bashスクリプト

Googleサイトではなく「お名前.com」のDNSサーバーに直接問い合わせたい…
本当なら Python関数を用いてDNSサーバーを指定しながらレコード検索する方法が分かればわざわざ bashスクリプトに分ける必要はなかったのですが、現時点では分かりませんでしたしたのでbashスクリプトを用いることにします。

bashスクリプト自体はとても簡単です。digコマンドを用いて直接「01.dnsv.jp」(=お名前.com のDNSレンタルサービス) に問い合わせます。

</usr/local/bin/onamaeddns/getdns.sh>

#!/usr/bin/bash

domainname="example.com"

# 作業用ディレクトリ
basedir="/usr/local/bin/onamaeddns"

# このスクリプト実行時に引数としてホスト名を渡すことにする
# ex) ./getdns.sh       =>「example.com」のAレコードを確認
# ex) ./getdns.sh host  =>「host.example.com」のAレコードを確認 


if [ $# -eq 0 ]; then
    domain=$domainname
else
    domain="$1.${domainname}"
fi

dig $domain @01.dnsv.jp +short > "${basedir}/dns_${domain}.txt"


私の場合はAレコードを3個ほど管理する予定ですが、いずれのAレコードも同一の IPアドレスを使用します。
でもそれらのAレコードの設定状況にズレがある可能性を考慮して(IPアドレスの一括更新が途中で失敗した場合など)、Aレコードごとに別々に問い合わせられるようホスト名を引数として渡せるようにしました。

 

スクリプトを実行する

crondへの登録

ホスト数が多いのであれば、お好みでスクリプトに渡す引数(ホスト名)を一纏めにしたスクリプトを用意しても良いかもしれません。

getdns.sh をすべてのホストについて一纏めにしたスクリプトです。
</usr/local/bin/getdns_all.sh>

#!/usr/bin/bash

# 作業用ディレクトリ
basedir="/usr/local/bin/onamaeddns"

"${basedir}/getdns.sh"         # =>「example.com」を問い合わせる
"${basedir}/getdns.sh" host1   # =>「host1.example.com」を問い合わせる
"${basedir}/getdns.sh" host2   # =>「host2.example.com」を問い合わせる
"${basedir}/getdns.sh" host3   # =>「host3.example.com」を問い合わせる


同様に anamaeddns.py についてもすべてのホスト名について一纏めにしました。
</usr/local/bin/onamaeddns_all.sh>

#!/usr/bin/bash

# 作業用ディレクトリ
basedir="/usr/local/bin/onamaeddns"

"${basedir}/onamaeddns.py"         # =>「example.com」のAレコードを更新する
"${basedir}/onamaeddns.py" host1   # =>「host1.example.com」のAレコードを更新する
"${basedir}/onamaeddns.py" host2   # =>「host2.example.com」のAレコードを更新する
"${basedir}/onamaeddns.py" host3   # =>「host3.example.com」のAレコードを更新する


最後にここまで作成したスクリプトを crond に登録します。

# crontab -e
*/5 * * * * /usr/local/bin/onamaeddns/getip.py && /usr/local/bin/onamaeddns/getdns_all.sh
1-59/5 * * * * /usr/local/bin/onamaeddns/onamaeddns_all.sh

 

動作確認

ルーターを再起動し、意図的にグローバルIPアドレスを変更してみます。

「お名前.com」のDNS管理ページを確認してみると、すべてのホスト名に対して問題なくAレコードのIPアドレス部分が更新されたようです。
また更新頻度を 5分間隔で設定しても無駄な更新処理に入ることはなかったので、期待通りの動作でした。

 

あとがき

今回は複数のAレコードを一括更新したかったのでPythonスクリプトを用いましたが、交信するAレコードが一つしかない場合はNECルーター(Aterm)が搭載するDDNS更新機能でも問題ありません。
でも今回のようにスクリプトによる更新手段を持っておけば、将来的に「お名前.com」との連携のないルーターに買い換える時も安心ですね。

コメント

タイトルとURLをコピーしました