Linux環境でCoovaChilliとFreeRADIUSを用いたセキュアなゲストWi-Fi向けキャプティブポータルを構築する

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

土曜日の午前2時15分、私のポケットベルが鳴り響きました。ある地方拠点のオープンな/23ゲストサブネットに無許可の個人端末が殺到し、わずか20分足らずで全508件のDHCPリースが枯渇してしまったのです。

さらに悪いことに、制御不能となったブロードキャストノイズがアップストリームスイッチを圧迫し、社内DNSリゾルバーを機能不全に追い込みました。厳格なLayer 2/3分離が行われていないオープンなゲストネットワークは、まさに時限爆弾です。境界で不正なトラフィックを確実に阻止するには、ルーティング処理が行われる前にトラフィックをインターセプトし、認証ハンドシェイクを強制した上で、セッションごとの厳格な帯域制限を適用できるアクセスゲートウェイが必要です。

ゲストゲートウェイアーキテクチャの比較・評価

高負荷下でパブリックなゲストトラフィックを分離する際、エンジニアは通常、次の3つのパターンのいずれかを選択します。

アーキテクチャ リダイレクト機構 アカウンティング & レートリミット 運用のトレードオフ
カスタム iptables / nftables + Webアプリ 動的ipsetリストを用いたDNATルール Linux標準のtc (Traffic Control) キューとcronスクリプトの手動管理 conntrackステートが破綻しやすく、接続の激しい変動下でメモリリークが発生しやすい。
組み込みアプライアンス (pfSense / OpenWrt) 標準搭載のポータルエンジン (Nodogsplash、pfSense captiveportalなど) ローカルSQLiteデータベースまたは標準RADIUSクライアントモジュール アプライアンスのハードウェア性能に依存。同時接続ゲスト数が300〜400台を超えるとクラッシュする傾向がある。
Linux上の CoovaChilli + FreeRADIUS ネイティブUAMインターセプトを備えたカーネルTUN/TAPドライバ (tun0) RFC 2866 RADIUSアカウンティング、WISPrレート属性、動的セッションタイムアウト デーモンの手動チューニングが必要だが、一般的な汎用ハードウェアでも2,500以上の同時アクティブセッションを難なく処理可能。

素のiptablesルールを組み合わせる手法は、机上では手軽に見えます。しかし実際には、コネクショントラッキングの状態不整合によってセッションが瞬く間に破損し、アウトバウンドのHTTPS接続をインターセプトしようとすればブラウザのTLSセキュリティ警告が引き起こされます。

専用アプライアンスも、カンファレンスの基調講演などのピーク時におけるアクセス殺到には耐えきれません。CoovaChilliはこれらの課題をすべて解決します。アクセス制御を専用のTUNインターフェースにオフロードし、認証処理をFreeRADIUSに委譲することで、標準的なLinuxハードウェア上でキャリアグレードのセッション分離を実現します。

本番運用のトレードオフ:CoovaChilli + FreeRADIUSスタック

このスタックを標準構成として採用する前に、その高いパフォーマンスと保守運用のオーバーヘッドを天秤に掛けて検討する必要があります。

主なメリット

  • カーネルレベルでのトラフィック分離: CoovaChilliは専用の仮想インターフェース (tun0) を制御します。未認証のゲスト端末がアップストリームのゲートウェイに接触したり、192.168.1.0/24などの内部サブネットに到達したりすることは一切できません。
  • 標準化されたAAAアカウンティング: FreeRADIUSはRADIUSアカウンティングパケットを介して、正確なバイトカウンターとセッション持続時間を記録します。PostgreSQL、MySQL、またはLDAPディレクトリを通じて利用状況を容易に照会できます。
  • きめ細かなウォールドガーデン(制限付きアクセス): Web全体へのアクセスを無制限に許可することなく、DNSクエリやStripe (*.stripe.com) などの決済エンドポイント、CRL/OCSP検証サーバーへの通信のみを選択的に許可できます。
  • ユーザーごとの動的QoS制御: 認証成功時にFreeRADIUSがWISPrベンダ固有属性を返します。CoovaChilliは手動のtcスクリプトを作成することなく、個別のアップロード/ダウンロードトークンバケットを即座に適用します。

運用のボトルネック・留意点

  • MTUおよびトンネルオーバーヘッド: CoovaChilliはトンネルカプセル化を行います。MSSを1460バイトにクランプせずにMTUを1500のままにしておくと、クライアント側でTLSハンドシェイクの停止やファイルダウンロードの破損が発生します。
  • 近年のHTTPSプローブ検出の挙動: 最新のスマートフォンは、Apple Captive Network Assistant (CNA) やAndroidのConnectivityCheckといった専用プローブを使用します。Webサーバーがこれらのプローブエンドポイントを適切に処理しないと、ログインポップアップが表示されません。
  • 複数サービスにまたがるデバッグ: トラブルシューティングの際には、CoovaChilliデーモンの状態、Nginxのリダイレクトフック、FreeRADIUSの認証ログという3つの独立したレイヤーを調査する必要があります。

