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
| 引数 | 型 | 説明 |
|---|---|---|
$daemon | string | アクセス制御を適用するサービス名(/etc/hosts.allow のデーモン名と一致させる) |
$address | string | チェックするクライアントのIPアドレス |
$user | string | 接続ユーザー名(省略時は "unknown") |
$nodns | bool | true にすると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 / nftables | OS/カーネルレベルのパケットフィルタリング | 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/deny や iptables による制御のほうが主流になっています。
よくある落とし穴・注意点
- PECLからの別途インストールが必要
tcpwrap_check()はPHPコアには含まれておらず、PECLのtcpwrap拡張(pecl install tcpwrap)をインストールしたうえでphp.iniで有効化する必要があります。関数が存在するかどうかをfunction_exists('tcpwrap_check')で確認してから使うのを徹底しましょう。 - UNIX系OSのみで動作する TCP Wrappers は UNIX/Linux の仕組みであり、Windows環境では動作しません。クロスプラットフォームなコードを書く場合は必ずOS判定または
function_exists()によるガードを入れてください。 $nodns = false(デフォルト)ではDNS逆引きが発生する 第4引数$nodnsを省略またはfalseにすると、IPアドレスからホスト名へのDNS逆引きクエリが発生します。これはレスポンスタイムの増加やDNSの障害に引きずられる原因になるため、IPアドレスのみでポリシーを管理している環境ではtrueを指定してDNS逆引きを無効化することを推奨します。/etc/hosts.allow//etc/hosts.denyの設定ミスがアプリに直撃する OS側の設定ファイルを変更すると、PHPアプリケーション側の挙動も直接変わります。インフラチームとの連携なしにデーモン名を決めてしまうと、設定の混乱を招くことがあります。デーモン名の命名規則やポリシー変更の手順をチームで明文化しておくことが重要です。- 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/deny、iptables/nftables、PHPコード内でのIP検証 |
tcpwrap_check() はOSレベルのアクセス制御ポリシーをそのままPHPアプリケーションに取り込める点がユニークですが、PECL拡張の別途インストールやUNIX系OS限定という制約があります。レガシーシステムの保守や既存のTCP Wrappersポリシーとの統合が必要な場面では有用ですが、新規開発ではより広くサポートされた手段を検討するのが現実的です。
