👻

ソケット通信で自分のドメインの名前解決してみた

2024/07/17に公開

はじめに

いつも私はPHPでWebアプリケーションを作成しているのですが、
あらかじめ登録したドメイン名にHTTPリクエストをする中で
DNSサーバによる名前解決を意識することなく、
ブラウザまかせにしてきました。

時には調査の中でドメイン名からIPアドレスを正引きすることは
あっても、PHPのgethostbyname()関数などを使って
ホスト名を引数に渡してIPアドレスを得る程度だったのです。

そこで、今回はRFC1035に基づいてソケット通信で
DNSサーバから正引きをしてみることにしてみました。

それにしてもRFC1035が出されたのが1987年11月とは…
私が1983年生まれなので4歳のころの文書です。

環境

今回は Google Public DNS のプライマリサーバである
「8.8.8.8」に問い合わせをします。

名前解決をするドメインは私の個人サイトで使用している
「sakamotokenji.com」を使います。

.envファイルを作成する

今回の検証では名前解決をするドメイン名や、
問い合わせを行うDNSサーバを.envファイルで定義しました。
.envファイルは.gitignoreでバージョン管理から外していますので、
ソースのルートで次のコマンドを実行して.envファイルを作成してください。

cp .env.sample .env

DNSサーバを指定する

問い合わせを行うDNSサーバを指定します。

.env
# 問い合わせをするDNSサーバ
DNS_SERVER=8.8.8.8

ドメイン名を指定する

名前解決をしたいドメイン名を指定します。

.env
# 名前解決をするドメイン名
HOST_NAME=sakamotokenji.com

.envを読めるようにする

src/socket_dns_connect.php
require_once __DIR__ . "/../vendor/autoload.php";
use Dotenv\Dotenv;
src/socket_dns_connect.php
// .envのロード
$dotenv = Dotenv::createImmutable(__DIR__ . "/..");
var_dump($dotenv->load());

DNSサーバへリクエストする

メッセージのフォーマット

RFC1035のメッセージのフォーマットの説明をみるとこのような構成となっていました。

+---------------------+
|        Header       |
+---------------------+
|       Question      | the question for the name server
+---------------------+
|        Answer       | RRs answering the question
+---------------------+
|      Authority      | RRs pointing toward an authority
+---------------------+
|      Additional     | RRs holding additional information
+---------------------+
項目 説明
ヘッダー部 ヘッダー部は常に存在
問い合わせ部 ネームサーバーへの問い掛け
回答部 問い掛けに回答するRR
権威部 権威を指し示すRR
付加情報部 付加的な情報を保持するRR

ヘッダー部を組み立てる

RFC1035のヘッダー部の説明をみるとこのような構成となっていました。

                                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を設定する

まず1段目の16ビットであるIDを生成します。
今回は適当に「0xAAAA」としました。
16進表記の「A」は2進表記では「1010」となります。
なので、「AAAA」は「1010101010101010」ですね。
そして「0b1010101010101010」の頭2文字の「0b」はPHPでの
数値の2進数表記ということになります。
あとでpack()関数でバイナリ文字列にパック(つまり変換)します。

ここで設定するIDは、DNSサーバからのレスポンスのIDとしてコピーされますので、
リクエストのIDとレスポンスのIDが一致しているかみることで
リクエストしたもののレスポンスであることがわかるようになっています。

src/socket_dns_connect.php
/**
 * @var int $headerId ヘッダー部 ID
 * 16ビットのIDで、任意の種類の問い合わせを生成するプログラムによって割り当てられる。
 * このIDは対応する応答にコピーされ、リクエスト発行者が応答と発行済みの問い合わせを一致させるために使用できる。
 */
$headerId = 0b1010101010101010;

QRを設定する

次に2段目の16ビットの中のQRを設定します。
先頭から数えて最初の1ビットになります。
今回は問い合わせなので「0」を設定しています。

src/socket_dns_connect.php
/**
 * @var int $headerQr ヘッダー部 QR
 * 1ビットのフィールドで、このメッセージが問い合わせ(0)か応答(1)かを指定する。
 */
$headerQr = 0b0000000000000000;

OPCODEを設定する

次に2段目の16ビットの中のOPCODEを設定します。
先頭から数えて2ビット目から5ビット目までの4ビットになります。
今回は「標準問い合わせ」ですので「0000」を設定しています。

src/socket_dns_connect.php
/**
 * @var int $headerOpCode ヘッダー部 OPCODE
 * 4ビットのフィールドで、このメッセージの問い合わせ種別を
 * 指定する。この値は問い合わせ生成者によって設定され、
 * 応答にコピーされる。採りうる値は以下の通りである。
 * 0    標準問い合わせ(QUERY)。
 * 1    逆問い合わせ(IQUERY)。
 * 2    サーバーステータスのリクエスト(STATUS)。
 * 3-15 将来の利用のために予約。
 */
