[PHP]tcpwrap_check関数でTCP Wrappersによるアクセス制御を行う方法を解説

PHP

tcpwrap_check() は、UNIX系システムの伝統的なアクセス制御機構である TCP Wrappers/etc/hosts.allow / /etc/hosts.deny)をPHPから参照し、指定したホストがサービスへのアクセスを許可されているかどうかを確認する関数です。

現代のLinux環境では TCP Wrappers 自体の採用が減り、この関数を目にする機会は限られていますが、レガシーシステムの保守や、hosts.allow/hosts.deny ベースのセキュリティポリシーが運用されている環境では今でも有用です。

関数概要

項目内容
関数名tcpwrap_check()
読み方ティーシーピーラップ・チェック
分類ネットワーク関数(TCP Wrappers拡張)
対応バージョンPHP 4.0.7以降(PECL tcpwrap 拡張が必要)
引数デーモン名(必須)、クライアントIP(必須)、ホスト名(省略可)、ユーザー名(省略可)
戻り値bool(true:アクセス許可、false:アクセス拒否)
必要な拡張tcpwrap(PECLよりインストール)
動作環境UNIX系OSのみ(Windowsでは動作しない)

構文

tcpwrap_check(
    string $daemon,
    string $address,
    string $user = "unknown",
    bool   $nodns = false
): bool
引数説明
$daemonstringアクセス制御を適用するサービス名(/etc/hosts.allow のデーモン名と一致させる)
$addressstringチェックするクライアントのIPアドレス
$userstring接続ユーザー名(省略時は "unknown"
$nodnsbooltrue にするとDNSの逆引きを行わない(PHP 5.0以降で追加)

前提となるTCP Wrappersの仕組み

クライアント(IP: 192.168.1.100)
        │
        │ 接続要求
        ▼
  tcpwrap_check()
        │
        ├─→ /etc/hosts.allow を上から順にチェック
        │      一致するルールがあれば → true(許可)
        │
        └─→ /etc/hosts.deny をチェック
               一致するルールがあれば → false(拒否)
               どちらにも一致しなければ → true(デフォルト許可)

/etc/hosts.allow/etc/hosts.deny の基本的な書式:

# /etc/hosts.allow
myapp: 192.168.1.0/24
myapp: 127.0.0.1

# /etc/hosts.deny
myapp: ALL

この設定例では、myapp というデーモン名に対して 192.168.1.x のサブネットと 127.0.0.1 のみアクセスを許可し、それ以外をすべて拒否する構成になっています。

基本的な使い方

<?php
$daemon    = 'myapp';
$clientIp  = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';

if (tcpwrap_check($daemon, $clientIp)) {
    echo "アクセスが許可されました: {$clientIp}" . PHP_EOL;
} else {
    http_response_code(403);
    echo "アクセスが拒否されました: {$clientIp}" . PHP_EOL;
    exit;
}

実行結果(192.168.1.100/etc/hosts.allow に一致する場合):

アクセスが許可されました: 192.168.1.100

実践的なコード例

例1:アクセス制御ミドルウェアクラス

<?php
class TcpWrapAccessController
{
    public function __construct(
        private readonly string $daemon,
        private readonly bool   $noDns = true
    ) {}

    public function isAllowed(string $ipAddress): bool
    {
        if (!function_exists('tcpwrap_check')) {
            // tcpwrap拡張が未インストールの場合はデフォルト許可
            trigger_error('tcpwrap拡張が利用できません。アクセス制御をスキップします。', E_USER_WARNING);
            return true;
        }

        return tcpwrap_check($this->daemon, $ipAddress, 'unknown', $this->noDns);
    }

    public function denyIfNotAllowed(string $ipAddress): void
    {
        if (!$this->isAllowed($ipAddress)) {
            http_response_code(403);
            header('Content-Type: application/json');
            echo json_encode(['error' => 'Forbidden', 'ip' => $ipAddress]);
            exit;
        }
    }
}

$controller = new TcpWrapAccessController('myapp');
$clientIp   = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';

$controller->denyIfNotAllowed($clientIp);
echo "処理を続行します。" . PHP_EOL;

実行結果(拒否された場合):

HTTP/1.1 403 Forbidden
{"error":"Forbidden","ip":"203.0.113.5"}

function_exists() による拡張の存在確認を入れておくことで、tcpwrap が未インストールの環境でも致命的エラーにならない設計にしています。

例2:複数デーモン名でのポリシー分離

<?php
class MultiDaemonAccessPolicy
{
    private array $rules = [
        'myapp-admin' => ['allowDefault' => false],
        'myapp-api'   => ['allowDefault' => true],
    ];

    public function check(string $daemon, string $ip): bool
    {
        if (!function_exists('tcpwrap_check')) {
            return $this->rules[$daemon]['allowDefault'] ?? true;
        }

        return tcpwrap_check($daemon, $ip, 'unknown', true);
    }
}

$policy = new MultiDaemonAccessPolicy();

$testIps = ['192.168.1.1', '10.0.0.1', '203.0.113.99'];

foreach ($testIps as $ip) {
    $adminAllowed = $policy->check('myapp-admin', $ip) ? '許可' : '拒否';
    $apiAllowed   = $policy->check('myapp-api', $ip)   ? '許可' : '拒否';
    echo "IP: {$ip} | admin: {$adminAllowed} | api: {$apiAllowed}" . PHP_EOL;
}

実行結果(/etc/hosts.allow の設定内容による):

IP: 192.168.1.1   | admin: 許可 | api: 許可
IP: 10.0.0.1      | admin: 拒否 | api: 許可
IP: 203.0.113.99  | admin: 拒否 | api: 拒否

デーモン名を分けることで、管理画面用とAPI用のアクセスポリシーを /etc/hosts.allow の設定だけで細かく制御できます。

例3:DNSの逆引きを無効化してパフォーマンスを最適化する

<?php
class FastIpChecker
{
    /**
     * $nodns = true にするとDNS逆引きが発生しないため、
     * レスポンス速度の改善が見込める。IPアドレスのみでの
     * ポリシー管理をしている環境では常に true を推奨。
     */
    public function check(string $daemon, string $ip): bool
    {
        return tcpwrap_check($daemon, $ip, 'unknown', true);
    }
}

例4:アクセスログとの連携

<?php
class AuditedAccessController
{
    private string $logFile;

    public function __construct(
        private readonly string $daemon,
        string $logFile = '/var/log/myapp-access.log'
    ) {
        $this->logFile = $logFile;
    }

    public function checkAndLog(string $ip): bool
    {
        $allowed  = function_exists('tcpwrap_check')
            ? tcpwrap_check($this->daemon, $ip, 'unknown', true)
            : true;

        $status   = $allowed ? 'ALLOW' : 'DENY';
        $logEntry = sprintf(
            "[%s] %s daemon=%s ip=%s\n",
            date('Y-m-d H:i:s'),
            $status,
            $this->daemon,
            $ip
        );

        file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX);

        return $allowed;
    }
}

