DNSクライアントのシェルスクリプト実装

bind-utils由来の名前解決コマンド(dig, host, nslookupなど)は、エラー時でも終了ステータスを0で返すことがあったり、エラーを標準出力に吐いてきたり、回答にデータ以外の修飾語を付加してきたり、スクリプト内で使うには少し苛立ちを感じるときがあります。そんなわけで、文句があるなら自分でやればいいだろ!の煽りを真に受け、DNSクライアントをシェルスクリプト(bash)で実装してみました。
2025-12-25  

1. はじめに
2. 実証環境
3. DNS QUERY
4. DNS RESPONSE
5. 逆引き
6. シェルスクリプトの制限
7. 実践例
8. まとめ


1. はじめに

DNSはプロトコルとしてはステイトレスであり、HTTP同様、「行って来い」でセッションが完了します(TCP/53やEDNS0、再送制御などを除く)。その分、簡単なのですが、HTTPとは異なり、バイナリによるセッションであるため人間には少々わかりにくいものとなっています。逆に人間にはわかりにくい分、簡潔にコードが書けるという側面もあります。そこで、本稿ではDNSクライアントをシェルスクリプト(bash)で実装してみます。実装は基本的な機能のみ、Aレコード(IPv4)の正引き、逆引きとします。実装の概要としては、まず、bashでUDPソケットを開き、そこにDNS QUERYを書き込み、そこからDNS RESPONSEを受け取り、そのDNS RESPONSEを解析する(バイナリを人間の読める形にする)という流れになります。

次項で本稿の実証環境について述べ、次にDNS QUERYの生成および送信、続いてDNS RESPONSEの受信およびバイナリの解析について考察し、さらに別項で逆引きについて言及します。最後に今回の実装におけるシェルスクリプト特有の問題やエラー処理について取り上げ、実践例を示した後、まとめとなります。

DNSの基本仕様については RFC 1035を参照してください。メッセージフォーマットについては Section 4、DNS メッセージ圧縮(圧縮ポインタ)については Section 4.1.4で規定されています。なお、本稿では RFC 6891(EDNS0)には対応していません。

本稿ではバイナリ(16進数)の表示として、一般的な0x00表示やシェルのエスケープ\x00、および、odにテキスト化された素の00などが混在しています。本稿ではこれらはすべて同じ16進数として扱って下さい。


2. 実証環境

実証環境は、Fedora 42 bash-5.2.26(1)-release (x86_64-redhat-linux-gnu) (GNU coreutils) 9.6、および、Gentoo Linux bash-5.3.3(1)-release (x86_64-pc-linux-gnu) (GNU coreutils) 9.7 となっています。本稿で特に言及がなければ「シェルスクリプト」と称した場合は、bashのシェルスクリプトを意味します。当サイトのシェルスクリプトは慣習として、概ね、bashのビルトインコマンドとcoreutils収録のコマンドのみによって書かれていますが、そのような環境制限を推奨または誇示するものではありません。


3. DNS QUERY

DNS QUERYは単純に言えば先頭12バイトのヘッダーに問い合わせ対象のドメイン名を付加したものです。12バイトの内訳は、先頭2バイトが任意のトランザクションID、次の2バイトはflagsフィールドで、問い合わせの概要が設定されており、ここでは、RD(Recursion Desired)ビットを立てた標準的な再帰問い合わせになっています。次の2バイトは問い合わせの数で今回の実装では常に一つの問い合わせしかしないので1になっています。残りの6バイト(3項目、各2バイト)は、Answer(回答の数)/Authority(権威サーバー情報の数)/Additional(追加情報の数)でこれらは応答時にのみ意味を持ち、DNS QUERYでは全て0です。
QUERY="\x12\x34"              # transaction ID (任意の2バイト、例:0x1234)
QUERY=$QUERY"\x01\x00"        # flags
QUERY=$QUERY"\x00\x01"        # query = 1 (問い合わせは一つ)
QUERY=$QUERY"\x00\x00"        # answer = 0
QUERY=$QUERY"\x00\x00"        # Authority record = 0
QUERY=$QUERY"\x00\x00"        # additional RR = 0
つまり、本稿の実装では先頭12バイトは上記で固定でかまわないということになります。