$headerOpCode = 0b0000000000000000;

AAを設定する

次に2段目の16ビットの中のAAを設定します。
先頭から数えて6ビット目になります。
これは応答のみ有効ということですので「0」を設定しています。

src/socket_dns_connect.php
/**
 * @var int $headerAa ヘッダー部 AA
 * 権威を持つ回答(Authoritative Answer):このビットは応答で
 * 有効なものであり、応答したネームサーバーが問い合わせ部の
 * ドメイン名の権威であるかを指定する。
 * 回答部の内容は、別名に起因して複数の所有者名を持つ場合が
 * あることに注意せよ。
 * AAビットは、問い合わせ名に一致する名前か、回答部の最初に
 * 現れる所有者名に対応する。
 */
$headerAa = 0b0000000000000000;

TCを設定する

次に2段目の16ビットの中のTCを設定します。
先頭から数えて7ビット目になります。
これも応答時の項目となりますので「0」を設定しています。

src/socket_dns_connect.php
/**
 * @var int $headerTc ヘッダー部 IC
 * 切り詰め(TrunCation): このメッセージが転送チャネルで許容
 * されるよりも長かったために切り詰められたかを指定する。
 */
$headerTc = 0b0000000000000000;

RDを設定する

次に2段目の16ビットの中のRDを設定します。
先頭から数えて8ビット目になります。
これは再帰的に問い合わせを継続してほしいので「1」を設定します。

src/socket_dns_connect.php
/**
 * @var int $headerRd ヘッダー部 RD
 * 再帰要求(Recursion Desired): このビットは問い合わせで設定
 * することができ、応答にもコピーされる。RDが設定されている
 * 場合、ネームサーバーに再帰的に問い合わせを継続せよという
 * 指示になる。再帰問い合わせのサポートは任意である。
 */
$headerRd = 0b0000000100000000;

RAを設定する

次に2段目の16ビットの中のRAを設定します。
先頭から数えて9ビット目になります。
これは応答時の項目となりますので「0」を設定しています。

src/socket_dns_connect.php
/**
 * @var int $headerRa ヘッダー部 RA
 * 再帰可能(Recursion Available): このビットは応答で設定または
 * クリアされるもので、そのネームサーバーにおいて再帰問い合わせが
 * 利用可能かどうかを提示する。
 */
$headerRa = 0b0000000000000000;

Zを設定する

次に2段目の16ビットの中のZを設定します。
先頭から数えて10ビット目から12ビット目になります。
これは将来の利用のために予約済みということで「000」を設定しています。

src/socket_dns_connect.php
/**
 * @var int $headerZ ヘッダー部 Z
 * 将来の利用のために予約済みとなっている。すべての問い合わせ、
 * 応答で、このフィールドはすべてゼロでなければならない。
 */
$headerZ = 0b0000000000000000;

RCODEを設定する

次に2段目の16ビットの中のRCODEを設定します。
この項目が最後の4ビットになります。
これは応答時の項目となりますので「0000」を設定しています。

src/socket_dns_connect.php
/**
 * @var int $headerRcode ヘッダー部 RCODE
 * 応答コード(Response code): この4ビットフィールドは、応答の
 * 一部として設定される。設定される値は以下に示すように解釈される。
 * 0    ... エラー無し
 * 1    ... フォーマットエラー: ネームサーバーは問い合わせを解釈できなかった。
 * 2    ... サーバー障害: ネームサーバーは、サーバー側の問題でこの問い合わせを処理できなかった。
 * 3    ... ドメイン名不在: 権威ネームサーバーからの応答でのみ意味を持つ。このコードは、
 *          問い合わせで参照されたドメイン名が存在しなかったことを提示する。
 * 4    ... 未実装: ネームサーバーはリクエストされた種別の問い合わせをサポートしていない。
 * 5    ... 問い合わせ拒否: ネームサーバーは、ポリシーによる理由で指定された処理の実行を拒否する。
 *          例えば、ネームサーバーは特定のリクエスト発行者には情報を提供したくないと望むかも
 *          しれない。あるいはネームサーバーは特定のデータに関する特定の処理(例えばゾーン転送)を実行したくないと望むかもしれない。
 * 6-15 ... 将来の利用のために予約。
 */
$headerRcode = 0b0000000000000000;

QDCOUNTを設定する

次に3段目の16ビットのQDCOUNTを設定します。
今回問い合わせする数は1つなので「1」を設定しています。

