お前、誰よ (その 2 )
っつったら DNS ですよね。今度はこれを Python でやってみようと思いました。で、どんな仕組みで動いてるか調べたら DNS クライアントを作ってみよう (3) もぉ Perl でしてはる方がいらっしゃいました。しかも超真面目に。
こちとらゆるふわなのでもちっと緩く、A レコードがとりあえず調べれればいい感じでいってみよう!
つわけで先ず RFC は http://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 じゃないけど。
これで名前引き放題だ、わー