次に問い合わせ対象のドメイン名をラベル化します。対象のドメイン名が、rita.karing.jp(www.karing.jpのAレコード)だとすると以下のようになります。
\x04rita\x06karing\x02jp\x00
単純に各ラベルの文字数を(ドットがある場合はドットを省略して)ラベルの先頭に付加し、\x00でドメイン名が終端に達したことを示します。最後にドメイン名に続けて問い合わせのタイプを2バイトで、問い合わせのclassを2バイトで表し、DNS QUERYが完成します。問い合わせのタイプは、Aレコードの正引きは\x00\x01で、逆引きは\x00\x0cです。問い合わせのclassというのは事実上ほぼインターネット(IN)しか使われておらず、\x00\x01です。この例題で完成した正引きのDNS QUERYは以下のようになります。
QUERY="\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x04rita\x06karing\x02jp\x00\x00\x00\x01\x00\x01"

このQUERYをbashで開いたUDPソケットに流し込み、そのUDPソケットから返信を受け取ります。ここではDNSサーバーは8.8.8.8(dns.google.)にしていますが、各自の環境に合わせて下さい。なお、DNSはバイナリによるセッションになりますが、基本的にbashはバイナリを扱うことを想定されておらず、\x00を扱えない(0x00を文字列の終端と認識するため0x00を含むバイト列を変数に正しく代入できない)ため、バイナリデータを適切に扱えません。本稿では、バイナリデータをodでテキスト化することでこの問題を回避しています。また、この項でのechoはbashのビルトインコマンドではなく、coreutilsのechoを使っているのもこの制限に関するもので詳細は第6項で言及します。
#!/bin/bash
QUERY="\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x04rita\x06karing\x02jp\x00\x00\x00\x01\x00\x01"
exec 3<>/dev/udp/8.8.8.8/53           # bashでUDPソケットを読み書き用に開く。
/usr/bin/echo -ne $QUERY >&3          # $QUERYをDNSサーバーに送る。
timeout 1s cat <&3 |od -t x1 -A n     # DNSサーバから応答を受け取る。
exit 0
上述のスクリプトを実行するとDNSサーバーから下記のようなバイト列が返信されてきます。
12 34 81 80 00 01 00 01 00 00 00 00 04 72 69 74 61 06 6b 61 72 69 6e 67 02 6a 70 00 00 01 00 01 c0 0c 00 01 00 01 00 00 00 3f 00 04 dc 93 d0 9d

このバイト列が取得できれば、本項 DNS QUERYの目的は達成です。あとはこのバイト列を解読する作業になります。


4. DNS RESPONSE

DNS RESPONSEは、DNS QUERYよりかなり面倒です。最大の理由は、回答の形式が不特定であることで、その時々の条件に合わせて解析する必要があることです。具体例としては、問い合わせるドメイン名がCNAMEかAレコードかで回答の形式というか回答の数が変わってきます。

まずは、バイト列を受け取る方法を考察します。前項では、以下のようにcatで受け取り、timeoutで終了し、受け取ったデータを直接odに処理させてテキスト化するという流れでした。
/usr/bin/echo -ne $QUERY >&3                 # $QUERYをDNSサーバーに送る。
RESPONSE=`timeout 1s cat <&3 |od -t x1 -A n` # 応答を受け取り、バイナリデータをテキスト化。
シェルスクリプト的には入力を受け取るコマンドと言えば、第一にreadが考えられますが、readでは上手くいきません。理由は、前述のようにシェルスクリプトでは、0x00を変数に格納できず、バイト列を正しく変数に代入できないためです。その制限を回避するため本稿では、変数に代入する前にodを通してバイナリデータをテキスト化しています。また、通信の終了を固定時間で判断しているのもシェルスクリプトによる制限です。