src/socket_dns_connect.php
/**
 * @var int $headerQdCount ヘッダー部 QDCOUNT
 * 符号無し16ビット整数で、問い合わせ部のエントリー数を指定する。
 */
$headerQdCount = 0b0000000000000001;

ANCOUNTを設定する

次に4段目の16ビットのANCOUNTを設定します。
これは応答時の項目となりますので「0000000000000000」を設定しています。

src/socket_dns_connect.php
/**
 * @var int $headerAnCount ヘッダー部 ANCOUNT
 * 符号無し16ビット整数で、回答部のリソースレコード数を指定する。
 */
$headerAnCount = 0b0000000000000000;

NSCOUNTを設定する

次に5段目の16ビットのNSCOUNTを設定します。
これも応答時の項目となりますので「0000000000000000」を設定しています。

src/socket_dns_connect.php
/**
 * @var int $headerNsCount ヘッダー部 NSCOUNT
 * 符号無し16ビット整数で、権威部のネームサーバーリソースレコード数を指定する。
 */
$headerNsCount = 0b0000000000000000;

ARCOUNTを設定する

次にヘッダー部最後の6段目となるARCOUNTを設定します。
これも応答時の項目となりますので「0000000000000000」を設定しています。

src/socket_dns_connect.php
/**
 * @var int $headerArCount ヘッダー部 ARCOUNT
 * 符号無し16ビット整数で、付加情報部のリソースレコード数を指定する
 */
$headerArCount = 0b0000000000000000;

ヘッダー部のバイナリデータを作成する

2進数表記の数値として準備してきたヘッダー部の各値を
バイナリデータとしてパックし結合します。

src/socket_dns_connect.php
    $header = "";
    // ヘッダー部の1段目としてバイナリデータにパックする
    $header .= pack("n", $headerId);
    // ヘッダー部の2段目としてバイナリデータにパックする
    $header .= pack(
        "n",
        $headerQr |
        $headerOpCode |
        $headerAa |
        $headerTc |
        $headerRd |
        $headerRa |
        $headerZ |
        $headerRcode
    );
    // ヘッダー部の3段目としてバイナリデータにパックする
    $header .= pack("n", $headerQdCount);
    // ヘッダー部の4段目としてバイナリデータにパックする
    $header .= pack("n", $headerAnCount);
    // ヘッダー部の5段目としてバイナリデータにパックする
    $header .= pack("n", $headerNsCount);
    // ヘッダー部の6段目としてバイナリデータにパックする
    $header .= pack("n", $headerArCount);

結合したバイナリデータを確認してみます。

src/sockert_dns_connect.php
// ヘッダーを16進数表現の文字列に変換する
$headerHex = bin2hex($header);
// 大文字に変換する
$headerHex = strtoupper($headerHex);
echo $headerHex;

するとこのような出力結果が得られました。

AAAA01000001000000000000

整形してみます。

0xAA 0xAA ←ヘッダー部1段目(16ビット) 
0x01 0x00 ←ヘッダー部2段目(16ビット)
0x00 0x01 ←ヘッダー部3段目(16ビット)
0x00 0x00 ←ヘッダー部4段目(16ビット)
0x00 0x00 ←ヘッダー部5段目(16ビット)
0x00 0x00 ←ヘッダー部6段目(16ビット)

意図したとおりのヘッダーができました。

問い合わせ部を組み立てる

RFC1035の問い合わせ部の説明をみるとこのような構成となっていました。

                                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                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

QNAMEを設定する

ここでは実際に問い合わせたいドメイン名を指定します。
今回は「sakamotokenji.com」になります。
RFCを見ると次のように説明されています。

ラベルの並びとして表現されるドメイン名で、各ラベルは、
ラベル長オクテットと、その数だけオクテットが続くもので構成
される。ドメイン名は、ルートのヌルラベルに関するゼロが指定
されたラベル長オクテットで終端される。このフィールドの
オクテット数が奇数であってもよいことに注意せよ。パディングは
使用されない。

ということで、ここはすこし複雑なのですが、
「sakamotokenji.com」というドメインであれば
ここでいうところの各ラベル、というのは、
「sakamotokenji」と「com」の2個ということになります。
つまり、ドットで分割するということですね。

ここで少し気になったことがあるのですが、
ドットがここでいうところのラベルのセパレータである、
というところがRFC内で定義されているのか確認してみたのですが
確認した限りではそのような記述は見つけられませんでした。

そしてこのラベルたちをバイナリデータとして送ることになるのですが、
ラベルの先頭には「ラベル長オクテット」というデータをつける必要があります。

オクテットは8ビット(1バイト)のデータということになりますので、
「sakamotokenji」であれば13文字なので、2進数表記で13は 「00001101」となります。
なので、
「00001101」+「sakamotokenji」
ということになります。

つぎに「com」は3文字なので、2進数表記で3は「00000011」となります。
なので、
「00000011」+「com」
ということになります。

最後に、「ルートのヌルラベルに関するゼロが指定されたラベル長オクテット」をつける、
ということなのですが、この「ルートのヌルラベル」というのは、
ルートゾーンを示す特別なラベルのことで、ドメイン末尾につけるドットのことになります。
つまり、「sakamotokenji.com」というドメインにルートのヌルラベルをつけると、
「sakamotokenji.com.」という表記になります。

それで、この「ルートのヌルラベル」をつける代わりに、
「ゼロが指定されたラベル長オクテット」をつけるということなので、
2進数表記では「00000000」となります。

コードにすると以下のようになります。

src/socket_dns_connect.php
/**
 * @var string $questionQname QNAME
 * ラベルの並びとして表現されるドメイン名で、各ラベルは、
 * ラベル長オクテットと、その数だけオクテットが続くもので構成
 * される。ドメイン名は、ルートのヌルラベルに関するゼロが指定
 * されたラベル長オクテットで終端される。このフィールドの
 * オクテット数が奇数であってもよいことに注意せよ。パディングは
 * 使用されない。
 */
$questionQname = ""; // 変数を初期化する
$labels = explode(".", $hostname); // ドメイン名をドットで分割する
foreach ($labels as $label) {
    $length = strlen($label); // ラベルの長さを取得する
    $binaryLength = pack("C", $length); // ラベルの長さをオクテットに変換する
    $questionQname .= $binaryLength . (binary) $label; // ラベル長オクテット + ラベルを結合してセットする
}
$questionQname .= pack("C", 0); // 最後にヌルラベルをセットする

実行後の$questionQnameの値は次のようになっています。

src/socket_dns_connect.php
// $questionQnameの確認
// ヘッダーを16進数表現の文字列に変換する
$headerHex = bin2hex($questionQname);
// 大文字に変換する
$headerHex = strtoupper($headerHex);
echo sprintf("\$questionQname: %s\n", $headerHex);
$questionQname: 0D73616B616D6F746F6B656E6A6903636F6D00

整形してみます。

0x0D 0x73 0x61 0x6B 0x61 0x6D 0x6F 0x74 0x6F 0x6B 0x65 0x6E 0x6A 0x69 // 13 + sakamotokenji
0x03 0x63 0x6F 0x6D // 3 + com
0x00 // ヌルラベル

となっていることが確認できました。

QTYPEを設定する

次に問い合わせのタイプを指定します。
RFC1035の3.2.2に次の記述を参考にしました。

3.2.2. TYPEの値

TYPEフィールドは、リソースレコード内で使用される。これらのタイプはQTYPEの
サブセットであることに注意せよ。

タイプ          値とその意味

A               1 ホストアドレス。

NS              2 権威ネームサーバー。

MD              3 メールの宛先(廃止: MXを使用せよ)。

MF              4 メールのフォワーダー(廃止: MXを使用せよ)。

CNAME           5 別名の正式名。

SOA             6 ゾーンの権威の開始を示す。

MB              7 メールボックスのドメイン名(実験的)

MG              8 メールグループのメンバー(実験的)

MR              9 リネームした新しいメールボックスのドメイン名(実験的)

NULL            10 ヌルRR(実験的)

WKS             11 よく知られているサービスの記述

PTR             12 ドメイン名へのポインター

HINFO           13 ホスト情報

MINFO           14 メールボックスまたはメールリストの情報

MX              15 メールエクスチェンジ

TXT             16 テキスト文字列

今回はAレコードのホストアドレスを取得したいと思いますので 「1」を設定します。

src/socket_dnsconnect.php
/**
 * @var int $questionQtype QTYPE
 * 2オクテットのコードで、問い合わせのタイプを指定する。
 * このフィールドの値は、TYPEフィールドで有効なすべてのコードに
 * 加えて、二つ以上のタイプのRRに一致できるより一般的なコードを
 * 幾つか含む。
 */
$questionQtype = 0b0000000000000001;

QCLASSを設定する

問い合わせ部の最後に問い合わせのクラスを設定します。
RFC1035の 3.2.4 - 3.2.5 の記載を参考に、
ここでは「1」のインターネットを指定します。

3.2.4. CLASSの値

CLASSフィールドはリソースレコード内に現れる。以下のCLASSのニーモニックと
値が定義される。

IN              1 インターネット。

CS              2 CSNETクラス(廃止: 幾つかの廃止されたRFCの中で例として
                  使用されているに過ぎない)。

CH              3 CHAOSクラス

HS              4 Hesiod [Dyer 87]

3.2.5. QCLASSの値

QCLASSフィールドは、問い合わせの問い合わせ部に現れる。QCLASSの値はCLASSの値の
スーパーセットであるから、CLASSの値はすべてQCLASSでも有効である。それに
加えて、以下のQCLASSが定義される。
src/socket_dns_connect.php
/**
 * @var int $questionQclass QCLASS
 * 2オクテットのコードで、問い合わせのタイプを指定する。
 * このフィールドの値は、TYPEフィールドで有効なすべてのコードに
 * 加えて、二つ以上のタイプのRRに一致できるより一般的なコードを
 * 幾つか含む。
 */
$questionQclass = 0b0000000000000001;

問い合わせ部の各項目の結合

src/socket_dns_connect.php
/**
 * @var string $question 問い合わせ部
 */
$question = "";
$question .= $questionQname; // QNAME
$question .= pack("n", $questionQtype); // QTYPE
$question .= pack("n", $questionQclass); // QCLASS

ヘッダー部と問い合わせ部を結合する

ここまでで、ヘッダー部と問い合わせ部ができました。
RFC1035のメッセージのフォーマットとして記載されている残りの、

  • 回答部
  • 権威部
  • 付加情報部

は応答時のメッセージになりますので作成は不要です。

src/socket_dns_connect.php
// DNSのクエリメッセージを作成
$query  = "";
$query .= $header; // ヘッダー部
$query .= $question; // 問い合わせ部

DNSサーバにソケット接続する

送信するためのメッセージができましたので、
メッセージを送信するためDNSサーバに接続します。
ソケットはPHPの socket_create() 関数を使用します。

https://www.php.net/manual/ja/function.socket-create.php

src/socket_dns_connect.php
// UDPソケットを作成
if (!$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP)) {
    throw new Exception(sprintf(
        "socket_create() failed: reason: %s\n",
        socket_strerror(socket_last_error())
    ));
}

第1引数「AF_INET」

第1引数はプロトコルファミリーを指定します。
プロトコルファミリーとはネットワーク通信における一連のプロトコル(規約や規格)のグループという意味です。
ここではIPv4アドレスを使用するソケット通信のためのプロトコルファミリーである「AF_INET」を指定します。

第2引数「SOCK_DGRAM」

第2引数はソケットが利用する通信方式を指定します。
今回はDNSサーバへの問い合わせとなりますので、UDPの53番ポートを使用します。
上記のPHPマニュアルによるとUDPプロトコルは「SOCK_DGRAM」のソケットタイプに基づくということでこれを設定します。

第3引数「SOL_UDP」

第3引数はソケット上の通信で使われる domain で指定されたファミリーに属するプロトコルを指定と記載があります。
今回はUDPになりますので「SOL_UDP」を設定します。

DNSサーバにメッセージを送信する

DNSサーバへの接続ができましたので
phpのsocket_sendto()関数を使用して
さきほど作成したメッセージを送信します。

https://www.php.net/manual/ja/function.socket-sendto.php

src/socket_dns_connect.php
// DNSサーバにメッセージを送信
if (!socket_sendto($sock, $query, strlen($query), 0, $dns_server, 53)) {
    throw new Exception(sprintf(
        "socket_sendto() failed: reason: %s\n",
        socket_strerror(socket_last_error())
    ));
}

第1引数「$sock」

socket_create()で作成したSocketクラスのインスタンスということなので
前述の「$sock」を指定します。

第2引数「$query」

先ほど作成したヘッダー部と問い合わせ部からなるバイナリメッセージデータを指定します。

第3引数「strlen($query)」

データサイズを計算して指定します。

第4引数「0」

今回は特にどのフラグも立てないので「0」を指定します。

第5引数「$dns_server」

リモートホストのIPアドレスということで、今回は
Google Public DNSサーバのプライマリを指定します。

第6引数「53」

ポートはDNSのUDP53ポートを指定します。

DNSサーバからのレスポンスを取得する

DNSサーバにメッセージを送信しました。
続いて、DNSサーバからの応答を受信します。
受信にはPHPのsocket_recvfrom()関数を使用します。

https://www.php.net/manual/ja/function.socket-recvfrom.php

src/socket_dns_connect.php
// 応答を受信する
$port = null;
if (!socket_recvfrom($sock, $response, 512, 0, $address, $port)) {
    throw new Exception(sprintf(
        "socket_recvfrom() failed: reason: %s\n",
        socket_strerror(socket_last_error())
    ));
}

第1引数「$sock」

socket_create()で作成したSocketクラスのインスタンスということなので
前述の「$sock」を指定します。

第2引数「$response」

受信したデータが格納されるということなので、
ここでは「$response」という変数に格納されるようにします。

第3引数「512」

RFC1035をみるとUDPのメッセージ最大長は512バイトである記載がありましたので、
ここでは「512」を指定します。

UDPで運ばれるメッセージは512バイト(IPヘッダーやUDPヘッダーは除く)までに
制限される。それよりも長いメッセージは切り詰められ、ヘッダー内のTCビットが
設定される。

第4引数「0」

フラグについては今回特に設定ないので「0」を設定し、
何もフラグを立てないことを示します。

第5引数「$address」

AF_UNIX 型のソケットの場合は、address はファイルへのパスということなので、
「$address」という名前を設定します。

第6引数「$port」

第6引数について、phpマニュアルに以下の記載がありました。

この引数は AF_INET 型あるいは AF_INET6 型のソケットに対してのみ適用され、
データを受信するリモートホストのポートを指定します。
接続済みソケットの場合は port は null となります。

なので、今回は接続済みソケットとなりますので、
あらかじめ $port に null をセットしてそれを指定します。

DNSサーバとのソケット接続を閉じる

DNSサーバからのデータの受信が完了しましたので、
ソケットを閉じます。
ソケットを閉じるにはPHPの socket_close() を使います。

https://www.php.net/manual/ja/function.socket-close.php

src/socket_dns_connect.php
// ソケットを閉じる
socket_close($sock);

これでソケットを閉じることができました。

DNSサーバから受信した応答データをみる

それでは応答データをみていきましょう。
まずは受信したデータのサイズをみてみます。

src/socket_dns_connect.php
// 受信したデータのサイズを確認する
echo sprintf("response size: %d Byte\n", strlen($response));
response size: 51 Byte

データサイズは51バイトでした。

次に受診したデータの中身を見てみます。

src/socket_dns_connect.php
// 受信したデータを表示する
echo sprintf("response:\n%s\n", strtoupper(bin2hex($response)));

データ16進表記で下記の内容となっていました。

response:
AAAA818000010001000000000D73616B616D6F746F6B656E6A6903636F6D0000010001C00C000100010000012C00040D712FF7

整形してみます。

// ヘッダー部:1段目(16ビット)
0xAA 0xAA
// ヘッダー部:2段目(16ビット)
0x81 0x80
// ヘッダー部:3段目(16ビット)
0x00 0x01
// ヘッダー部:4段目(16ビット)
0x00 0x01
// ヘッダー部:5段目(16ビット)
0x00 0x00
// ヘッダー部:6段目(16ビット)
0x00 0x00

// 問い合わせ部: QNAME
0x0D 0x73 0x61 0x6B 0x61 0x6D 0x6F 0x74 0x6F 0x6B 0x65 0x6E 0x6A 0x69 // 13 + sakamotokenji
0x03 0x63 0x6F 0x6D //  3 + com
0x00 // ヌルラベル
// 問い合わせ部: QTYPE
0x00 0x01
// 問い合わせ部: QCLASS
0x00 0x01

// 回答部: NAME(圧縮、ポインタ)
0xC0 0x0C
// 回答部: TYPE
0x00 0x01
// 回答部: CLASS
0x00 0x01
// 回答部: TTL
0x00 0x00 0x01 0x2C
// 回答部: RDLENGTH
0x00 0x04
// 回答部: RDATA
0x0D 0x71 0x2F 0xF7

応答データのヘッダー部

1段目の16ビット

こちらはIDになります。

HEX: 0xAA 0xAA
BIN: 10101010 10101010
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1| 0| 1| 0| 1| 0| 1| 0| 1| 0| 1| 0| 1| 0| 1| 0|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

リクエストで生成したIDと同じものが返ってきていました。

2段目の16ビット

こちらはフラグ群(QR、OPCODE、AA、TC、RD、RA、Z、RCODE)になります。

HEX: 0x81 0x80
BIN: 10000001 10000000
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   OPCODE  |AA|TC|RD|RA|   Z    |   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1| 0| 0| 0| 0| 0| 0| 1| 1| 0| 0| 0| 0| 0| 0| 0|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

QR

リクエスト時は「0=問い合わせ」だったものが「1=応答」になっていました。
応答なので、正しいです。

OPCODE

リクエスト時と同じ「0=標準問い合わせ(QUERY)」になっていました。
この値はリクエストと応答で変わらない要素なので正しいです。

