倉金家ホームページ

趣味の部屋/ホームページ余話

逆引エラーを自動で調べる

2013年6月19日 2020年1月1日 更新
ホームページのアクセスログを見ていますと逆引きができていないアクセスが時折目につきます。都度whoisコマンドや whois lookupサービスなどで確認していましたが面倒なので自動的にwhois情報を検索してアクセスログに記録されるようにしてみたところ、たいへんわかりやすくなりました。
ビジネスサイトなどではホスト名を隠したライバル企業の閲覧、顧客となりうる企業のページ閲覧なども一目瞭然。もしまだでしたらぜひやるべきでしょう。


 このサイトでは自前のアクセスログにアクセスを記録し、多少情報処理して一目でどんなアクセスがあったかがわかるようにしています。 → (公開用)アクセスログ
どんな記事が読まれているかということと併せて、どんな人が読んでいるかも気になるところです。またアタックやスパムのような挙動も逆引き設定されていないホストに多い気がします。
そう思って見ますと逆引き設定されていないホストからのアクセスがどんなアクセスなのか全くわからず、気になるときは都度whoisコマンドを打ったり "http://whois.domaintools.com/" のようなwhois loookupサービスサイトを利用して確認したりしていましたが、サーバー管理上必要とはいえいちいち面倒と言えば面倒です。

 そこで、逆引きできないホストに対しては自動的にwhois情報を検索してIPアドレスの所有者と国をログに記録されるようにしたところ、たいへんわかりやすくなりました。

たとえば、ホスト名なしで単独に本来ないはずの内部パラメータを持ってよく来る不可解なアクセスにIPアドレス 103.246.39.212 があります。
whoisコマンドで調べますとたくさんの項目が表示されます。
# whois 103.246.39.212
[Querying whois.arin.net]
[Redirected to whois.apnic.net]
[Querying whois.apnic.net]
[whois.apnic.net]
........

inetnum: 103.246.39.0 - 103.246.39.255
netname: BLUECOAT-CS-AP
descr: Blue Coat Systems Inc
country: JP
admin-c: ........
tech-c: ........
status: ........
....... ........

 この中で特にほしい情報は黄色マーカーしたIPアドレスの所有者名と国コードです。これを抽出しなくてはなりません。
こうして抽出したものを見ればこのアクセスはセキュリティー会社のおそらく確認のためのアクセスで、アタックやスパムアクセスなどではなさそうだとわかります。
さらに上記例は日本を含むアジア・太平洋地域のwhois管理組織からのもので、アドレスによっては海外の他の地域の場合もあり、問題は各々の管理組織で書式が異ることです。

 とりあえずこれらを考慮し、さほど厳密でなくてもいいことにして、phpで抽出スクリプトを書いてみました。
ここではサーバーにwhoisコマンドがインストールされていることが前提です。whoisコマンドは使い慣れてるもので。
ただまれに応答に長時間かかったり、ときに応答しないサーバーがありtimeout設定は必須です。
<?php

function access_whois($ip)
{
.........
$whois = shell_exec("timeout 5 whois ".escapeshellarg($ip));
.........
} …その後コードを改良したのでページ最後に載せておきます。

?>
そして、
// REMOTE_HOSTが空または有効なホスト名でない(IPアドレスの場合)ならwhoisを引く。
$host = (empty($_SERVER['REMOTE_HOST']) || !mb_eregi('[a-z]', $_SERVER['REMOTE_HOST'])) ?
access_whois($_SERVER['REMOTE_ADDR']) : $_SERVER['REMOTE_HOST'];
として$hostをアクセスログに記録します。
逆引きがちゃんと設定されていれば逆引き結果が、設定されていなければwhois情報(IP所有者名)(国コード)がログに記録されます。

 ごくまれにwhois情報すら設定されていなかったり独自の書き方のためうまく情報がとれないこともありますが、現在のところこれで充分用は足りています。
→逆引未設定アクセスのみ表示してみたアクセスログ(ただし現時点での)
 …現在は日本の情報は日本語でとれるようになっています(要点のみ後述)。

 欠点はwhois情報を引くのに少し時間がかかり(場合によっては数秒以上)その分ページの表示が遅くなりますが、ちゃんと逆引き設定していないアクセスには少し待ってもらいましょう。

 しばらくこれで運用してみましたが、海外からのアクセスはともかく日本のアクセスでも意外と多くの企業や公共機関、学校などにも逆引きが設定されていないことがあり(マナーではあっても必ずしも規定ではないからね)、こんなところからもアクセスがあったのか〜と日々アクセスログを見て楽しんでいる今日このごろです。
