お前、誰よ (その 2 )

っつったら DNS ですよね。今度はこれを Python でやってみようと思いました。で、どんな仕組みで動いてるか調べたら DNS クライアントを作ってみよう (3) もぉ Perl でしてはる方がいらっしゃいました。しかも超真面目に。

こちとらゆるふわなのでもちっと緩く、A レコードがとりあえず調べれればいい感じでいってみよう!

つわけで先ず RFChttp://www.ietf.org/rfc/rfc1035.txt だって上のページに書いてありました。UDP の使い方は先達が id:miho36:20101007 とか id:nullpobug:20080925 とか沢山いらっしゃいます。pack/unpack の引数はいつまでも覚えれなくて struct — Interpret bytes as packed binary data — Python 3.7.2 documentation 見ちゃうのね。覚えれるの? これ。

ちゅーわけで RFC 眺めながら request (スクリプトでは query としてしまってるけど) を作って投げて、response を適当に parse して返すようにしてみました。

# coding=utf8
import socket
import struct
import random

random.seed()

# Google 使っちゃうよ
DNSSERVER = '8.8.8.8'
PORT = 53


def resolve(fqdn):
  # DNS ゆーたら UDP でしょ
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  qdata = _query(fqdn)
  qid = qdata[:2]
  sock.sendto(qdata, (DNSSERVER, PORT))

  rdata = sock.recvfrom(1024)[0]
  # query の id をそのまま返してきてるはず
  rid = rdata[:2]
  # qr は response なら 1
  qr = ord(rdata[3]) >> 7
  # rcode は成功なら 0
  rcode = ord(rdata[4]) & 31

  # id 同じで response で成功なら parse する
  if (rid, qr, rcode) == (qid, 1, 0):
    return _parse(rdata)
  else:
    return None


def _query(fqdn):
  """fqdn の IP address を下さいという query を作る"""

  fqdn = fqdn.encode('utf-8')
  qid = struct.pack('>H', random.randint(0, 2**16-1))
  header = qid + b'\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00'

  # d.hatena.ne.jp -> \x01d\x06hatena\x02ne\x02jp\x00
  qname = b''.join([chr(len(x)) + x for x in fqdn.split(b'.')]) + b'\x00'
  # 1 が "a host address"
  qtype = struct.pack('>H', 1)
  # 1 が "the Internet"
  qclass = struct.pack('>H', 1)

  return header + qname + qtype + qclass


def _parse(data):
  """DNS response をパースして list of tuples で返す

  data -> [(name, type, class ttl, response_data)]
  """
  result = []
  i = 12 # これ hard coding でいいの?

  # response には query が含まれてるのね
  qname, i = _pick_name(data, i)
  qtype = struct.unpack('>H', data[i:i+2])[0]
  qclass = struct.unpack('>H', data[i+2:i+4])[0]
  i += 4

  while True:
    # response data を喰いつくす、全部読んだら終わり
    if len(data) <= i:
      break

    rec_name, i = _pick_name(data, i)
    rec_type = struct.unpack('>H', data[i:i+2])[0]
    rec_class = struct.unpack('>H', data[i+2:i+4])[0]
    rec_ttl = struct.unpack('>I', data[i+4:i+8])[0]
    rec_dlength = struct.unpack('>H', data[i+8:i+10])[0]

    if rec_type == 1:
      # A レコードだったら
      rec_data = '.'.join([str(ord(x)) for x in data[i+10:i+14]])
      i += 14
    elif rec_type == 5:
      # CNAME レコードだったら
      rec_data, i = _pick_name(data, i+10)
    else:
      # それ以外だったら parse しないで bytes として
      rec_data = data[i+10:i+10+rec_dlength]
      i = i+10+rec_dlength
    result.append((rec_name, rec_type, rec_class, rec_ttl, rec_data))

  return result


def _pick_name(data, num):
  """data の num bytes 目から始まる名前を拾う"""

  name_list = []
  while True:
    length = ord(data[num])
    num += 1
    if length == 0:
      break
    elif length >= 192:
      # length (2 octets) の頭が 11 だと compressed なので別処理
      name_length = struct.unpack('>H', data[num-1:num+1])[0] % (3 << 14)
      name, _ = _pick_name(data, name_length)
      name_list.append(name)
      num += 1
      break
    else:
      name_list.append(data[num:num+length])
      num += length
  return b'.'.join(name_list), num


if __name__ == '__main__':
  import sys

  print(resolve(sys.argv[1]))

で、ちょっと試してみますよ。

$ python resolve.py google.com
[('google.com', 1, 1, 300, '66.249.89.99'), ('google.com', 1, 1, 300, '66.249.89.104')]
$ python my.py www.google.com
[('www.google.com', 5, 1, 83203, 'www.l.google.com'), ('www.l.google.com', 1, 1, 300, '66.249.89.99'), ('www.l.google.com', 1, 1, 300, '66.249.89.104')]
$ python my.py mail.google.com
[('mail.google.com', 5, 1, 86399, 'googlemail.l.google.com'), ('googlemail.l.google.com', 1, 1, 299, '72.14.203.19'), ('googlemail.l.google.com', 1, 1, 299, '72.14.203.18'), ('googlemail.l.google.com', 1, 1, 299, '72.14.203.83'), ('googlemail.l.google.com', 1, 1, 299, '72.14.203.17')]
$ python my.py gmail.com
[('gmail.com', 1, 1, 300, '72.14.203.18'), ('gmail.com', 1, 1, 300, '72.14.203.83'), ('gmail.com', 1, 1, 300, '72.14.203.19'), ('gmail.com', 1, 1, 300, '72.14.203.17')]

うん、まぁ、動いてるっぽいぞ。あんま pretty じゃないけど。

これで名前引き放題だ、わー