AA

応答したネームサーバがドメイン部の権威であるかというところですが、
これは「0=権威ではない」となっていました。
これは sakamotokenji.com のネームサーバがawsのRoute53を使用しているので、
応答した Google Public DNS のプライマリサーバが権威ではないという回答をしたということになります。

TC

「0=切り詰めされていない」になっていました。
そのことから、このメッセージが転送チャネルで許容されるよりも
長くなかったので切り詰められなかったということになります。

RD

これは再帰要求フラグですが、RFCによると「このビットは問い合わせで設定
することができ、応答にもコピーされる」ということなので意図した通りです。

再帰要求(Recursion Desired): このビットは問い合わせで設定
することができ、応答にもコピーされる。RDが設定されている
場合、ネームサーバーに再帰的に問い合わせを継続せよという
指示になる。再帰問い合わせのサポートは任意である。

RA

「1=再帰問い合わせが可能」になっていました。
そのことから Google Public DNS のプライマリサーバでは
再帰問い合わせが可能、という返答になります。

Z

将来の利用のために予約済みとなっているということで、
問い合わせ時と同様に3ビットすべてが「0」となっていました。

RCODE

RCODEは「0=エラーなし」になっていました。
今回の問い合わせが成功したことを意味しています。

3段目の16ビット

こちらは問い合わせ部のエントリー数です。

HEX: 0x00 0x01
BIN: 00000000 00000001
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

リクエスト時と同じ「1」となっていました。

4段目の16ビット

こちらは回答部のリソースレコード数です。

HEX: 0x00 0x01
BIN: 00000000 00000001
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

応答値は「1」となっていました。
今回1個のドメインの正引きをリクエストしましたが、
それに対して1個の回答があった、ということですね。

5段目の16ビット

こちらは権威部のネームサーバのリソースレコード数です。

HEX: 0x00 0x00
BIN: 00000000 00000000
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

応答値は「0」となっていました。
そのことからリソースを提供した権威DNSサーバの
リソースのレコード数は「0」ということになります。

6段目の16ビット

こちらは付加情報部のリソースレコード数です。

HEX: 0x00 0x00
BIN: 00000000 00000000
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

応答値は「0」となっていました。
付加情報はないということですね。

応答データの問い合わせ部

QNAME

HEX:
0x0D 0x73 0x61 0x6B 0x61 0x6D 0x6F 0x74 0x6F 0x6B 0x65 0x6E 0x6A 0x69 // 13 + sakamotokenji
0x03 0x63 0x6F 0x6D //  3 + com
0x00 // ヌルラベル

BIN:
00001101 01110011 01100001 01101011 01100001 01101101 01101111 01110100 01101111 01101011 01100101 01101110 01101010 01101001
00000011 01100011 01101111 01101101
00000000
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     QNAME                     |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 1| 1| 0| 1| 0| 1| 1| 1| 0| 0| 1| 1|
| 0| 1| 1| 0| 0| 0| 0| 1| 0| 1| 1| 0| 1| 0| 1| 1|
| 0| 1| 1| 0| 0| 0| 0| 1| 0| 1| 1| 0| 1| 1| 0| 1|
| 0| 1| 1| 0| 1| 1| 1| 1| 0| 1| 1| 1| 0| 1| 0| 0|
| 0| 1| 1| 0| 1| 1| 1| 1| 0| 1| 1| 0| 1| 0| 1| 1|
| 0| 1| 1| 0| 0| 1| 0| 1| 0| 1| 1| 0| 1| 1| 1| 0|
| 0| 1| 1| 0| 1| 0| 1| 0| 0| 1| 1| 0| 1| 0| 0| 1|
| 0| 0| 0| 0| 0| 0| 1| 1| 0| 1| 1| 0| 0| 0| 1| 1|
| 0| 1| 1| 0| 1| 1| 1| 1| 0| 1| 1| 0| 1| 1| 0| 1|
| 0| 0| 0| 0| 0| 0| 0| 0|  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

バイナリデータで「sakamotokenji.com.」となっていました。
問い合わせ時に指定した値を同じです。

QTYPE

HEX: 0x00 0x01
BIN: 00000000 00000001
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     QTYPE                     |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

「1」となっていますので、Aレコードになっています。
問い合わせ時に指定した値と同じです。

QCLASS

HEX: 0x00 0x01
BIN: 00000000 00000001
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     QCLASS                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

「1」となっていますので、インターネットになっています。
問い合わせ時に指定した値と同じです。

応答データの回答部

NAME

HEX: 0xC0 0x0C
BIN: 11000000 00001100
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                                               |
/                                               /
/                      NAME                     /
|                                               |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1| 1| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 1| 1| 0| 0|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

