一覧NW-13-3 · DNSの名前解決

DNSの否定応答とTCPへの切り替え

空の回答と打切りを区別し、完全なメッセージを取り出します。

説明

DNSの回答が空でも、その理由は一つではありません。名前がない場合、名前はあっても指定した型がない場合、UDPの応答が打ち切られた場合では、次に行う処理が違います。

NXDOMAINとNODATAをPCAPとヘッダー値で比較し、SOAから否定回答のTTLを計算します。続いてTC付きの応答を確認し、TCPの長さプレフィックスから完全なDNSメッセージを取り出します。

1名前の不在と型の不在

この試料のゾーンではwww.example.testにAだけがあり、absent.example.testという名前はありません。二つの応答を読みます。

tcpdump -n -tt -vvv質問名・型・応答コード -r negative.pcap二種類の否定応答

absentのAはNXDomain、wwwのAAAAはNoErrorでAnswer0です。後者は名前があっても指定型のRRsetがないNODATAです。Answerが空という一点だけでは、名前の不在と型の不在を同じに扱えません。

2ヘッダーのRCODEと件数

二つのDNSメッセージのflags最下位4ビットと各欄の件数を復号します。

python3 -cRCODEと各欄の個数 'from pathlib import Path; import struct; rows=[struct.unpack("!6H",Path(n).read_bytes()[:12]) for n in ("nxdomain.bin","nodata.bin")]; print([(v[1]&15,v[2:]) for v in rows])'名前不在と型不在の差

RCODE3と0で、どちらも質問1、回答0、Authority1、Additional0です。AuthorityのSOAは否定回答の範囲とキャッシュ期限を扱うために使われ、空の回答を通常のAレコードとして保存するわけではありません。

3否定回答のキャッシュ期限

ゾーンに設定したSOA TTLは300秒、MINIMUMは120秒です。応答中のSOAは小さい方の120秒に調整されています。その値を計算します。

python3 -cSOAから否定TTLを計算 'soa_ttl=300; minimum=120; negative_ttl=min(soa_ttl,minimum); print(negative_ttl); print([(elapsed,max(0,negative_ttl-elapsed)) for elapsed in (30,121)])'小さいTTLと残り時間

否定回答は120秒を上限として保存し、30秒後は残り90秒です。権威側にレコードを追加しても、以前の否定回答がキャッシュに残っている間は、利用者ごとに見える結果が違う場合があります。

4UDP応答が打ち切られた場合

bulk.example.testのTXTは大きな回答になるため、この記録ではUDP応答にTCが付きました。

tcpdump -n -tt -vvv打切りフラグを確認 -r truncated.pcapTC付きの応答

このtcpdumpのID直後の縦線はTCフラグで、応答が打ち切られたことを示します。NoErrorでもこの回答だけで全データを取得したとは扱えず、TCP等で完全な回答を取得します。UDPの大きさはEDNSの利用等で変わるため、常に512バイト固定とは考えません。

5TCPの先頭2バイトはDNSの長さ

tcp-response.binはTCPバイト列に載せる、長さプレフィックス付きの完全なDNS応答です。

python3 -c長さプレフィックスとDNSヘッダー 'from pathlib import Path; import struct; b=Path("tcp-response.bin").read_bytes(); size=struct.unpack("!H",b[:2])[0]; values=struct.unpack("!6H",b[2:14]); print(size,len(b)-2,size==len(b)-2); print(hex(values[0]),bool(values[1]&0x200),values[3])'完全な回答の長さと件数

先頭2バイトはDNSメッセージの長さで、自分自身の2バイトを含みません。ID0x1234、TCなし、回答5件です。TCPを使うこととTLSで暗号化することは別で、この長さの扱いはDNS over TCPの形式です。

6TCPの読出し一回で全体が来るとは限らない

最初の読出しで300バイトしか得られなかった場合、長さから残りを求めます。

python3 -c分割された読出しを復元 'from pathlib import Path; b=Path("tcp-response.bin").read_bytes(); first=b[:300]; size=int.from_bytes(first[:2],"big"); needed=2+size-len(first); print(len(first),needed); print(first+b[300:]==b)'必要な残りバイト数

長さを満たすまで残りを読み、連結すれば元のバイト列に一致します。一回のrecvと一つのDNS応答が一致する保証はないため、先頭2バイトの長さでメッセージを切り出します。名前が不在なのか、回答が途中なのか、転送中なのかを分けて確認します。

まとめ

RCODE、質問名と型、SOAを合わせて否定回答を解釈します。

TC付き応答を完全な回答として扱わず、TCPでは2バイトの長さを使ってメッセージを切り出します。

操作履歴と解説を振り返れます。

シェル / PCAPと通信の記録

1
tcpdump -n -tt -vvv -r negative.pcap
reading from file negative.pcap, link-type EN10MB (Ethernet)
0.000000 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto UDP (17), length 151)
    192.0.2.53.53 > 192.0.2.10.53000: [udp sum ok] 4660 NXDomain* q: A? absent.example.test. 0/1/0 ns: example.test. [2m] SOA ns.example.test. hostmaster.example.test. 2026110901 3600 600 86400 120 (123)
0.010000 IP (tos 0x0, ttl 64, id 1, offset 0, flags [DF], proto UDP (17), length 148)
    192.0.2.53.53 > 192.0.2.10.53000: [udp sum ok] 4660* q: AAAA? www.example.test. 0/1/0 ns: example.test. [2m] SOA ns.example.test. hostmaster.example.test. 2026110901 3600 600 86400 120 (120)
2
python3 -c 'from pathlib import Path; import struct; rows=[struct.unpack("!6H",Path(n).read_bytes()[:12]) for n in ("nxdomain.bin","nodata.bin")]; print([(v[1]&15,v[2:]) for v in rows])'
[(3, (1, 0, 1, 0)), (0, (1, 0, 1, 0))]
3
python3 -c 'soa_ttl=300; minimum=120; negative_ttl=min(soa_ttl,minimum); print(negative_ttl); print([(elapsed,max(0,negative_ttl-elapsed)) for elapsed in (30,121)])'
120
[(30, 90), (121, 0)]
4
tcpdump -n -tt -vvv -r truncated.pcap
reading from file truncated.pcap, link-type EN10MB (Ethernet)
0.000000 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto UDP (17), length 63)
    192.0.2.53.53 > 192.0.2.10.53000: [udp sum ok] 4660| q: TXT? bulk.example.test. 0/0/0 (35)
5
python3 -c 'from pathlib import Path; import struct; b=Path("tcp-response.bin").read_bytes(); size=struct.unpack("!H",b[:2])[0]; values=struct.unpack("!6H",b[2:14]); print(size,len(b)-2,size==len(b)-2); print(hex(values[0]),bool(values[1]&0x200),values[3])'
1185 1185 True
0x1234 False 5
6
python3 -c 'from pathlib import Path; b=Path("tcp-response.bin").read_bytes(); first=b[:300]; size=int.from_bytes(first[:2],"big"); needed=2+size-len(first); print(len(first),needed); print(first+b[300:]==b)'
300 887
True

演習完了