pythonでネームサーバを指定してdigするためのライブラリ


# きっかけ

複数ゾーンの複数レコードをネームサーバごとに一致することをチェックするというタスクがあり、そこそこ数があったので 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
# 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

https://docs.python.org/ja/3/library/socket.html#socket.gethostbyname  

> socket.gethostbyname_ex(hostname)
ホスト名から、IPv4形式の各種アドレス情報を取得します。戻り値は (hostname, aliaslist,  ipaddrlist) のタプルで、 hostname は ip_address で指定したホストの正式名、 aliaslist は同じアドレスの別名のリスト(空の場合もある)、 ipaddrlist は同じホスト上の同一インターフェイスのIPv4アドレスのリスト(ほとんどの場合は単一のアドレスのみ) を示します。

### 使い方

```test1.py
#!/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レコードのルックアップを試したい。

```test.py
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/

### 使い方

```test.py
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

```nslookup.py
import dns.resolver, dns.exception
```
しており、内部的に dnspython をそのまま使ってるぽかった。

## pydig

https://github.com/leonsmith/pydig
https://pypi.org/project/pydig/

### 使い方

```test.py
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ムズい。