次に回答のフォーマットについてですが、まず、ヘッダーがあり、その後に回答が続きます。一つの回答は、当該のドメイン名とその回答という形式になっていて、それが複数続くこともあります。逆引きの場合もほぼ同様で、IPアドレスが特殊なドメイン名として扱われるだけです。逆引きの詳細については次項で扱います。

ヘッダーの構成は、先頭2バイトがDNS QUERYで送られてきたtransaction ID、次の2バイトはどんな問い合わせなのかを示すflagsと問い合わせに対する成否を示す下位4ビットのステイタスコード(RCODE)を合わせたもので、次の2バイトがDNS QUERYで送られてきた質問の数、次の2バイトが回答の数になり、最後の4バイトは補助的なデータで今回の実装では全て無視します(全て0で対応)。
12 34                    # transaction ID (任意の2バイト、例:0x1234)
81 80                    # flags+RCODE
00 01                    # query = 1 (問い合わせは一つ)
00 01                    # answer = 1 (回答は一つ)
00 00                    # Authority record = 0
00 00                    # additional RR = 0
このヘッダーの後に問い合わせのドメイン名が続きます。これはDNS QUERYで送られてきたものと同じです。
04 72 69 74 61           # 04(4文字)+rita
06 6b 61 72 69 6e 67     # 06(6文字)+karing
02 6a 70 00              # 02(2文字)+jp+00(ドメイン名の終端を示すヌル)
00 01                    # 問い合わせのタイプは、IPv4の正引き = 1
00 01                    # 問い合わせのクラスはIN(インターネット) = 1
この後に回答が付加されます。回答は、まず、質問の主体であるドメイン名が示され、それに回答が付加されます。回答は、まず、最初にドメイン名なのですが、ここでは"c0 0c"となっています。"c(上位2ビットが立つ)"は圧縮ポインタであることを示していて、2バイト中残り14ビットがオフセット値を表しています。つまり、"c0 0c"は、"(0xc00c&0x3fff)=0xc(先頭を0として12番目)"に記述されているドメイン名を表しています。なので、この例では"04 72 69 74 61 06 6b 61 72 69 6e 67 02 6a 70 00"=rita.karing.jpです。
c0 0c                    # 圧縮ポインタが0xc(先頭0から12番目)から始まるドメイン名を示す
00 01                    # 問い合わせのタイプは、IPv4の正引き = 1
00 01                    # 問い合わせのクラスはIN(インターネット) = 1
00 00 00 3f              # TTL(秒)
00 04                    # 回答の長さ、4バイト(dc 93 d0 9d)
dc 93 d0 9d              # dc=220 93=147 d0=208 9d=157
もっとも単純なDNS RESPONSEの解読はこのようになりますが、ドメイン名を圧縮ポインタで示してくるのが実装では意外に厄介です。圧縮ポインタによる省略はドメイン名の一部参照にも使われ、ラベルを解析している途中から圧縮ポインタでの参照に移行したり、また、仕様上、圧縮ポインタで圧縮ポインタを参照することも許容されているので厳密に実装するとなるとループを避けるような仕組みも必要でなかなか大変です。

・ドメイン名がCNAMEの場合
ここまでは、一つのAレコード(ドメイン名)に対する問い合わせに一つのIPv4アドレスが返答されるというパターンでしたが、これをCNAMEでIPアドレスを問い合わせると少し事情が変わってきます。CNAMEで問い合わせた場合、最低2つの回答が返ってきます。一つは問い合わせに使われたCNAMEと対応するAレコードで、もう一つがAレコードと本来の問い合わせ目的であるIPv4アドレスです。www.karing.jp(CNAME)のIPv4アドレスをDNSに問い合わせると以下のようになります。
$ host www.karing.jp
www.karing.jp is an alias for rita.karing.jp.
rita.karing.jp has address 220.147.208.157
バイナリで示すと以下のようになります。
QUERY=\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06karing\x02jp\x00\x00\x01\x00\x01
上記のQUERYは、問い合わせのドメイン名がAレコード(rita.karing.jp)からCNAME(www.karing.jp)に変わっただけですが、RESPONSEでは、回答数が二つになっています。つまり、hostで返された「www.karing.jp is an alias for rita.karing.jp.」と「rita.karing.jp has address 220.147.208.157」で二つの回答ということになります。

