IPアドレスから取得したホスト名が信頼できるか、DNS正引きを行って検証する


## 概要

PHPでは、gethostbyaddr関数を利用することでDNS逆引きを行い、IPアドレスからホスト名を取得できます。

[PHP: gethostbyaddr - Manual](http://php.net/manual/ja/function.gethostbyaddr.php)

しかし、ここから返ってきたホスト名は信頼できるものではありません。
そもそもDNS逆引きの結果が信頼できるものではなく、このホスト名が正しいものかDNS正引きを行って検証する必要があります。

[逆引き - Wikipedia](https://ja.wikipedia.org/wiki/%E9%80%86%E5%BC%95%E3%81%8D#.E9.80.86.E5.BC.95.E3.81.8D_.28DNS.29)

<blockquote cite="https://ja.wikipedia.org/wiki/%E9%80%86%E5%BC%95%E3%81%8D#.E9.80.86.E5.BC.95.E3.81.8D_.28DNS.29"><ul> <li>逆引きを設定していないIPアドレスも存在する。</li> <li>逆引きで返ってくる結果は所詮は自称にすぎないので、信頼性をそれなりにでも確保するためには、得られた結果をもう一度<a href="https://ja.wikipedia.org/wiki/%E6%AD%A3%E5%BC%95%E3%81%8D" title="正引き">正引き</a>して、元のIPアドレスと合致するか確かめる必要があるだろう。 <ul> <li>つまりドメイン名を正引きして得たIPアドレスに対して、逆引きして得たドメイン名は一致しないこともある。</li> </ul> </li> <li><a href="https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%8D%E3%83%83%E3%83%88%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%83%97%E3%83%AD%E3%83%90%E3%82%A4%E3%83%80" title="インターネットサービスプロバイダ">ISP</a>等によっては、クライアントホストのIPアドレスの逆引きに、アクセスポイント名が含まれるドメイン名を設定しているところもある。</li> </ul></blockquote>

この投稿では、このDNS正引きを行い、ホスト名が信頼できるものなのか検証する実装を紹介します。

## 処理

PHPには、ホスト名からDNS正引きを行い、IPアドレスの一覧を取得するgethostbynamel関数が存在します。

[PHP: gethostbynamel - Manual](http://php.net/manual/ja/function.gethostbynamel.php)

この関数はIPアドレスの配列を返します。そのため、in_array関数で結果の配列に元のIPアドレスが含まれているか検証し、含まれていればそのホスト名が信頼できるものであると判断できます。

[PHP: in_array - Manual](http://php.net/manual/ja/function.in-array.php)

ただし、gethostbynamel関数はIPv4のIPアドレスを返してきます。

[PHP: gethostbynamel - Manual](http://php.net/manual/ja/function.gethostbynamel.php#refsect1-function.gethostbynamel-returnvalues)

<blockquote cite="http://php.net/manual/ja/function.gethostbynamel.php#refsect1-function.gethostbynamel-returnvalues"><p> IPv4 アドレスの配列を返します。 もし <code>hostname</code> が解決できなかった場合は <strong><code>FALSE</code></strong> を返します。 </p></blockquote>

このため、IPv6のIPアドレスではgethostbynamel関数を利用することはできません。
そこで、代わりにdns_get_record関数を利用します。

[PHP: dns_get_record - Manual](http://php.net/manual/ja/function.dns-get-record.php)

第二引数にDNS_AAAA定数を指定することで、DNSレコードのうち、IPv6のIPアドレスを持つAAAAレコードのみを取得します。
CNAMEレコードによって定義された別名(エイリアス)は解決されるようです[^CNAMEレコードのエイリアスは解決される]。

[^CNAMEレコードのエイリアスは解決される]: IPv4のアドレスを持つ私のドメインで確認しました。

## コード

以上をふまえたコードが以下になります。

```php
/**
 * @param string $hostname ホスト名
 * @param string $ip_address ホスト名に対応する(はずの)IPアドレス
 * @return bool ホスト名が信頼できる場合はtrue、そうでない場合はfalseを返します
 */
function is_reliable_host ($hostname, $ip_address)
{
	$ip_list = false;

	if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
		/**
		 * IPv6
		 */

		$records = @dns_get_record($hostname, DNS_AAAA);
		if ($records) {
			$ip_list = array_column($records, 'ipv6');
		}
	}　elseif (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
		/**
		 * IPv4
		 */

		$ip_list = gethostbynamel($hostname);
	}

	return ($ip_list && in_array($ip_address, $ip_list, true));
}
```

なお、上記のコードではPHP 5.5から追加されたarray_column関数を利用しています。

[PHP: array_column - Manual](http://php.net/manual/ja/function.array-column.php)

このため、PHP 5.5未満には対応していません。
PHP 5.5未満に対応する場合、代わりに以下のコードを利用して下さい。

```php:PHP&#32;5.5未満対応
/**
 * @param string $hostname ホスト名
 * @param string $ip_address ホスト名に対応する(はずの)IPアドレス
 * @return bool ホスト名が信頼できる場合はtrue、そうでない場合はfalseを返します
 */
function is_reliable_host ($hostname, $ip_address)
{
	$ip_list = false;

	if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
		/**
		 * IPv6
		 */

		$records = @dns_get_record($hostname, DNS_AAAA);
		if ($records) {
			$ip_list = array_map(function ($v) { return $v['ipv6']; }, $records);
		}
	}　elseif (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
		/**
		 * IPv4
		 */

		$ip_list = gethostbynamel($hostname);
	}

	return ($ip_list && in_array($ip_address, $ip_list, true));
}
```

### 使い方

以下のように利用します。

```php:例
/**
 * アクセスしてきたユーザのIPアドレス
 */
$REMOTE_ADDR = $_SERVER['REMOTE_ADDR'];

/**
 * DNS逆引きにより、IPアドレスからホスト名を取得
 */
$HOST_NAME = gethostbyaddr($REMOTE_ADDR);

/**
 * 取得したホスト名が信頼できるか検証
 */
if (is_reliable_host($HOST_NAME, $REMOTE_ADDR)) {
	/**
	 * 信頼できる場合
	 */

	// ...
} else {
	/**
	 * 信頼できない場合
	 */

	// ...
}
```