でも身元を隠してアクセスするようなところってなんか胡散臭いなあという気がするのは私だけでしょうか。

ページの最後に今回検討したphpのコードを載せてあります。

少し改良、キャッシュを利用 (2014.3.15 追記)
 whoisコマンドによるwhois情報の取得には少し時間がかかるようです。(whoisサーバーの応答が遅いと数秒とか。)
さらに逆引き設定のされていない海外のあやしい情報収集アクセスほど連続的にやたらと多くのページにアクセスしてくることが多く、そのたびごとにwhoisを引いていたのではサーバーが疲れますし他のまともなアクセスが待たされます。
 そこで、whois情報を独自にキャッシュして一度引いたら以後キャッシュを使って対応するようにしてみました。
またこうするとアクセスログとは単独にキャッシュファイルを見るだけで逆引き未設定ホストの情報を見ることもできます。
(参考)
 whoisはCentOS6のjwhoisパッケージを使っていますが、なぜだかバージョンによって問い合わせるwhoisサーバーが変わる場合があります。実はバージョンアップによって今まで所有者名がとれていたのがその上のネットワーク会社名になってしまったので気がつきました。
IPアドレスと問い合わせるwhoisサーバーなどの設定ファイルは /etc/jwhois.conf ですが、調べて修正する気もありませんし、互換性を保ちたいこともあって今も古い設定ファイルをそのまま使っています。

データを見やすくリスト表示するようにしてみた (2014年5月6日 追記)
 その後ついでに正引きできないアクセスなども記録するようにし、さらにキャッシュファイルを多少国別に分けたりもして見やすくリスト表示するようにしてみました。
 必ずしも逆引き設定は規程ではないようなのでだからどうということもないと言えばそれまでなのですが、言ってみれば顔を隠して他人の家を訪問するようなものです。規程とマナーはちがいますからね。できればちゃんと設定してほしいと思うのでデータをあえて公表します。
→ホスト名が不完全なアクセスのリスト(日本に限らず)
→日本のホスト名が不完全なアクセスのリスト(日本のアクセスのみ抽出)
…このリストを海外のあやしい情報収集ロボットらしきものが大量に、毎日毎日、幾度も幾度も、必死に見にきます。...いったいなんなんなんだ?...あまりにすさまじいアクセスですぐにログが埋まってしまうので普通のアクセスとはログを別にしました。

 しばらく運用してみるとかなりの逆引きを設定していないアクセスがあり、さらには名のある大企業、大学、公官庁などけっこうなところからのアクセスもずいぶんあります。身元隠しのつもりで逆引き設定しないなどというのは逆効果。こんな簡単な方法で調べられてかえって目立つというもの。
さらには正引すらちゃんとできていないIT企業も意外と多いのですが、こんなところに頼んでだいじょうぶなのかなと思ってしまうのでした。

(2015年3月20日 追記) さらに改良、日本語名を自動でとれるようにしてみた。
(2020年1月1日 追記の追記)サーバーをレンタルサーバーhetemlに移動したところwhoisの言語設定が英語のため、残念ながら日本語名がとれなくなりまたまた改造。

(以下昔の記録として。)
日本のアクセスについては自動で日本語のwhoisも検索して日本語名もとるように改良してみました。
CentOS6では英語がディフォルト。(CentOS7ではそのままで日本語になっています。)
さらにphpのcURLでwhoisサーバーから日本語のwhoisをとるようにしました。
日本語名が必ずとれるとは限らないけど、とれれば上記のリストに反映されています。

日本語でwhois情報をとるのは少しコツがいりましたのでメモしておきます。
・CentOS6では日本語をとるためには/etc/jwhois.confの変更が必要。
 230行目あたりと1040行目あたり、query-formatの英語指定の"/e"を削除。
       query-format = "$*"; ←(変更)← query-format = "$*/e";
・システムにもよるが言語の環境変数"LANG"のexportとnkfによる文字コード変換が必要。両方やっておけば間違いないようだ。
 英語の場合:
 $whois = shell_exec("timeout 5 whois -h whois.nic.ad.jp ".escapeshellarg("$ip/e"));
 $name = mb_ereg('.*\n.*?\[Organization\]\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
 日本語:
 $whois = shell_exec("export LANG=ja_JP.UTF-8 ; timeout 5 whois -h whois.nic.ad.jp ".escapeshellarg($ip)." | nkf -w8");
 $name = mb_ereg('.*\n.*?\[組織名\]\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
・システムのwhoisが英語の設定で変えられない場合、phpのcURL関数を使ってwhois.nic.ad.jpよりデータをとればよい。

 すべての日本のアクセスについてwhoisをとりたくもなりましたが、サーバーに余計な負荷がかかり、さらにはお客さんを待たせることにもなりますのでそこまではやりません。

以下、whois情報をとってメモリおよびファイルにキャッシュ処理をするコードです。
 データを拾うためのreg関数はmb系を使用していますが、単にpreg系より使い慣れているというだけの話。
各whois組織の書式をもう少し詳細に検討すればさらに正確なデータが得られるとは思いますが、今のところこれで充分役に立っています。

アクセスのwhoisをとって名前と国を抽出するコード:
//// IPのwhois情報を調べて所有者、国名を得る。IPv6は非対応。
function access_whois($ip)
{
  // メモリキャッシュ。複数回のページ呼び出しに対応。
  static $m_cached_ip = NULL, $m_cached_whois = '';
  if($ip === $m_cached_ip){ return($m_cached_whois); } // キャッシュしていればそれを返す。

  // 外部に問い合わせてはいけないIP。IPの形式をなしていないのやローカルのIPなど。
  if(!mb_ereg('^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$', $ip)){ return(''); } // not IP
  if($ip == '127.0.0.1'){ return('(localhost)'); } // local loopback
  if(mb_ereg('^192\.168\.', $ip)){ return("($ip-somehost.localdomain)"); } // local LAN

  // ファイルキャッシュ。複数のページアクセスに対応。
  $cache_file = 'whois_cache.inc.php'; // キャッシュファイルの指定。
  // キャッシュファイルがまだない場合空のデータでつくる。
  if(!file_exists($cache_file))
  {
    file_put_contents($cache_file, '<?php
$ip_whois = array(
);
?>');
  }
  $max_f_cache = 1000; // キャッシュ件数。サイトのアクセス量とサーバーパワーに応じて適当に。
  include($cache_file);
  if(isset($ip_whois))
  {
    foreach($ip_whois as $f_cached_ip => $f_cached_whois)
    {
      if($f_cached_ip === $ip){ return($f_cached_whois); } // キャッシュがあればそれを返す。
    }
  }
  else
  {
    $ip_whois = array();
  }

  // キャッシュはなかった。whoisをとる。システムによっては日本語をとるのにexport LANGとnkfが必要。
  if(!($whois = trim(shell_exec("export LANG=ja_JP.UTF-8 ; timeout 5 whois ".escapeshellarg($ip)." | nkf -w8")))
  || mb_eregi('ERROR ', $whois))
  {
    return(''); // whoisがうまくとれなかった。
  }

  $name = $name_jp = $country = '';
  if(mb_ereg('JPNIC database', $whois)) // whois.nic.ad.jp
  {
    if(!($name_jp = mb_ereg('.*\n.*?\[組織名\]\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : ''))
    {
      $whois = getjpwhois_byip($ip); // 日本語でなかったらとりなおす。
      $name_jp = (mb_ereg('.*\n.*?\[組織名\]\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '');
    }
    $name = mb_ereg('.*\n.*?\[Organization\]\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
    $country = 'jp';
  }
  elseif(mb_ereg('\[whois\.apnic\.net\]', $whois)) // whois.apnic.net
  {
    $name = mb_eregi('.*?\ndescr:\s+([^\n]*(?:co|ltd|inc|net|service)[^\n]*)\n', $whois, $regs) ? $regs[1]
      : (mb_ereg('.*?\ndescr:\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '');
    $country = mb_ereg('.*?\ncountry:\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
    if($country == 'JP' || $country == 'jp') // 日本の場合はさらに詳細に調査。
    {
      $whois = getjpwhois_byip($ip); // この場合最初から日本語でとる。
      $name_jp = (mb_ereg('.*\n.*?\[組織名\]\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '');
      $name = mb_ereg('.*\n.*?\[Organization\]\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
    }
  }
  elseif(mb_ereg('RIPE Database|AfriNIC Whois server', $whois)) // whois.ripe.net, whois.afrinic.net
  {
    $name = mb_eregi('.*\ndescr:\s+([^\n]*(?:co|ltd|inc|net|service)[^\n]*)\n', $whois, $regs) ? $regs[1]
      : (mb_ereg('.*?\ndescr:\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '');
    $country = mb_ereg('.*?\ncountry:\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
  }
  elseif(mb_ereg('LACNIC resource: whois\.lacnic\.net|Brazilian resource: whois\.registro\.br', $whois))
  { // whois.lacnic.net, registro.br; whois.nic.br,whois.registro.br同じ。
    $name = mb_ereg('.*\nowner:\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
    $country = mb_ereg('.*?\ncountry:\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
  }
  elseif(mb_ereg('KRNIC WHOIS Service', $whois)) // whois.krnic.net, whois.nic.or.kr...同じ。
  {
    $name = mb_ereg('.*\nOrganization Name\s+:\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
    $country = 'kr';
  }
  elseif(mb_ereg('ARIN WHOIS data and services', $whois)) // whois.arin.net
  {
    $name = mb_ereg('.*\nOrgName:\s+([^\n]+)\n', $whois, $regs) ? $regs[1]
      : (mb_eregi('.*?\n([^\[#\s][^\n]+(?:Inc|Ltd)\s*(?:[^\(]+\s*)*)', $whois, $regs) ? $regs[1]
        : (mb_ereg('.*?\n([^\[#\s](?:[^(]+\s*)+)', $whois, $regs) ? $regs[1] : ''));
    $country = mb_ereg('.*\nCountry:\s+([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
  }

  // 上記にないサーバー、書式が合わない、記載がないなどでとれないときの最後のトライ。
  if(!$name)
  {
    $name = mb_eregi('.*?(?:owner|name):\s*([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
  }
  if(!$country)
  {
    $country = mb_eregi('.*?(?:country|Country-Code):\s*([^\n]+)\n', $whois, $regs) ? $regs[1] : '';
  }
  $name = trim($name);
  $country = trim($country);

  // whois情報が得られればキャッシュに保存。まれにwhoisが引けずERRORする。
  // 国コードは必ずしも取れなくてもよしとする。書かれてないこともある。
  if($name)
  {
    // メモリキャッシュ作成
    $m_cached_ip = $ip;
    $m_cached_whois = "($name)";
    if($country){ $m_cached_whois .= "($country)"; } // 逆引きデータと区別しやすいよう()で囲む。
    if($name_jp){ $m_cached_whois .= ", [$name_jp][日本]"; } // 区切りの","は\nでも<br>でも。
    // ファイルキャッシュ用データ作成。
    $new_ip_whois = '<?php
$ip_whois = array(';
    $new_ip_whois .= "
'$m_cached_ip'\t=> '$m_cached_whois',"; // 検索が速いように新しいのを頭に。
    $count = 1;
    foreach($ip_whois as $f_cached_ip => $f_cached_whois)
    {
      if($count++ > $max_f_cache){ break; }
      $new_ip_whois .= "
'$f_cached_ip'\t=> '$f_cached_whois',";
    }
    $new_ip_whois .= '
);
?>
';
    // ファイルキャッシュに保存。
    if($fp = fopen($cache_file, 'c+'))
    {
      flock($fp, LOCK_EX);
      ftruncate($fp, 0);
      fwrite($fp, $new_ip_whois);
      fflush($fp);
      flock($fp, LOCK_UN);
      fclose($fp);
    }
    return($m_cached_whois);
  }
  else
  {
    return('');
  }
}

// hetemlのサーバーではwhoisコマンドで日本語whoisがとれないので日本語でとる関数を別途作成。
function getjpwhois_byip($ip){
  $url = 'https://whois.nic.ad.jp/cgi-bin/whois_gw?key='.$ip;
  $ch = curl_init($url);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 返り値を出力せず文字列で返す。
  curl_setopt($ch, CURLOPT_BINARYTRANSFER, true); // 出力結果を何も加工せずに返す。
  curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3); // 接続試行待ち秒数。
  curl_setopt($ch, CURLOPT_TIMEOUT, 6); // cURL関数実行にかけられる時間の最大値。
  $output = curl_exec($ch);
  curl_close($ch);
  $output = strip_tags(mb_convert_encoding($output, 'UTF-8', 'SJIS'));
  return($output);
}

?>