この項目は「このリソースレコードが関連付けられるドメイン名」
ということなので、「sakamotokenji.com」が返却されるのかと
思っていたら、16ビットの値が返ってきていました。

RFC1035を確認してみると、4.1.4に「メッセージの圧縮」
という項目がありました。

メッセージサイズを削減するために、ドメインシステムはメッセージ内の
ドメイン名のくり返しを取り除く圧縮の仕組みを使用する。この仕組みでは、
ドメイン名全体またはドメイン名末尾のラベルのリストが、以前に同じ名前が
出現した場所へのポインターに置き換えられる。

「sakamotokenji.com」という文字列を通信データ内で繰り返さないということのようです。
DNSメッセージの先頭からのオフセットのバイト表現ということから、
このポインタの値をみてみると2進数表記の「1100」となっていますので
10進数表記に変換すると「12」となります。
応答データで最初に「sakamotokenji.com」が出てくるのが先頭から12バイトの位置なので
ポイントであるということが確認できました。

TYPE

HEX: 0x00 0x01
BIN: 00000000 00000001
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      TYPE                     |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

この項目は後ほど出てくるRDATAフィールド内のデータの意味ということで、
「0x00 0x01」はAレコードを意味します。
RDATAのデータはAレコードであるということになります。

CLASS

HEX: 0x00 0x01
BIN: 00000000 00000001
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     CLASS                     |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

この項目も後ほど出てくるRDATAフィールド内のデータのクラスをいうことで、
「0x00 0x01」はインターネットクラス(IN)を意味します。
RDATAのデータのクラスはインターネットクラス(IN)であるということになります。

TTL

HEX: 0x00 0x00 0x01 0x2C
BIN: 00000000 00000000 00000001 00101100
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      TTL                      |
|                                               |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0|
| 0| 0| 0| 0| 0| 0| 0| 1| 0| 0| 1| 0| 1| 1| 0| 0|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

この項目は「Time To Live」、所謂TTLになります。
このリソースレコード(RR)をキャッシュしてもよい時間が秒単位で返ってきています。
この「sakamotokenji.com」のドメインはawsのRoute53でTTL300で指定しており、
16進数表記の「0x00 0x00 0x01 0x2C」は10進数表記で「300」になりますので
設定通りの値が返ってきていることが確認できました。

RDLENGTH

HEX: 0x00 0x04
BIN: 00000000 00000100
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                   RDLENGTH                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 0| 1| 0| 0|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

この項目は「RDATAフィールドのオクテット単位の長さ」となります。
今回は「sakamotokenji.com」の「Aレコード」に設定されている
32ビットのIPアドレスを期待していますので、
2進数表記で「100」は10進数表記で「4」となり、
4オクテットは、1オクテットが8ビットですので32ビットとなります。

RDATA

HEX: 0x0D 0x71 0x2F 0xF7
BIN: 00001101 01110001 00101111 11110111
================================================
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/                     RDATA                     /
/                                               /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 0| 0| 0| 0| 1| 1| 0| 1| 0| 1| 1| 1| 0| 0| 0| 1|
| 0| 0| 1| 0| 1| 1| 1| 1| 1| 1| 1| 1| 0| 1| 1| 1|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

リソースを記述する可変長のオクテット列ということで、
今回は「sakamotokenji.com」の「Aレコード」の値を要求しました。
期待結果としては32ビットのIPアドレスとなりますので、
まず長さとしては、32ビットあることが確認できました。

これをIPv4のIPアドレスとして解析していきましょう。

まず、「0x0D」は10進数表記で「13」になります。
次に、「0x71」は10進数表記で「113」になります。
次に、「0x2F」は10進数表記で「47」になります。
次に、「0xF7」は10進数表記で「247」になります。

つなげると、
13.113.47.247
となり、awsのRoute53に設定している「sakamotokenji.com」の
Aレコードを取得することができました。

まとめ

普段は、nslookupコマンドやdigコマンド、
PHPを使うときはgethostbyname()を使うことで
気軽にドメイン名の名前解決をしてきましたが、
低レベルな通信に目をやることで、これまでブラックボックスだった
処理内容が確認できてよかったです。
これまでも「このドメインの名前解決できますか?」という問いに対して
「はい、できます」ということができましたが、これからはより、
「はい、できます」と言えそうです。

今回使用したプログラムソース

https://github.com/sakamotokenji1983/socket-dns-connect-php

参考文献

https://datatracker.ietf.org/doc/html/rfc1035
https://jprs.jp/tech/material/rfc/RFC1035-ja.txt

Discussion