$controller = new AuditedAccessController('myapp');
$result = $controller->checkAndLog('192.168.1.50');

echo $result ? "アクセス許可" : "アクセス拒否";

ログファイルへの出力例:

[2026-06-25 14:30:00] ALLOW daemon=myapp ip=192.168.1.50
[2026-06-25 14:30:05] DENY  daemon=myapp ip=203.0.113.99

例5:フォールバック付きファクトリークラス

<?php
interface AccessCheckerInterface
{
    public function isAllowed(string $ip): bool;
}

class TcpWrapChecker implements AccessCheckerInterface
{
    public function __construct(private readonly string $daemon) {}

    public function isAllowed(string $ip): bool
    {
        return tcpwrap_check($this->daemon, $ip, 'unknown', true);
    }
}

class AllowAllChecker implements AccessCheckerInterface
{
    public function isAllowed(string $ip): bool
    {
        return true;
    }
}

class AccessCheckerFactory
{
    public static function create(string $daemon): AccessCheckerInterface
    {
        if (function_exists('tcpwrap_check')) {
            return new TcpWrapChecker($daemon);
        }
        trigger_error(
            'tcpwrap拡張が利用不可のため、すべてのアクセスを許可するフォールバックを使用します。',
            E_USER_WARNING
        );
        return new AllowAllChecker();
    }
}

$checker = AccessCheckerFactory::create('myapp');
var_dump($checker->isAllowed('192.168.1.1'));

実行結果(tcpwrap未インストール環境):

Warning: tcpwrap拡張が利用不可のため、すべてのアクセスを許可するフォールバックを使用します。
bool(true)

インターフェースと依存性逆転を活用し、tcpwrap の有無にかかわらず同じ呼び出し方で動くように設計した例です。

例6:CLIからの接続元IPチェック(バッチ処理向け)

<?php
class CliBatchAccessController
{
    public function __construct(
        private readonly string $daemon,
        private readonly array  $allowedIps = ['127.0.0.1', '::1']
    ) {}

    public function isAllowed(): bool
    {
        // CLI実行の場合はREMOTE_ADDRが存在しないためlocalhostとして扱う
        if (PHP_SAPI === 'cli') {
            $ip = '127.0.0.1';
        } else {
            $ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
        }

        if (!function_exists('tcpwrap_check')) {
            return in_array($ip, $this->allowedIps, true);
        }

        return tcpwrap_check($this->daemon, $ip, 'unknown', true);
    }
}

$controller = new CliBatchAccessController('myapp-batch');

