2016/11/03

Pythonやコマンドでドメイン(ホスト)名からIPアドレス(IPv4,IPv6)を得る

Pythonではsocketモジュールを用いることでTCP/IPの低レベルな処理を呼び出すことができるのだが、その中のsocket.getaddrinfo()を用いると、ドメイン名をDNSサーバに問い合わせて対応するIPアドレスを得る(名前解決を行う)ことができる。以前扱ったsocket.gethostbyname()はIPv4のみの対応だが、こちらはIPv6にも対応している。名前解決に失敗した場合は例外socket.gaierrorが発生する。

  1. Pythonの対話モード上でのテスト
  2. 名前解決処理におけるsocket.getaddrinfo()の詳細
  3. サンプルとその実行例
  4. 関連:コマンドでドメイン名の名前解決を行う

Pythonの対話モード上でのテスト

下はPython 3の対話モードでexample.comに対してsocket.getaddrinfo()を呼び出しているところ。本記事全体においてIPアドレス部分は伏せている。

(モジュールをインポート)
>>> import socket

(example.comに対してgetaddrinfo()を呼び出す)
>>> i = socket.getaddrinfo ('example.com', None)

(戻り値を確認)
>>> i
[(<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('xxxx:xxxx:xxx:x:xxx:xxxx:xxxx:xxxx', 0, 0, 0)), (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('xxxx:xxxx:xxx:x:xxx:xxxx:xxxx:xxxx', 0, 0, 0)), (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_RAW: 3>, 0, '', ('xxxx:xxxx:xxx:x:xxx:xxxx:xxxx:xxxx', 0, 0, 0)), (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('xx.xxx.xxx.xx', 0)), (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('xx.xxx.xxx.xx', 0)), (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_RAW: 3>, 0, '', ('xx.xxx.xxx.xx', 0))]

(存在しない名前を指定した場合)
>>> i = socket.getaddrinfo ('examplee.co', None)
Traceback (most recent call last):
  ...
socket.gaierror: [Errno -2] Name or service not known

名前解決処理におけるsocket.getaddrinfo()の詳細

  • socket.getaddrinfo()の2番目の引数は、名前解決のみを行うのであればNoneでよい
  • 戻り値は5要素のタプルが並んだリストの形となる
    • タプルの1番目の要素がAF_INET6ならIPv6アドレスでAF_INETならIPv4アドレスとなり、これらの定数はPython 3ではsocket.AddressFamilyの下にあるが、Python 2ではsocketのすぐ下にある
    • リスト内にはタプルの2番目の要素の値以外が同一の複数の項目が存在する
      • この要素の定数はPython 3ではsocket.SocketKindの下にあり、Python 2ではsocketのすぐ下にある
    • タプルの5番目の要素はタプル型となっており、この最初(0番)の要素がIPアドレスの文字列となっている
    • socket.getaddrinfo()の6番目の引数にsocket.AI_CANONNAMEを指定すると、指定したドメイン名にCNAME(別名)が存在する場合にリストの1番目の要素のタプルの4番目の要素にその名前が入り、存在しない場合は元の名前(1番目の引数に渡した文字列)が入る
      • リストの2番目以降の要素のタプルの4番目の要素は全て空文字列となる

サンプルとその実行例

下のコードは引数に指定したドメイン名(複数可)全てについて名前解決を試みて結果を表示する。別名が存在する場合にはその名前を表示した後で名前解決を行う。

[任意]ファイル名:nslookup.py
#! /usr/bin/python
# -*- coding: utf-8 -*-

# 引数に指定(複数可)した全ドメイン文字列に対してDNSによる名前解決を行う
# IPv4/IPv6両対応,CNAME対応

from __future__ import print_function

from socket import AI_CANONNAME, getaddrinfo, gaierror
try:
  from socket.AddressFamily import AF_INET6, AF_INET  # Python 3
  from socket.SocketKind import SOCK_STREAM
except ImportError:
  from socket import AF_INET6, AF_INET, SOCK_STREAM   # Python 2
import sys


def nslookup (hostlist):
  """
  名前解決を行う
  """
  for host in hostlist:
    try:
      # 別名の処理を行うためにAI_CANONNAMEを指定する
      addrinfolist = getaddrinfo (host, None, 0, 0, 0, AI_CANONNAME)
    except gaierror:
      print ('{0} -> *** NOT FOUND ***'.format (host))
      continue

    # getaddrinfo()ではタプルの *リスト* が返されるため
    # リストの各要素ごとにループしつつタプルの要素も展開する
    # IPアドレスの文字列はタプルsockaddrの最初(0番)の要素となる
    # このプログラムではprotoは使用していない
    for family, kind, proto, canonical, sockaddr in addrinfolist:
      # IPv4かIPv6かを判定して出力に含めることにする
      ipver = ''
      if family == AF_INET6:
        ipver = ' [IPv6]'
      elif family == AF_INET:
        ipver = ' [IPv4]'

      # kindの値ごとに重複した項目が存在し
      # また、canonicalは一番最初の項目以外は空文字列になるため
      # SOCK_STREAMの項目でのみ表示処理を行う
      if kind == SOCK_STREAM:
        if canonical == host or canonical == '':
          # 別名が設定されていない場合はIPアドレスを表示
          print ('{0} = {1}{2}'.format (host, sockaddr[0], ipver))
        else:
          # 別名が設定されている場合はそれを表示してから
          # 名前解決処理を行い、ループを抜ける
          print ('{0} -> {1}'.format (host, canonical))
          nslookup ([canonical,])
          break


if __name__ == '__main__':
  if len (sys.argv) < 2:
    sys.exit ('USAGE: {0} [HOSTNAME...]'.format (__file__))

  # 重複する引数があれば取り除く
  hosts = []
  for host in sys.argv[1:]:
    if not host in hosts:
      hosts.append (host)

  # 処理対象のリストを関数に渡す
  nslookup (hosts)
実行例
$ /path/to/nslookup.py example.com example.jp example.net example.org
example.com = xxxx:xxxx:xxx:x:xxx:xxxx:xxxx:xxxx [IPv6]
example.com = xx.xxx.xxx.xx [IPv4]
example.jp -> *** NOT FOUND ***
example.net = xxxx:xxxx:xxx:x:xxx:xxxx:xxxx:xxxx [IPv6]
example.net = xx.xxx.xxx.xx [IPv4]
example.org = xxxx:xxxx:xxx:x:xxx:xxxx:xxxx:xxxx [IPv6]
example.org = xx.xxx.xxx.xx [IPv4]

このコードは名前解決に成功したものも失敗したものも表示するが、これを改造することで名前解決に失敗したドメインか成功したドメインのいずれかのみを表示させることもできる。

関連:コマンドでドメイン名の名前解決を行う

コマンドで名前解決を行う場合、hostコマンドを用いるのが最も簡単で、IPv4とIPv6のアドレスの両方に対応しているドメインの場合、両方のアドレスが表示される。出力もシンプルで見やすい。

(example.comを試して成功)
$ host example.com
example.com has address xx.xxx.xxx.xx
example.com has IPv6 address xxxx:xxxx:xxx:x:xxx:xxxx:xxxx:xxxx

(example.jpを試して失敗)
$ host example.jp
Host example.jp not found: 3(NXDOMAIN)
$ echo ${?}
1

上のhostBINDのもので、Debian/Ubuntuでは “bind9-host” というパッケージとなっており、別の実装である “knot-host” という別のパッケージでは少しだけ出力が異なる。

BINDのツールであるnslookupdigといったコマンド(Debian/Ubuntuでは “dnsutils” というパッケージ)では “any” を問い合わせの種類に指定することで、IPv4とIPv6のアドレスの両方に対応しているドメインで両方のアドレスが表示され、 “AAAA” のところに表示されるのがIPv6のアドレスで “A” のところに表示されるのがIPv4のアドレスとなる。ただし他の種類の情報も表示されるため、IPアドレス部分が見つけづらい。

(nslookupコマンドを使用する場合)
$ nslookup -type=any example.com

...

example.com     has AAAA address xxxx:xxxx:xxx:x:xxx:xxxx:xxxx:xxxx
Name:   example.com
Address: xx.xxx.xxx.xx

(digコマンドを使用する場合)
$ dig example.com any

...

;; ANSWER SECTION:
example.com.            xxxxx   IN      AAAA    xxxx:xxxx:xxx:x:xxx:xxxx:xxxx:xxxx

...

example.com.            xxxxx   IN      A       xx.xxx.xxx.xx

なお、この “any” の部分を “aaaa” にして実行するとIPv6のアドレスのみが表示される。

使用したバージョン:
  • Python 2.7.12, 3.5.2
  • BIND 9.10.3-P4