Kyoto Tech Talk #8

Kyoto Tech Talk #8

DNSのパケット雰囲気速習
この記事


誰?
ファイルのバイナリフォーマットや、通信のワイヤフォーマットなどがすき。
過去: /nota-techconf/15分で読めた気になるPNG
フォーマットの話をすると、RFCを読め! となってしまうんだけど、サマリーが書かれていると嬉しいことはあるので話しています。
エーアイにとってかわられるかもしれない……!
あなたのすきなフォーマット、プロトコルの解説をぜひしてください!
最近はRevolution Idleをやって虹色にビカビカ光る画面を眺め続ける生活。
#RubyKaigiNOC を2018 仙台ぐらいからずっとやっている。

広告

#RubyKaigiNOC
2023年から、セキュアなDNSの提供がなされている。
まあ私はこのへんの実装はやってないですが……
でもデバッグできるとよいな と関連情報を得るぐらいはしている。
(今年のIPv6-mostlyはまだぜんぜん理解していない)

セキュアなDNS?
アンセキュア
DNS(DNS over 53)(Do53)(レトロニム)
RFC1034(DNS概要)
RFC1035(フォーマット)
<query> をUDP/TCPの53番portでやりとり。 <anser> が帰ってくる。
後述するような内容のパケットがケーブルの中を通っているので、中間者には何を引いているかが丸わかり。
セキュア
DNS over TLS(DoT)
TCPの上でやりとりするものをTLSの上でやる。
TLSが暗号化してくれる。
DNS over HTTPS(DoH)
httpsで会話するが、 GET /dns-query?dns={Base64(<query>)} すると <anser> がレスポンスとして返る。
HTTPSのレイヤで暗号化。
HTTP2ならTCP、HTTP3ならUDPを通る。
DNS over QUIC(DoQ)
QUICのストリーム上でクエリ/レスポンスをやりとり。
QUICが暗号化。
ここでのセキュア とは
クエリの内容が通信経路にいる人に盗聴改竄されないこと。
盗聴下にあると、私が kmc-jp.slack.com にアクセスすると、通信経路にいる人にはこの文字列が読めるので、KMCに関係があることがバレる。
まあ接続先IPアドレスはわかったり、httpsで繋ぐ際のSNIでバレるのですが……。
後者は、HTTP/3で解消される? された?
後述のHTTPS RRがあれば最初からH3で繋ぎにいくことにはなれるので、例えばfastlyに繋いでることまでしかわからない みたいなことにはなるかもしれない?
DNSSECは?
たしかに改竄はされなくなるが(それだけでも意味は十分あるが)、盗聴には脆弱なまま。
権威で対応が必要なのもあり、リゾルバとクライアントが対応していれば有効になるセキュアなDNSのほうが普及している感じがする。

<query>? <answer>? (RFC1035)
どっちも同じく メッセージ という形式でやりとりがされる。
message
Copied!
+---------------------+
| ヘッダー部 |
+---------------------+
| 問い合わせ部 | ネームサーバーへの問い掛け
+---------------------+
| 回答部 | 問い掛けに回答するRR
+---------------------+
| 権威部 | 権威を指し示すRR
+---------------------+
| 付加情報部 | 付加的な情報を保持するRR
+---------------------+
ヘッダはどんなメッセージにもある。それ以外のパートの数はヘッダーの中に書かれている。

ヘッダー
header
Copied!
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
ID: 16bitの任意の値。queryに入っていた値がそのままanswerに入るので、どのqueryに対応する値なのかがわかる。
QR: queryなら0, answerなら1。
AA: 権威サーバからの応答なら1が立つ。
RD: 再帰問い合わせの要求。普通クライアントから送られる時には1、キャッシュサーバからさらに送られる際には0。
RA: 再帰問い合わせに答えるかどうか。普通権威サーバだと0で、キャッシュサーバだと1。
RCODE: response code. 正常に引けたかなどの情報が入っている。
QDCOUNT/ANCOUNT/NSCOUNT/ARCOUNT: それぞれクエリ、アンサ、権威サーバ、付加情報部のリソースレコードの数。
その他
Opcode: 0しか見たことない。
TC: UDPで送れるサイズを越えたりした時などにフォールバック必要なことを返す。
Z: 予約されていて常に0。

クエリ
問い合わせ部のフォーマットは以下のようになる。
query
Copied!
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ QNAME /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QTYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QCLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

NAME/ラベル クエリに使う名前
example.nna774.net のようなドメイン名を、ドットで区切ったもののそれぞれをラベルと呼ぶ。
example , nna774 , net の3つのラベルによってこれは構成されていて、この名前をバイナリに落とす時には、 <ラベルの長さ><ラベルの中身> の繰り返し(最後は長さ0のラベルで終わる)となる。
上記の例だと、 7 e x a m p l e 6 n n a 7 7 4 3 n e t 0 と埋め込まれる。
07 65 78 61 6d 70 6c 65 06 6e 6e 61 37 37 34 03 6e 65 74 00 (asciiの数字とラベル長は区別する必要があることに注意)

NAMEの圧縮
実はNAMEは圧縮のためにポインタが利用でき、長さのフィールドの上位2bitが1だと下位6bitとして既に出てきたところへのヘッダのIDフィールドからのoffsetで表す。
名前は0の長さのラベルで終端されるので、上記の 7 e x a m p l e 6 n n a 7 7 4 3 n e t 0 の例で、offsetとして 6 の位置を指すと、 nna774.net を表現できたりする。
逆に 4 h o g e の後ろに 6 の位置を指すようにポインタを置くと、 hoge.nna774.net を指したりできる。

QTYPE
Aレコードが引きたい時はAに対応する1。CNAMEだと5とか、それぞれのRRのtypeによって数字が定まってるのでそれを入れる。