推奨される本番アーキテクチャ

500〜1,500の同時アクティブゲストを処理する環境では、デュアルNICゲートウェイ(例:4 vCPU、8 GB RAM、デュアル 1GbE/10GbE インターフェース)上でUbuntu 22.04またはDebian 12を稼働させます。

  • WAN (eth0): アップストリームのトランジットIP(例:198.51.100.2/29)を割り当て、LinuxカーネルのアウトバウンドNAT (MASQUERADE) を有効にします。
  • ゲストVLANトランク (eth1): ゲストWi-FiアクセスポイントのVLANトランク(例:VLAN 100)に直接接続します。この物理インターフェースにはIPを割り当てず未設定のままにしておき、CoovaChilliをリンクレイヤーで直接バインドさせます。

このように物理インターフェースを分離することで、未認証のワイヤレスブロードキャストフレームが社内のルーティングテーブルに混入するのを根本から防ぎます。

ステップ・バイ・ステップでのゲートウェイ構築手順

以下の手順に従って、Ubuntuサーバー上にゲートウェイスタック全体を展開および設定します。

ステップ 1: カーネルチューニングと依存パッケージのインストール

sysctlでIPv4パケット転送を有効にし、必要なビルドツール、Webスタック、FreeRADIUSサーバーをインストールします。

# カーネルのパケット転送を有効化し、コネクショントラッキングを最適化
sudo tee -a /etc/sysctl.d/99-captive-portal.conf <<'EOF'
net.ipv4.ip_forward = 1
net.ipv4.tcp_syncookies = 1
net.netfilter.nf_conntrack_max = 262144
EOF
sudo sysctl --system

# ビルドツールチェーン、ライブラリ、FreeRADIUS、Nginxをインストール
sudo apt update
sudo apt install -y build-essential libssl-dev libcurl4-openssl-dev \
    libjson-c-dev freeradius freeradius-utils nginx php8.1-fpm iptables-persistent

ステップ 2: FreeRADIUSの設定とユーザーポリシー定義

まず、ローカルループバック上でCoovaChilliを信頼できるRADIUSクライアントとして承認します。/etc/freeradius/3.0/clients.confを編集します。

client coovachilli {
    ipaddr = 127.0.0.1
    secret = S3cur3Sh4redK3y#2026
    shortname = coovachilli
    require_message_authenticator = no
}

次に、ゲストアカウントのポリシーと帯域制限を/etc/freeradius/3.0/usersで定義します。WISPr帯域属性はbps(ビット/秒)単位で指定する必要がある点に注意してください。

# ゲストプロファイル: 最大セッション2時間、アイドル切断10分、下り10 Mbps / 上り2 Mbps
guest_visitor Cleartext-Password := "WelcomeWifi2026!"
    Session-Timeout = 7200,
    Idle-Timeout = 600,
    ChilliSpot-Max-Input-Octets = 1073741824,
    ChilliSpot-Max-Output-Octets = 1073741824,
    WISPr-Bandwidth-Max-Down = 10485760,
    WISPr-Bandwidth-Max-Up = 2097152

FreeRADIUSを再起動し、radtestを使用してローカルでクレデンシャルを検証します。

sudo systemctl restart freeradius
radtest guest_visitor "WelcomeWifi2026!" 127.0.0.1 0 S3cur3Sh4redK3y#2026

コマンドを実行すると、設定したWISPr-Bandwidth-Max-DownやSession-Timeout属性を含むAccess-Acceptレスポンスが即座に返されるはずです。

ステップ 3: CoovaChilliのコンパイルと設定

セキュアなUAMハンドシェイクを有効にするため、SSLおよびプロキシリダイレクトサポートを有効にしてCoovaChilliをコンパイルします。

cd /usr/local/src
sudo wget https://github.com/coova/coova-chilli/archive/refs/tags/1.6.tar.gz -O coovachilli-1.6.tar.gz
sudo tar -zxvf coovachilli-1.6.tar.gz
cd coova-chilli-1.6

sudo ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var \
    --enable-chilliproxy --enable-chilliredir --enable-binstatusfile --with-openssl
sudo make -j$(nproc)
sudo make install

続いて、/etc/chilli/local.confでネットワークインターフェースのバインドとポータルの設定を行います。