12 34 85 80 00 01 00 02 00 00 00 00 03 77 77 77 06 6b 61 72 69 6e 67 02 6a 70 00 00 01 00 01 c0 0c 00 05 00 01 00 03 f4 80 00 07 04 72 69 74 61 c0 10 c0 2b 00 01 00 01 00 00 01 12 00 04 dc 93 d0 9d
 12 34             # transaction ID (任意の2バイト、例:0x1234)
 85 80             # flags+rcode
 00 01             # query = 1 (問い合わせは一つ)
 00 02             # answer = 2 (回答は二つ)
 00 00             # Authority record = 0
 00 00             # additional RR = 0
 
 03 77 77 77 06 6b 61 72 69 6e 67 02 6a 70 00 # 問い合わせのドメイン名 www.karing.jp
 00 01             # 問い合わせのタイプは、IPv4の正引き = 1
 00 01             # 問い合わせのクラスはIN(インターネット) = 1

 回答1 CNAMEの解析 www.karing.jp(CNAME)は、rita.karing.jp(Aレコード)に対応
 c0 0c             # 圧縮ポインタが0xc(先頭0から12番目)から始まるドメイン名(www.karing.jp)を示す
 00 05             # 回答のタイプは、別名の解析 = 5
 00 01             # 回答のクラスはIN(インターネット) = 1
 00 03 f4 80       # TTL(秒)
 00 07             # 回答の長さ、7バイト(04 72 69 74 61 c0 10、途中からc0ポインタ)
 04 72 69 74 61    # rita
 c0 10             # 圧縮ポインタが0x10(先頭0から16番目)から始まるドメイン名(karing.jp)を示す

 回答2 IPv4正引き rita.karing.jpのIPv4アドレスは、220.147.208.157
 c0 2b             # 圧縮ポインタが0x2b(先頭0から43番目)から始まるドメイン名(rita.karing.jp)を示す
 00 01             # 回答のタイプは、IPv4の正引き = 1
 00 01             # 回答のクラスはIN(インターネット) = 1
 00 00 01 12       # TTL(秒)
 00 04             # 回答の長さ、4バイト(dc 93 d0 9d)
 dc 93 d0 9d       # dc=220 93=147 d0=208 9d=157
このように二つの回答があるのは、CNAMEの問い合わせだけではなく、ドメインがIPアドレスを複数持つ場合も回答が複数になります。CNAMEの解析はクライアントが直接求めた回答ではないので省略してもいいような気もするというか、むしろ省略するべきくらいに思わないこともないですが、複数のIPアドレスがある場合はどれもクライアントが求めたIPアドレスという回答なので省略すべきでなく、結局、複数の回答はすべて解析するという方針に落ち着きます。


5. 逆引き

逆引きは、基本的にIPアドレスをバイト毎(255毎)に反転させ、最後にin-addr.arpaをつけて、それをドメイン名の代わりにするだけです。
220.147.208.157 → 157.208.147.220.in-addr.arpa
これが、DNS QUERY内でラベル化されると以下のようになります。
03 31 35 37 03 32 30 38 03 31 34 37 03 32 32 30 07 69 6e 2d 61 64 64 72 04 61 72 70 61 00

Aレコードの正引きの回答では、IPv4アドレスは4バイトの数値として返ってきましたが、逆引きの問い合わせの数値部分は数値ではなく、in-addr.arpaというTLDを持つ特殊なドメイン名として扱います。つまり、DNS QUERYでは220が\xdcにはならず、そのままテキストの220(\x32\x32\x30)となります。また、最後の4バイトの前2バイト部分の\x00\x0cが逆引きの問い合わせであることを示しています。
QUERY=\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03157\x03208\x03147\x03220\x07\x69\x6e\x2d\x61\x64\x64\x72\x04\x61\x72\x70\x61\x00\x00\x0c\x00\x01

上記のDNS RESPONSEは下記となり、
12 34 81 80 00 01 00 01 00 00 00 00 03 31 35 37 03 32 30 38 03 31 34 37 03 32 32 30 07 69 6e 2d 61 64 64 72 04 61 72 70 61 00 00 0c 00 01 c0 0c 00 0c 00 01 00 00 0e 10 00 25 0f 32 32 30 2d 31 34 37 2d 32 30 38 2d 31 35 37 05 74 6f 6b 79 6f 02 61 70 07 67 6d 6f 2d 69 73 70 02 6a 70 00
12 34            # transaction ID (任意の2バイト、例:0x1234)
81 80            # flags+rcode
00 01            # query = 1 (問い合わせは一つ)
00 01            # answer = 2 (回答は二つ)
00 00            # Authority record = 0
00 00            # additional RR = 0
03 31 35 37 03 32 30 38 03 31 34 37 03 32 32 30 07 69 6e 2d 61 64 64 72 04 61 72 70 61 00 # 問い合わせのドメイン名
00 0c            # 問い合わせのタイプは、IPv4の逆引き = 12(0x0c)
00 01            # 問い合わせのクラスはIN(インターネット) = 1

逆引きの回答
c0 0c            # 圧縮ポインタが0xc(先頭0から12番目)から始まるドメイン名(157.208.147.220.in-addr.arpa)を示す
00 0c            # 問い合わせのタイプは、IPv4の逆引き = 12(0x0c)
00 01            # 問い合わせのクラスはIN(インターネット) = 1
00 00 0e 10      # TTL(秒)
00 25            # 回答の長さ、25バイト
0f 32 32 30 2d 31 34 37 2d 32 30 38 2d 31 35 37 # 220-147-208-157
05 74 6f 6b 79 6f                               # tokyo
02 61 70                                        # ap
07 67 6d 6f 2d 69 73 70                         # gmo-isp
02 6a 70 00                                     # jp

$ host 220.147.208.157
157.208.147.220.in-addr.arpa domain name pointer 220-147-208-157.tokyo.ap.gmo-isp.jp.
220.147.208.157は、www.karing.jpの本稿執筆時のIPv4アドレスで、正引きではDDNSのmydns.jp(karing.jpの権威サーバー)が担当し、IPv4アドレスを返しますが、逆引きでは当サーバーのプロバイダであるGMOBB管理下のFQDNが取得されます。

逆引きでも問い合わせのタイプの数値が異なるだけで処理の流れ自体は正引きとまったく変わりません。問い合わせ時のドメイン名変換が正引きともっとも異なる部分かと思います。


6. シェルスクリプトによる制限とその回避策

本稿の目的はDNSクライアントのシェルスクリプトによる実装ですが、DNSサーバーとのセッションはバイト列でのやり取りになり、バイト列の処理は想定外としているシェルスクリプトでは様々な制約が生じます。これまで見てきたようにDNS QUERY、DNS RESPONSEともに0x00が頻出しますが、シェルは0x00を文字列の終端と認識するためこれらのバイト列を正しく扱えません(0x00を変数内に保持できない)。本稿ではこの問題を回避するために、まず、最初にodでバイト列をテキスト化し、その後に様々な処理を行っています。
RESPONSE=`timeout 1s cat <&3 |od -t x1 -A n`

そして、第3項 DNS QUERYでふれたようにDNS QUERYの送出ではシェルのビルトインコマンドのechoを使っていません。DNS QUERYのUDPソケットへの送出でビルトインコマンドのechoを使った場合、この処理は変数への代入ではないので\x00は大丈夫ですが、\x0a(改行)では問題が出ます(ビルトインコマンドのprintfでも同じです)。ドメイン名に10文字のラベル(例 cloudflare.com)があるとその先頭は\x0aになり、ビルトインコマンドのechoはその\x0aを改行と認識してしまうという問題です。このため、正しくDNS QUERYを送れないという事態に陥りました(結果、DNSサーバーはFORMERRを返してきます)。この問題は、前述の通りcoreutilsのechoを使えば回避できますが、ビルトインコマンドのechoでも以下のようにパイプ経由のcatで出力すれば回避できます。
$ echo -en "$QUERY" |cat >&3 # >&3はDNSサーバと繋がれたUDPソケット

シェルのビルトインコマンドでUDPソケットに直接書き込む場合とパイプ経由で書き込む場合とで違いが出るこの挙動は、bash 2.05bでも確認でき、仕様なのか何らかのバグなのか判断に迷うところです。この差異は端末上ではわからず、直接パケットキャプチャ(tcpdump)してみるまで、けっこうはまりました。
*** ビルトインコマンドのechoでは、0aで終了してしまう。/usr/bin/echoは最後まで出力する。 ***
# echo -ne '\xaf\x69\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x0acloudflare\x03com\x00\x00\x01\x00\x01' >&3
# tcpdump -X -i eu0 port 53 -c 3
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eu0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
23:19:33.078370 IP mimoza.karing.jp.53078 > tampopo.domain: 44905+ [|domain]
        0x0000:  4500 002d c24e 4000 4011 9d11 c0a8 2d08  E..-.N@.@.....-.
        0x0010:  c0a8 2d07 cf56 0035 0019 1f77 af69 0100  ..-..V.5...w.i..
        0x0020:  0001 0000 0000 0000 0377 7777 0a00       .........www..

# /usr/bin/echo -ne '\xaf\x69\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x0acloudflare\x03com\x00\x00\x01\x00\x01' >&3
# tcpdump -X -i eu0 port 53 -c 3
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eu0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
23:30:41.808892 IP mimoza.karing.jp.42511 > tampopo.domain: 44905+ A? www.cloudflare.com. (36)
        0x0000:  4500 0040 f1cb 4000 4011 6d81 c0a8 2d08  E..@..@.@.m...-.
        0x0010:  c0a8 2d07 a60f 0035 002c 690d af69 0100  ..-....5.,i..i..
        0x0020:  0001 0000 0000 0000 0377 7777 0a63 6c6f  .........www.clo
        0x0030:  7564 666c 6172 6503 636f 6d00 0001 0001  udflare.com.....
また、DNS RESPONSEを受け取るとき、catをtimeoutで制御しているのはとても非効率で、本来ならばDNS RESPONSEが終了した時点で終了すべきです。UDPによるDNSでは明確な終了シグナルといったものはありませんが、一般的なDNSクライアントは割り込みやパケットのメタデータなどを駆使して即時に終了を判断しています。しかし、シェルスクリプトではそのような低レベルな操作は難しく、まさにシェルスクリプトの限界と言えます。

その他、シェルスクリプトにおける一般的な課題として「実行速度が遅い」というものがあります。可能な限り、ビルトインコマンドを使う、サブシェルは起動しない(パイプは使わない)、という方針で設計、実装するのがベターです。とはいえ、最近のパワフルなマシンではあまり気にしてもたいして意味がないかもしれません。シェルの機能を駆使した複雑でわかりにくい処理より、パイプを通して外部コマンド一発の簡明さはそれなりに魅力的です。

最後に、bashの環境依存についてですが、UDPリダイレクト(/dev/udp/host/port)はDebian/Ubuntu系のシステム版bashではセキュリティ上の理由で無効化されていることがあるそうです。本稿は、FedoraまたはGentooで検証されています。詳細は第2項 実証環境を参照してください。


7. 実践例

ここまでの思索をまとめたものが以下(dnscl.sh)です。400行強のスクリプトで単純、丁寧にDNSプロトコルを追いかけていると思います。AIの最初の評価は、コメントが少ないとのことだったので可読性向上のため大分コメントを増やしました。別に明示したわけではないのにAIはこのスクリプトを教材として評価・認識しており、AIはそういうニュアンスや深意も理解できるんだなぁ、と驚くばかりでした。まぁ、他に使い道がないので当たり前と言えば当たり前かもしれませんが。
dnscl.sh
$ ./dnscl.sh www.karing.jp
DOMAIN: www.karing.jp
ARECOR: rita.karing.jp.
DOMAIN: rita.karing.jp
V4ADDR: 220.147.208.157

## エラー(存在しないドメイン名)
$ ./dnscl.sh nonexistent.example.com
NXDOMAIN

## 逆引き
$ ./dnscl.sh 8.8.8.8
DOMAIN: 8.8.8.8.in-addr.arpa
REVMAP: dns.google.
これまでの項では、RFC上、transaction IDは任意なので固定でも問題がないとして固定してきましたが、この実践例ではランダムに生成するようになっています。また、DNSサーバーの選択は第二引数で与えられ、デフォルトでは127.0.0.1になっています。使っているDNSサーバーがどこなのかを示す方法があった方が良いような気もしましたが、とりあえず、ここでは割愛しました。なお、本稿では\x0a対策としてcoreutilsのechoを使っていますが、この実践例ではパイプ経由のcatで対応しています。これは単純に好みの問題で、ビルトインのechoこそが真のechoだ!というある種の原理主義ですね。ただし、coreutilsのechoによる対策の方が微妙に速いようです。

そして、この実装の最大の難所は圧縮ポインタの解析です。圧縮ポインタは他の圧縮ポインタを参照することも許容されるので関数(p_resolve)は再帰問い合わせに対応する必要があり、合わせて無限ループ防止策も必要となります。実際には、それらの実装自体はそんなに大変ではなかったのですが、むしろ無限ループに陥るような検証用のDNS RESPONSEを作る方が難しかったです。と、いろいろ頑張ったところ、AIは、圧縮ポインタは前方参照必須なので無限ループ回避はもっと簡易に可能ですと教えてくれたのでした。

さて、実装の難所は圧縮ポインタでしたが、このスクリプトの実運用面での最大の難点はDNS RESPONSEの受信をtimeoutで定時制御しているところで必然的に無駄な待ち時間が生じることです。この無駄な待ち時間を最小化するため本スクリプトでは、timeoutの待ち時間(秒)をwait_sec_listで調整することにしました。初期値では"0.1 0.2 0.5 1 2"と試行していくようになっています。この値はとくに検証したわけではなく、とりあえず問題がないというだけの値です。興味のある方は体感や統計値で調整し、最適化を目指してみて下さい。


8. まとめ

DNSクライアントはバイナリの読解や対応などが面倒なだけでそんなに難しくはないと思いつつも、ずっとスルーしてきたのですが、そうだ!RFCの要約とか抜粋とか面倒事はAIにやらせればいいんだ!と思いつき、始めたのが今回の企画です。これまで、smtp(s)、http(s)、pop3(s)、などのクライアントをシェルスクリプトで実装してきましたが、AIのおかげで今回のDNSクライアントが一番楽というか最も効率的に短時間で実装できました。

とはいえ、今回はAIは要約や抜粋、改良や改善には力を発揮してくれましたが、直接的な問題解決にはあまり役に立たなかったのも事実です。AIはゼロからやってもらう分にはなかなか優秀ですが、既存のデータの間違い探しみたいなことはあまり得意ではないように見えました。もっとも、それはAIの限界とかを論ずる次元の話ではなく、おそらく、少なくとも今回の企画に関しては、単にシェルスクリプトについての類題学習の不足が最大の原因だったと思われます。つまり、こんなこと誰もやってないのよなぁ…、といういつものため息が漏れてしまいました。こういう時、AIは「こんなことを考えるのはあなただけ!」と絶賛してくれるのですが、なんだかなぁ…、ってな気分です。

2025-12-25 よしのぶ
yoshino@rita.karing.jp
  戻る
  index.html


2025-12-25 Thu 18:52