QCLASS
これはIN(1)しか普通は見ない。
なんか他の値は過去に使われていたもの みたいな感じがする。

ここまでを合わせた例
nna774.net のAを引くためのqueryの内容は以下のようになる。
このトーク、詳細とか自分で知らなくてもこのようにWiresharkで見れるならよくない?(構造がわかるとおもしろいから!!!)
query
Copied!
0000 2b 1b 01 20 00 01 00 00 00 00 00 01 06 6e 6e 61 +.. .........nna
0010 37 37 34 03 6e 65 74 00 00 01 00 01 00 00 29 10 774.net.......).
0020 00 00 00 00 00 00 00 .......
これがそのまま53番ポートで運ばれていったり、base64されてhttpで投げられたりする。

リソースレコード(RR)
rr
Copied!
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ NAME /
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/ RDATA /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
このへんになると、表現はともかく書いてある内容はみなさんも見たことがありそう(だし、RDATAの構造はTYPEごとに変わる)なので、軽く流す。

A
Aレコードの場合のLENGTHは4で、データにはIPアドレスがそのまま4バイトなので入っている。
Aレコードをさっき引いた時の例( nna774.net A )のアンサーパケットの様子。
ヘッダのサイズは12byte固定なので、query sectionの頭は0x0cがオフセットなんですね(ポインタの c0 0c )。
anserには、query sectionにqueryの内容がそのまま入っており、それに対する回答がanswer sectionに入っている。また、便利な情報(権威はどこ とか、どうせ引くものとか)がそれぞれのセクションに入ってくることもある。

CNAME
typeは5
RDATAはNAMEがそのまま入る。
7 e x a m p l e 6 n n a 7 7 4 3 net 0 のように。
RDLENGTHはこの列の長さ。
CNAMEがanswerに含まれている例( bsky.nna774.net A を引いたところ、 CNAME を含んた返答が帰ってきている)
だがムズい図すぎる……。

HTTPS 自明でない例(RFC9460) しかし、時間がなさそう……。
最新のChromeなら、httpsなURLにアクセスする時にこのレコードを引いて、あれば優先して使う挙動に実はなっています。
typeは65
手元のmacのdigだと、HTTPSレコードは引けないので(なんで?)、TYPE番号で引いてみましょう。
dig
Copied!
nana@er ~ % dig TYPE65 google.com @1.1.1.1

; <<>> DiG 9.10.6 <<>> TYPE65 google.com @1.1.1.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53284
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;google.com. IN TYPE65

;; ANSWER SECTION:
google.com. 18841 IN TYPE65 \# 13 00010000010006026832026833

;; Query time: 8 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Wed May 28 19:42:59 JST 2025
;; MSG SIZE rcvd: 64
google.com. 18841 IN TYPE65 \# 13 00010000010006026832026833
長さ13を持つようで、このdigはdataの読み方を知らないのでそのままHEXで出してきています。
ここでこれを読もうと、RFC9460の2.2をおもむろに読むと、
a 2-octet field for SvcPriority as an integer in network byte order.
2バイトの優先度
the uncompressed, fully qualified TargetName, represented as a sequence of length-prefixed labels per Section 3.1 of RFC1035.
名前(ここで . (TOP Level)が来ると、優先度が0以外の時には自分自身を指す)
the SvcParams, consuming the remainder of the record (so smaller than 65535 octets and constrained by the RDATA and DNS message sizes).
残りはサービスパラムとかいうやつ。
RFC9460の14.3.2にリストがある。
配列を値に持つhashのような……

data
00010000010006026832026833
優先度1.SvcParams
params
00010006026832026833
alpnというkeyvalueの長さが6長さ2"h2"(h2が利用可能)長さ2"h3"

まとめると優先度が1、このホストに対してalpnのh2, h3が利用可能なことを示すレコードとなっています。

もちろん新しいdigならちゃんと中身を出してくれます。
dig
Copied!
[nona7@ringo(19:41)] ~ % dig https google.com

; <<>> DiG 9.18.24-1-Debian <<>> https google.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 55705
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;google.com. IN HTTPS

;; ANSWER SECTION:
google.com. 21600 IN HTTPS 1 . alpn="h2,h3"

;; Query time: 24 msec
;; SERVER: 192.168.220.53#53(192.168.220.53) (UDP)
;; WHEN: Wed May 28 19:41:57 JST 2025
;; MSG SIZE rcvd: 64

これを使うといきなりh3でgoolge.comにアクセスしていいことがブラウザがわかってそのようになっているんですね。
DDR+Do*+HTTPS record+h3で中間者に対して何も心配ない世界が来るのは……いつ?

まとめ
DNSのパケットの様子を駆け足で紹介。
詳細はRFC1035とかを……。
こんな感じで眺めてると読めるようになってくる……ならない?
なんとなくでも構造が頭に入っていると嬉しい気がする。
wiresharkで読むにしても、多分そう。
目で読んでいると、我々はパーサではないので1byte目が滑って意味不明になる。そしてパーサを書きたくなってくる……!
何故読むかというとたのしいから。
ふるまいを知っていればよく、中身を忘れていい というのは良い抽象化がされている ということだけど、何かあった時に中を覗けると嬉しいこともあるかもしれない。
あなたのすきなフォーマット、プロトコルの解説をぜひしてください!
「RFCを読めば書いてある」にしても、「じゃあ突然読むか!」とはあんまりならないので、人に押しつけられるランダムネスがほしい(?)。

この記事(再掲)

ref:
理解するために書いて、あとでいい感じにしよ と思ってあとでいい感じにしていないコード……。
バイナリパーサジェネレータみたいなのがほしくなってくる今日このごろ。

Powered by Helpfeel