0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【第4回】海外からのスパム爆撃を水際遮断!GeoIPを利用した日本国内限定アクセス制御

0
Posted at

「公開初日、日本語が一切通じない海外カジノ宣伝ボットが秒間50件の投稿を始めた」
令和に自作した2ちゃんねる風掲示板「つゔぁいちゃんねる(2ch.biz)」開発秘話の第4弾。今回は、サイト公開直後に必ず直面する「海外ボットによるスパム絨毯爆撃」を、CAPTCHAを使わずに水際で99%遮断したGeoIPアクセス制御エンジンの全貌を解説します。


💡 はじめに:個人サイトを公開すると何が起きるのか?

「個人でWebサービスや掲示板を作って、ドメインを取ってインターネットに公開した!」
開発者として最も興奮する瞬間ですが、その数時間後には インターネットの暗部 を思い知らされることになります。

Googleにすらまだ登録されていないはずの新規ドメインに対し、世界中の悪質なクローラーやボットネットがIPレンジを総当たりで巡回し、フォームを見つけては以下のような書き込みを絨毯爆撃してきます。

  • 海外オンラインカジノの誘導URL
  • 暗号資産(仮想通貨)詐欺グループへのTelegramリンク
  • 違法薬物・偽ブランド品の販売サイトリンク

これらはすべてロシア、東欧、北米、東南アジアなどの海外ホスティングサーバーや踏み台プロキシから自動化スクリプトで送信されています。

「CAPTCHA(画像認証)を導入すれば止まるのでは?」
確かに止まります。しかし、歪んだ英数字を読ませたり「信号機を選んでください」と要求するCAPTCHAは、 書き込みたい一般ユーザーの熱量を瞬時に冷え切らせる最悪のUX です。

2ちゃんねるの「思い立ったら3秒で書き込める手軽さ」を殺さずに、海外ボットだけを物理的に弾くにはどうすればいいか?
その答えが、開発初期(2026年8月17日)に導入した 「GeoIP 日本国内限定アクセス制御」 でした。


🛡️ GeoIP多段階判定アーキテクチャ

単に外部のGeoIP APIを毎回叩いていては、APIのレートリミットに達するか、レスポンスが数百ミリ秒遅延してサイト全体が重くなってしまいます。
そこで、 「オーバーヘッドゼロの環境変数優先 + DBキャッシュ」 の3段構えパイプラインを設計しました。

graph TD
    A[リクエスト受信] --> B{プライベートIP / ローカル?}
    B -- Yes --> C[✅ アクセス許可 (開発環境保護)]
    B -- No --> D{Cloudflare / サーバーGeoIPヘッダー?}
    
    D -- あり (JP) --> E[✅ アクセス許可 (オーバーヘッド 0ms)]
    D -- あり (海外) --> F[🚫 403 Forbidden 遮断]
    D -- なし --> G{DBキャッシュ (geoip_cache) に存在?}
    
    G -- ヒット (JP) --> E
    G -- ヒット (海外) --> F
    G -- 未キャッシュ --> H[外部GeoIP API照会 / MMDB]
    H --> I[キャッシュDBへ結果保存]
    I --> J{判定結果は日本?}
    J -- JP --> E
    J -- 海外 --> F

1. ローカル・開発環境の自動バイパス

開発中のローカルマシン(127.0.0.1, ::1)や社内LAN(192.168.0.0/16, 10.0.0.0/8)からのリクエストは、GeoIP判定を無条件にスキップして開発作業を阻害しません。

2. リバースプロキシ・CDNヘッダーの最優先判定(オーバーヘッド 0ms)

CloudflareやLiteSpeed Web Serverなどのインフラを通過している場合、WebサーバーがすでにIPアドレスから国コードを判定し、$_SERVER['HTTP_CF_IPCOUNTRY'] や $_SERVER['GEOIP_COUNTRY_CODE'] などの環境変数にセットしてくれています。
これを最優先で参照することで、 DBクエリすら1回も発行せずにゼロ遅延で遮断 できます。

3. SQLite/MySQLによる判定結果キャッシュテーブル

環境変数が利用できない場合でも、IPアドレスをキーに自前のキャッシュテーブルへ結果を保存(TTL付き)することで、同一IPからの2回目以降のリクエストは数マイクロ秒で判定を完了させます。


💻 判定エンジンの実装(api.php 抜粋)

class GeoBlocker {
    private $db;
    private $enabled = true;

    public function __construct($pdo, $enabled = true) {
        $this->db = $pdo;
        $this->enabled = $enabled;
    }

    public function checkAccess($clientIp) {
        if (!$this->enabled) return true; // 機能OFFならスルー

        // 1. プライベートIP / ループバックは許可
        if ($this->isPrivateIp($clientIp)) {
            return true;
        }

        // 2. サーバー環境変数のチェック (Cloudflare等)
        $countryCode = $this->getCountryFromHeaders();
        if ($countryCode !== null) {
            return ($countryCode === 'JP');
        }

        // 3. DBキャッシュの照会
        $countryCode = $this->getCountryFromCache($clientIp);
        if ($countryCode === null) {
            // 4. 外部API / MMDB照会
            $countryCode = $this->lookupCountryCode($clientIp);
            $this->saveCache($clientIp, $countryCode);
        }

        return ($countryCode === 'JP');
    }

    private function getCountryFromHeaders() {
        if (!empty($_SERVER['HTTP_CF_IPCOUNTRY'])) {
            return strtoupper($_SERVER['HTTP_CF_IPCOUNTRY']);
        }
        if (!empty($_SERVER['GEOIP_COUNTRY_CODE'])) {
            return strtoupper($_SERVER['GEOIP_COUNTRY_CODE']);
        }
        return null;
    }

    private function lookupCountryCode($ip) {
        // ip-api.com 等の軽量JSON API(またはローカルMaxMind DB)
        $ctx = stream_context_create(['http' => ['timeout' => 1.5]]);
        $json = @file_get_contents("http://ip-api.com/json/{$ip}?fields=countryCode", false, $ctx);
        if ($json) {
            $data = json_decode($json, true);
            if (!empty($data['countryCode'])) {
                return strtoupper($data['countryCode']);
            }
        }
        // ルックアップ失敗時は利用者をブロックしないよう安全側に倒す (JP扱い)
        return 'JP'; 
    }
}

工夫したポイント:フェイルオープン(安全側に倒す)

外部APIのダウンやタイムアウトが発生した際、厳格にしすぎて「日本の一般ユーザーを誤って遮断する」事態は絶対に避けなければなりません。ルックアップが失敗した場合はフォールバックとして JP(許可)を返し、サービス停止事故を防ぐフェイルオープン設計にしています。


⚙️ 管理画面での運用と「IPシミュレーター」

GeoIP制限を導入する際、最も怖いのが「誤遮断(False Positive)」です。
「本当に正しく判定できているのか?」「特定のIPが弾かれていないか?」を確認できるよう、管理画面に 専用のIPテスターツール を組み込みました。

<!-- admin.html のGeoIP設定コンポーネント -->
<div class="card admin-section">
    <h3>🇯🇵 日本国外アクセス制限 (Geo-blocking)</h3>
    <label class="toggle-switch">
        <input type="checkbox" id="geoBlockToggle">
        <span class="slider"></span>
        日本国外(海外IP)からのアクセス・投稿を完全に遮断する
    </label>

    <div class="ip-tester-box">
        <h4>🔍 IP判定シミュレーター</h4>
        <input type="text" id="testIpInput" placeholder="例: 8.8.8.8 や 133.242.x.x">
        <button id="btnTestIp" class="btn btn-secondary">判定テスト</button>
        <div id="testIpResult" class="test-result-badge"></div>
    </div>
</div>

管理者は、怪しい投稿者のIPアドレスをこのテスターに入力するだけで:

  • IP: 8.8.8.8 → 国: US (アメリカ) → ❌ 403 遮断
  • IP: 133.242.xxx.xxx → 国: JP (日本) → ✅ アクセス許可
    という結果を即座に視覚的にテストできます。

📊 導入後の劇的な効果

このGeoIPブロックを導入した瞬間、掲示板のログに驚くべき変化が現れました。

指標 導入前 導入後
海外スパムボットの書き込み数 日に数百〜千件 完全ゼロ(0件)
不要な404/探索リクエスト サーバーログの約60% ゲートウェイ層で即403破棄
一般ユーザーの投稿UX CAPTCHAなし(スパムだらけ) CAPTCHAなし(快適・治安維持)

日本語圏のコミュニティである以上、ターゲットユーザーの99%以上は日本国内からのアクセスです。
海外IPを水際で弾くだけで、複雑なAIフィルターや重厚なWAFを導入せずとも、 驚くほどあっけなくスパム問題の大部分が解決 してしまいました。


🎯 まとめと次回予告

第4回となる今回は、掲示板の治安を最小のコストと最高のUXで守る 「GeoIPアクセス制御」 の設計と実装について解説しました。

今回の学び・ポイント

  • CAPTCHAに頼らない治安維持 :海外IPを遮断するだけで、スパムの9割以上は消滅する。
  • 3段構えパイプライン :CDN環境変数を最優先し、DBキャッシュを挟むことで判定遅延をゼロにする。
  • フェイルオープンとテスト機能 :API障害時でも一般ユーザーを弾かない安全設計と、管理画面での検証ツールが運用を支える。

次回、 【第5回】板の乱立と混沌を防ぐ!管理画面からのカテゴリ作成と板の階層ツリー管理 へと続きます!


🔗 関連リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?