DNS問い合わせ

PHPでDNS問い合わせをソケットを使って試す。

https://www.ietf.org/rfc/rfc1035.txt

とにもかくにも厄介なのが、PHPでバイナリデータを扱うのに四苦八苦する。
とりあえず、DNSでAレコードの問い合わせを簡易的に作成。
今回は、DNSの応答ヘッダのみ出力するところまでを作成。
もちろんヘッダのみなので、肝心のIPアドレスを表示するまでには至っていないけど、
ある程度参考にはなると思います。
例によってコードはダラダラ書きです。

また、ローカル環境では、大抵、ゲートウェイのルータがDNSサーバとして機能していると
思いますので、ルーターのIPアドレスをDNSとしてセットすると問題ないと思われます。
外部DNSサーバーを指定すると、ブロックされたり、或はルーティングを静的に設定しないと
正しく動作しないので注意してください。
グローバルネットワークの環境であれば、余程きついセキュリティ設定でもされていない
限りは、問題なく動作すると思います。

//ここは利用するDNSサーバーのIPアドレスをセットしてください。
$dns_server_ip = '192.168.1.1';
$port = 53;

$id = mt_rand(1, 65535);

$qdata = createQueryARecord($id, 'tecblo.com');
$sendLen = strlen($qdata);

$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_sendto($sock, $qdata, $sendLen, 0, $dns_server_ip, $port);
$buf = '';

$wait = true;
while($wait){
	socket_recvfrom($sock, $buf, 4096, 0, $dns_server_ip, $post);
	$ret = unpack('nid/Cflag1/Cflag2/n4other', $buf);
	
	echo 'id:' . $ret['id'] . "\n";
	if ($ret['id'] != $id) {
		echo 'Mismatch ID.';
		break;
	}
	
	
	$QR = ($ret['flag1'] & 0x80) >> 7;
	echo 'QR:' . $QR . "\n";
	if (!$QR){
		echo 'Invalid Value.';
		break;
	}
	
	$OPCODE = $ret['flag1'] & 0x78;
	echo 'query code: ' . $OPCODE . "\n";
	// AAビットの取り出し
	$AA = $ret['flag1'] & 0x04;
	if (!$AA){
		echo 'Non-authoritative' . "\n";
	} else {
		echo 'Authoritative' . "\n";
	}
	//TCビット
	$TC = $ret['flag1'] & 0x02;
	if ($TC){//切り落し-伝送チャンネル上に最大長よりメッセージが長くなったためこのメッセージが切り落とされたことを示します。
		echo 'TrunCation On' . "\n";
		//とりあえず、対応しないので、ブレークする。
		break;
	} else {
		$wait = false;
	}
	
	//RD
	$RD = $ret['flag1'] & 0x01;
	if ($RD){// 再帰要望
		echo 'Recursion Desired' . "\n";
	}
	
	
	$RA = $ret['flag2'] & 0x80;
	if ($RA){// 再帰可能
		echo 'Recursion Available' . "\n";
	}
	//将来のために予約されたエリアで常に0,0以外なら何かが定義されている
	$Z = $ret['flag2'] & 0x70;
	if ($Z){
		echo 'Reserved for future use : ' . $Z . "\n";
	}
	// Response code.
	$RCODE =  $ret['flag2'] & 0x0f;
	switch($RCODE){
		case 0://No error condition
			break;
		case 1:
			echo 'Format error - The name server was unable to interpret the query.' ."\n";
			break;
		case 2:
			echo 'Server failure - The name server was unable to process this query due to a problem with the name server.' . "\n";
			break;
		case 3:
			echo 'Name Error - Meaningful only for responses from an authoritative name server, this code signifies that the domain name referenced in the query does not exist.' . "\n";
			break;
		case 4:
			echo 'Not Implemented - The name server does not support the requested kind of query.' . "\n";
			break;
		case 5:
			echo 'Refused.' . "\n";
			break;
		default:
			echo 'Reserved for future use.' . "\n";
	}
	// QDCOUNT
	echo 'Question Count: ' . $ret['other1'] . "\n";
	//ANCOUNT
	echo 'Answer Count: ' . $ret['other2'] . "\n";
	// NSCOUNT         an unsigned 16 bit integer specifying the number of name
    echo 'Server resource records: ' . $ret['other3'] . "\n";
	// ARCOUNT         an unsigned 16 bit integer specifying the number of
    echo 'Resource records in the additional records: ' . $ret['other4'] . "\n";
	
	//
}
socket_close($sock);
exit(0);

function createQueryARecord($id, $host){
	

//DNSヘッダフォーマット
// ID (16)		DNSのトランザクションID。クエリ時に指定し、応答パケットにコピーされて戻る。
/*
次の16bitは、
QR (1)	問い合わせが0、応答が1 
OPCODE (4)	問い合わせの種類を指定する。0が通常のクエリ、0 => 問い合わせ、1 => 逆問い合わせ、2 => サーバ状態要求
AA (1)	管理権限がある応答であることを示す
TC (1)	パケット長制限などで応答が切り詰められていることを示す
RD (1)	名前解決を要求するビット。0は権威DNSサーバへの問い合わせで、1はフルサービスリゾルバへの問い合わせ

RA (1)	名前解決可能であることを示す
Z (1)	将来のために予約。常に0とする
AD (1)	DNSSEC検証に成功したことを示す(応答)/応答のADビットを理解できることを示す(問い合わせ)
CD (1)	DNSSEC検証の禁止
RCODE (4)
*/

// QDCOUNT (16)	Questionセクションの数で、通常は1
// ANCOUNT (16)	Answerセクションのリソースレコード(RR)数
// NSCOUNT (16)	AuthorityセクションのRR数
// ARCOUNT (16)	AdditionalセクションのRR数


// Aレコード問い合わせは
// IDは任意でよく、通常のクエリであるから、ヘッダは以下のようにすればよい。
// 名前解決を要求する場合は、RDビットを1にする。そのため、以下のようなHeaderセクションとなる。
// 0x00,0x00,0x01(RDビットを1に),0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00

	$data = pack('n', $id);//ID 16bit
	//フラグ16ビットのセット
	$data .=  pack('C*',0x01, 0x00);//RDを1にセットし、それ以外は0でフラグをセット
	
	//	QDCOUNT 	Questionセクションの数で、通常は1
	$data .=  pack('C*', 0x00, 0x01);
	
	//ANCOUNT, NSCOUNT, ARCOUNT,
	$data .= pack('C*', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
	
	$div = explode('.', $host);
	
	foreach($div as $v){
		$len = strlen($v);
		if ($len<=0){
			echo 'invalid host name: ' . $host . "\n";
			exit(1);
		}
		$data .= pack('C*', $len) . $v;
	}
	$data .= pack('C', 0x00);//ホスト名の最後にnullセット
	
	// A IN をセット
	$data .= pack('C*', 0x00,0x01,0x00,0x01);
	
	return $data;
}