きっかけ
複数ゾーンの複数レコードをネームサーバごとに一致することをチェックするというタスクがあり、そこそこ数があったので python でスクリプトにした。
その時に、python で NS指定 で dig するライブラリって何かいいのがないかなと思って調べたら、202006上旬現在時点で有用そうな情報がまとまったページが見当たらなかったのでここに書き残しておこうと思った。
python なのは、単純に自分が python を勉強してるだけなので簡単にやりたかったらシェルスクリプトで書いたほうが早かったと思う。
環境
この記事を書く上での動作確認は、
Windows10/WSL/Ubuntu-18.04 上にインストールした
# pyenv --version
pyenv 1.2.17-2-ga8ca63fc
# pipenv --version
pipenv, version 2018.11.26
# python --version
Python 3.8.2
で確認している。
上記とは別に、CentOS8上の同バージョン pyenv/pipenv/python でも実施している。
digコマンドで確認する場合
試したかったことのサンプル。
# dig blog.watarinohibi.tokyo @8.8.8.8
;; ANSWER SECTION:
blog.watarinohibi.tokyo. 19 IN A 157.230.43.191
# dig blog.watarinohibi.tokyo @dns1.p06.nsone.net
;; ANSWER SECTION:
blog.watarinohibi.tokyo. 20 IN A 157.230.45.115
dns1.p06.nsone.net は Netlify のNSサーバ
検索してるURLはNetlifyにデプロイしてる自分のブログサイト
上記は例で、実際は特定の1レコードが返ってくるAレコード、SOAレコード、TXTレコード、MXレコード等に対して実施した。
使えそうなライブラリ
- socket
- python-dns
- pydig
- pynslookup
socket
socket.gethostbyname_ex(hostname)
ホスト名から、IPv4形式の各種アドレス情報を取得します。戻り値は (hostname, aliaslist, ipaddrlist) のタプルで、 hostname は ip_address で指定したホストの正式名、 aliaslist は同じアドレスの別名のリスト(空の場合もある)、 ipaddrlist は同じホスト上の同一インターフェイスのIPv4アドレスのリスト(ほとんどの場合は単一のアドレスのみ) を示します。
使い方
#!/usr/bin/env python3
import socket
domain = 'blog.watarinohibi.tokyo'
ns1 = '8.8.8.8' # google DNS
ns2 = 'dns1.p06.nsone.net' # Netlify DNS
# socket
print ("=== socket.getaddinfo")
addrs = socket.getaddrinfo(domain, 80)
for addr in addrs:
print(addr)
print ("=== socket.gethostbyname")
addrs = socket.gethostbyname(domain)
print (addrs)
# ./test.py
=== socket.getaddinfo
(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('157.230.45.115', 80))
(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('157.230.45.115', 80))
(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_RAW: 3>, 0, '', ('157.230.45.115', 80))
(<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('2400:6180:0:d1::575:a001', 80, 0, 0))
(<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('2400:6180:0:d1::575:a001', 80, 0, 0))
(<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_RAW: 3>, 0, '', ('2400:6180:0:d1::575:a001', 80, 0, 0))
=== socket.gethostbyname
157.230.45.115
NSを指定するオプションが(たぶん、見落としてなければ)ない。
今回の用途には不適だが、標準ライブラリで名前解決を試したいときはこれで十分そう。
dnspython
http://www.dnspython.org/
https://github.com/rthalley/dnspython
https://pypi.org/project/dnspython/
使い方
http://www.dnspython.org/examples.html
あたりを参考に。今回は、NSを指定してのAレコードのルックアップを試したい。
import dns.resolver
print ("=== dns.resolver @", ns1)
resolver = dns.resolver.Resolver()
resolver.nameservers = [ns1]
answers = resolver.query(domain, 'A')
for rdata in answers:
print (rdata)
print ("=== dns.resolver @", ns2)
resolver = dns.resolver.Resolver()
resolver.nameservers = [ns2]
answers = resolver.query(domain, 'A')
for rdata in answers:
print (rdata)
# ./test.py
=== dns.resolver @ 8.8.8.8
134.209.106.40
=== dns.resolver @ dns1.p06.nsone.net
Traceback (most recent call last):
~~~ stack trace
という感じで、IPv4アドレスでのNS指定では動いたが、NetlifyDNSなどのCNAMEレコードでのNS指定ではエラーになってしまった。
https://github.com/rthalley/dnspython/issues/235
にこのエラーと思わしきやりとりがあるが、RFCにないので対応してないみたい。
とはいえ、NSレコードって全然CNAMEで使うものなイメージがあるので、ここらへんはよくわからない。。
pynslookup
https://github.com/wesinator/pynslookup
https://pypi.org/project/nslookup/
使い方
from nslookup import Nslookup
print ("=== nslookup @", ns1)
dns_query = Nslookup(dns_servers=[ns1])
ips_record = dns_query.dns_lookup(domain)
print(ips_record.response_full, ips_record.answer)
print ("=== nslookup @", ns2)
dns_query = Nslookup(dns_servers=[ns2])
ips_record = dns_query.dns_lookup(domain)
print(ips_record.response_full, ips_record.answer)
# ./test.py
=== nslookup @ 8.8.8.8
['blog.watarinohibi.tokyo. 19 IN A 178.128.17.49'] ['178.128.17.49']
=== nslookup @ dns1.p06.nsone.net
Traceback (most recent call last):
~~~ stack trace
というわけでこちらもエラー。
コードを確認したところ、
https://github.com/wesinator/pynslookup/blob/master/nslookup/nslookup.py
import dns.resolver, dns.exception
しており、内部的に dnspython をそのまま使ってるぽかった。
pydig
https://github.com/leonsmith/pydig
https://pypi.org/project/pydig/
使い方
import pydig
print ("=== nslookup @", ns1)
resolver = pydig.Resolver(
nameservers = [ns1]
)
answer = resolver.query(domain, 'A')
print (answer)
print ("=== nslookup @", ns2)
resolver = pydig.Resolver(
nameservers = [ns2]
)
answer = resolver.query(domain, 'A')
print (answer)
# ./test.py
=== nslookup @ 8.8.8.8
['157.230.35.153']
=== nslookup @ dns1.p06.nsone.net
['206.189.89.118']
お、動いた。こいつはCNAMEのNS指定でも動きそう。
コードを確認すると、
https://github.com/leonsmith/pydig/blob/master/pydig/resolver.py
import subprocess
--
Builds up the final arguments to pass into subprocess
と書いてあり、 subprocess でローカルの dig が動いてるだけだった。なるほど。
digのパスが違う、指定したい場合は、オプションで指定できる。
>>> resolver = pydig.Resolver(
... executable='/usr/bin/dig',
... nameservers=[
... '1.1.1.1',
... '1.0.0.1',
... ],
... additional_args=[
... '+time=10',
... ]
... )
結論
Python で NS を指定しての dig を使いたい場合は、digコマンドのラッパーである pydig を使えばよさそうだけど、単なるラッパーでしかないようなのでこれだったら自分で subprocess で書いてしまってもよいかもしれない。
DNSムズい。