if (!$controller->isAllowed()) {
    fwrite(STDERR, "実行が許可されていません。\n");
    exit(1);
}

echo "バッチ処理を開始します。" . PHP_EOL;

例7:設定ファイルベースでデーモン名を切り替える

<?php
class ConfigurableTcpWrapChecker
{
    private string $daemon;

    public function __construct(array $config)
    {
        $this->daemon = $config['tcpwrap_daemon'] ?? 'myapp';
    }

    public function check(string $ip): bool
    {
        if (!function_exists('tcpwrap_check')) {
            return true;
        }
        return tcpwrap_check($this->daemon, $ip, 'unknown', true);
    }
}

// config.php から読み込む想定
$config = [
    'tcpwrap_daemon' => 'myapp-production',
];

$checker = new ConfigurableTcpWrapChecker($config);
$ip = '10.0.0.5';

echo $checker->check($ip) ? "許可" : "拒否";

関連関数・仕組みとの比較

手段概要管理の場所
tcpwrap_check()TCP Wrappersポリシーを参照してIPを判定/etc/hosts.allow / /etc/hosts.deny
ip2long() + 範囲比較PHPコード内でIPレンジを判定PHPの設定ファイル・DB
$_SERVER['REMOTE_ADDR'] 検証PHPコード内で接続元IPを確認PHPコード内
iptables / nftablesOS/カーネルレベルのパケットフィルタリングOSの設定ファイル
Nginxのallow/denyディレクティブWebサーバーレベルのアクセス制御Nginx設定ファイル
.htaccess(Apache)WebサーバーレベルのIP制限.htaccess / Apache設定

tcpwrap_check() の最大の特徴は「PHPの外側、OSレベルで管理されている /etc/hosts.allow / /etc/hosts.deny をPHPから参照できる」という点です。システム全体のアクセスポリシーとPHPアプリケーションのポリシーを一元管理したい場合に有効ですが、現代では Nginx の allow/denyiptables による制御のほうが主流になっています。

よくある落とし穴・注意点

  1. PECLからの別途インストールが必要 tcpwrap_check() はPHPコアには含まれておらず、PECLの tcpwrap 拡張(pecl install tcpwrap)をインストールしたうえで php.ini で有効化する必要があります。関数が存在するかどうかを function_exists('tcpwrap_check') で確認してから使うのを徹底しましょう。
  2. UNIX系OSのみで動作する TCP Wrappers は UNIX/Linux の仕組みであり、Windows環境では動作しません。クロスプラットフォームなコードを書く場合は必ずOS判定または function_exists() によるガードを入れてください。
  3. $nodns = false(デフォルト)ではDNS逆引きが発生する 第4引数 $nodns を省略または false にすると、IPアドレスからホスト名へのDNS逆引きクエリが発生します。これはレスポンスタイムの増加やDNSの障害に引きずられる原因になるため、IPアドレスのみでポリシーを管理している環境では true を指定してDNS逆引きを無効化することを推奨します。
  4. /etc/hosts.allow / /etc/hosts.deny の設定ミスがアプリに直撃する OS側の設定ファイルを変更すると、PHPアプリケーション側の挙動も直接変わります。インフラチームとの連携なしにデーモン名を決めてしまうと、設定の混乱を招くことがあります。デーモン名の命名規則やポリシー変更の手順をチームで明文化しておくことが重要です。
  5. PHP 8系以降での利用実績・メンテナンス状況に注意 tcpwrap PECL拡張は長らくメンテナンスが活発ではなく、PHP 8.x系での動作確認やサポート状況を事前に確認することを推奨します。新規プロジェクトでのアクセス制御には、より管理しやすい Nginx の allow/deny やファイアウォールルールの活用を検討してください。

まとめ

ポイント内容
役割TCP Wrappersの/etc/hosts.allow//etc/hosts.denyを参照してIPのアクセス可否を判定する
引数デーモン名、クライアントIP、ユーザー名(省略可)、DNS逆引き無効フラグ(省略可)
戻り値bool(true:許可、false:拒否)
動作環境UNIX系OSのみ
必要な拡張PECL tcpwrap(別途インストールが必要)
注意点function_exists()でのガード必須、DNS逆引きによるパフォーマンス影響、PHP 8系での動作確認
現代的な代替手段Nginxのallow/denyiptables/nftables、PHPコード内でのIP検証

tcpwrap_check() はOSレベルのアクセス制御ポリシーをそのままPHPアプリケーションに取り込める点がユニークですが、PECL拡張の別途インストールやUNIX系OS限定という制約があります。レガシーシステムの保守や既存のTCP Wrappersポリシーとの統合が必要な場面では有用ですが、新規開発ではより広くサポートされた手段を検討するのが現実的です。

タイトルとURLをコピーしました