# ネットワークインターフェースのバインド設定
HS_WANIF=eth0             # ルーターへのアウトバウンドアップリンク
HS_LANIF=eth1             # サブスクライバートランクインターフェース(IP未割り当て)
HS_NETWORK=10.10.0.0      # ゲストリース用に割り当てられたサブネット
HS_NETMASK=255.255.248.0  # 最大2,046のクライアントリースをサポートする/21サブネット
HS_UAMLISTEN=10.10.0.1    # tun0上の仮想ゲートウェイIP
HS_UAMPORT=3990

# RADIUSサーバー認証設定
HS_RADIUS=127.0.0.1
HS_RADIUS2=127.0.0.1
HS_RADSECRET=S3cur3Sh4redK3y#2026

# ポータルリダイレクト先の設定
HS_UAMSERVER=portal.guestwifi.net
HS_UAMHOMEPAGE=http://10.10.0.1:3990/prelogin
HS_UAMFORMAT=http://portal.guestwifi.net/login.php

# ウォールドガーデン(制限付き許可)の例外設定
HS_UAMDOMAINS=".guestwifi.net,connectivitycheck.gstatic.com,captive.apple.com"
HS_DNS1=1.1.1.1
HS_DNS2=8.8.8.8

# トンネルのフラグメンテーションを防ぐTCP MSSクランピング
HS_TCPMSS=1460

ステップ 4: UAMスプラッシュページの配備

10.10.0.1:80宛てのportal.guestwifi.netへのリクエストに応答するようNginxを設定します。未認証のデバイスがWebアクセスを開始すると、CoovaChilliがそれをインターセプトし、クエリパラメータをログインハンドラーに渡します。

<?php
// /var/www/html/login.php
$challenge = $_GET['challenge'] ?? '';
$userurl   = $_GET['userurl'] ?? 'http://connectivitycheck.gstatic.com/generate_204';
$uamip     = $_GET['uamip'] ?? '10.10.0.1';
$uamport   = $_GET['uamport'] ?? '3990';
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>ゲストポータル認証</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; text-align: center; padding: 40px;">
    <h2>ゲストネットワーク認証</h2>
    <p>インターネットにアクセスするには認証情報を入力してください:</p>
    <form method="GET" action="http://<?= htmlspecialchars($uamip) ?>:<?= htmlspecialchars($uamport) ?>/logon">
        <input type="hidden" name="challenge" value="<?= htmlspecialchars($challenge) ?>">
        <input type="hidden" name="userurl" value="<?= htmlspecialchars($userurl) ?>">
        <div style="margin: 15px;">
            <input type="text" name="username" placeholder="ユーザー名" required style="padding: 8px; width: 220px;">
        </div>
        <div style="margin: 15px;">
            <input type="password" name="password" placeholder="パスワード" required style="padding: 8px; width: 220px;">
        </div>
        <button type="submit" style="padding: 10px 24px; background: #0066cc; color: #fff; border: none; border-radius: 4px; cursor: pointer;">ログイン</button>
    </form>
</body>
</html>

ユーザーが「ログイン」をクリックすると、ブラウザはポート3990にあるCoovaChilliの内部HTTPサーバーにフォームを直接送信します。デーモンは127.0.0.1経由でFreeRADIUSに対してMD5チャレンジ交換を実施します。検証が完了すると、CoovaChilliはフィルタを解除してステートテーブルにクライアントを登録し、パケットがeth0を経由して外部にルーティングされることを許可します。

ステップ 5: リアルタイムセッション状態の確認

デーモンを起動し、新しくプロビジョニングされた仮想インターフェースを確認します。

sudo chilli --conf /etc/chilli/local.conf
ip addr show dev tun0

アクティブなセッションの確認、帯域幅カウンターの監視、または未承認端末の強制切断を行うには、chilli_queryを使用して稼働中のデーモンに問い合わせます。

# アクティブなサブスクライバーリースとセッション状態を表示
sudo chilli_query list

以下のようなフォーマットで出力が表示されます。

bc:d0:74:11:22:33 10.10.0.14 1 64e29b100000001 guest_visitor 314 0 1482910/10485760 384192/2097152 ...

ステートフラグの1はクライアントが認証済みであることを示し、カウンターはFreeRADIUSによって割り当てられた下り10 Mbpsおよび上り2 Mbpsの制限に対する現在の帯域消費状況を記録しています。端末が不正な動作をしたり利用制限時間を超過したりした場合は、次のコマンドで直ちにセッションを切断できます。

sudo chilli_query logout bc:d0:74:11:22:33

これによりリースがリセットされてパケットフィルタが直ちに遮断され、その端末が次にブラウザリクエストを行った際にログイン画面へ強制リダイレクトされます。

Share: