Работа с IP адресами в PHP: функции, фильтрация и практические примеры
IP адрес в PHP: основные подходы к получению и обработке
Наиболее эффективное решение для получения реального IP адреса клиента в PHP учитывает возможное наличие обратного прокси (CDN, балансировщик). Вместо прямого использования $_SERVER['REMOTE_ADDR'] (который возвращает адрес прокси) применяется проверка заголовка HTTP_X_FORWARDED_FOR или HTTP_X_REAL_IP с обязательной фильтрацией доверенных прокси.
function getRealIp(): ?string
{
$trustedProxies = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']; // пример
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? null;
// Если клиент напрямую, сразу возвращаем REMOTE_ADDR
if (!isProxy($remoteAddr, $trustedProxies)) {
return $remoteAddr;
}
// Проверяем заголовки от доверенных прокси
$forwarded = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null;
if ($forwarded) {
$ips = explode(',', $forwarded);
$firstIp = trim($ips[0]);
if (filter_var($firstIp, FILTER_VALIDATE_IP)) {
return $firstIp;
}
}
return $remoteAddr; // fallback
}
Функция isProxy() проверяет, принадлежит ли переданный IP одному из диапазонов доверенных прокси (например, внутренние сети). Такой подход исключает подмену заголовка злоумышленником, не проходящим через прокси.
Типичная ошибка:
- Использование $_SERVER['HTTP_X_FORWARDED_FOR'] без проверки – злоумышленник может подставить любой IP.
- Игнорирование IPv6 при проверке диапазонов (функция должна обрабатывать обе версии).
Решение: всегда фильтровать заголовки на основе доверенных прокси и применять filter_var для базовой валидации.
Как получить IP адрес без учёта прокси?
Самый простой вариант – прямое обращение к элементу массива $_SERVER['REMOTE_ADDR'].
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
echo "IP: $ip";
Подходит для внутренних сетей или окружений без прокси. Проблема: в продакшене часто стоит nginx или CloudFlare, и REMOTE_ADDR показывает адрес прокси.
Ошибка: значение $_SERVER['REMOTE_ADDR'] может отсутствовать при запуске через командную строку. Всегда проверяйте через ?? или isset().
Как узнать IP адрес сервера по доменному имени?
Функция gethostbyname() возвращает IPv4 адрес, gethostbynamel() – массив всех адресов.
$domain = 'www.php.net';
$ip = gethostbyname($domain);
echo "IP: $ip";
$allIps = gethostbynamel($domain);
print_r($allIps);
IP: 185.85.0.1
Array
(
[0] => 185.85.0.1
[1] => 2a02:aa0:1::1
)
Для получения IPv6 используйте dns_get_record() с типом DNS_AAAA.
Ошибка: gethostbyname() при ошибке возвращает само имя домена. Проверять результат нужно на соответствие формату IP.
Как проверить, корректен ли IP адрес?
Фильтр FILTER_VALIDATE_IP с флагами FILTER_FLAG_IPV4 или FILTER_FLAG_IPV6.
$ip = '192.168.1.1';
if (filter_var($ip, FILTER_VALIDATE_IP)) {
echo "IPv4 или IPv6";
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
echo "Только IPv4";
}
Флаг FILTER_FLAG_NO_PRIV_RANGE исключает частные диапазоны (RFC 1918).
Ошибка: фильтр возвращает false для 0.0.0.0 (нет в текущей версии PHP?). На самом деле 0.0.0.0 считается валидным. Для строгих проверок используйте дополнительную парсинг.
Как проверить, принадлежит ли IP определённой подсети (CIDR)?
Для IPv4: преобразуем IP и маску в целые числа и сравниваем.
function ipInCidr(string $ip, string $cidr): bool
{
list($subnet, $mask) = explode('/', $cidr);
$ipLong = ip2long($ip);
$subnetLong = ip2long($subnet);
$maskLong = -1 << (32 - $mask);
return ($ipLong & $maskLong) === ($subnetLong & $maskLong);
}
var_dump(ipInCidr('192.168.1.100', '192.168.1.0/24')); // true
var_dump(ipInCidr('10.0.0.1', '10.0.0.0/8')); // true
Для IPv6 используют inet_pton() и побитовые операции.
Ошибка: ip2long() работает только с IPv4. Для IPv6 нужно использовать inet_pton() и сравнение битов.
Как сравнить IP адреса (или преобразовать в число)?
Для сортировки или проверки диапазона удобно представить IP в виде целого числа (IPv4) или packed-строки (IPv6).
$ip = '192.168.1.1';
$numeric = ip2long($ip); // 3232235777
echo $numeric;
// Для сравнения двух IP
$ip1 = ip2long('10.0.0.1');
$ip2 = ip2long('10.0.0.254');
if ($ip1 <= $ip2) { /* ... */ }
Для IPv6: inet_pton() возвращает бинарную строку (16 байт). Сравнение производится через strcmp() или преобразование к 128-битному числу (с использованием модулей GMP или BCMath).
Ошибка: ip2long() для IP 255.255.255.255 возвращает -1 (из-за знаковости целых). Используйте sprintf('%u') для беззнакового вывода.
Расширенные примеры работы с IP адресами в PHP
Ниже приведены более сложные сценарии, включающие обработку IPv6, работу с массивами прокси и построение универсальных функций.
// Пример 1: Универсальная функция проверки CIDR (IPv4 и IPv6)
function ipInCidrV6(string $ip, string $cidr): bool
{
if (strpos($cidr, '/') === false) {
return $ip === $cidr;
}
list($subnet, $mask) = explode('/', $cidr);
$ipBin = inet_pton($ip);
$subnetBin = inet_pton($subnet);
if ($ipBin === false || $subnetBin === false) {
return false;
}
$mask = (int)$mask;
// Для IPv4 маска от 0 до 32, для IPv6 от 0 до 128
$fullBytes = intdiv($mask, 8);
$partialBits = $mask % 8;
$compareBytes = $fullBytes + ($partialBits > 0 ? 1 : 0);
for ($i = 0; $i < $compareBytes && $i < strlen($ipBin); $i++) {
$ipByte = ord($ipBin[$i]);
$subnetByte = ord($subnetBin[$i]);
if ($i < $fullBytes) {
if ($ipByte !== $subnetByte) {
return false;
}
} else {
// последний неполный байт, маскируем старшие биты
$maskByte = 0xFF << (8 - $partialBits) & 0xFF;
if (($ipByte & $maskByte) !== ($subnetByte & $maskByte)) {
return false;
}
}
}
return true;
}
// Тест IPv6
echo ipInCidrV6('2a02:aa0:1::1', '2a02:aa0::/32') ? 'да' : 'нет'; // да
да
// Пример 2: Получение всех IP адресов из диапазона (только IPv4)
function rangeToIps(string $start, string $end): array
{
$startLong = ip2long($start);
$endLong = ip2long($end);
$ips = [];
for ($long = $startLong; $long <= $endLong; $long++) {
$ips[] = long2ip($long);
}
return $ips;
}
print_r(rangeToIps('192.168.1.1', '192.168.1.3'));
Array
(
[0] => 192.168.1.1
[1] => 192.168.1.2
[2] => 192.168.1.3
)
// Пример 3: Распознавание прокси-цепочки с доверенными серверами
$proxyChain = '203.0.113.5, 198.51.100.7, 192.168.1.1';
$trusted = ['192.168.0.0/16', '10.0.0.0/8'];
function extractClientIp(string $chain, string $remoteAddr, array $trusted): string
{
$ips = array_map('trim', explode(',', $chain));
$ips[] = $remoteAddr; // последний прокси - сам сервер
$clientIp = null;
foreach ($ips as $ip) {
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
break;
}
if ($clientIp === null) {
$clientIp = $ip;
}
// Если IP не доверенный, значит это клиент?
// На самом деле: все IP до первого недоверенного - клиент
// Здесь упрощение: ищем первый недоверенный
if (!isTrusted($ip, $trusted)) {
$clientIp = $ip;
break;
}
}
return $clientIp ?? $remoteAddr;
}
function isTrusted(string $ip, array $trusted): bool
{
foreach ($trusted as $cidr) {
if (ipInCidrV6($ip, $cidr)) { // предполагаем, что ipInCidrV6 работает для обоих версий
return true;
}
}
return false;
}
echo extractClientIp($proxyChain, '10.0.0.99', $trusted); // 198.51.100.7 (первый внешний)
198.51.100.7
// Пример 4: Преобразование IPv6 в десятичное число (используя GMP)
function ipv6ToDecimal(string $ip): string
{
$bin = inet_pton($ip);
if ($bin === false) return '';
$hex = bin2hex($bin);
return gmp_strval(gmp_init($hex, 16), 10);
}
echo ipv6ToDecimal('::1'); // 1
echo ipv6ToDecimal('2a02:aa0:1::1');
1 34642286731310395309171746644232101889
// Пример 5: Проверка, является ли IP частным (RFC 1918 + уникальные локальные IPv6)
function isPrivateIp(string $ip): bool
{
$privateRanges = [
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'127.0.0.0/8',
'169.254.0.0/16',
'fc00::/7', // unique local
'fe80::/10', // link-local
'::1/128' // loopback
];
foreach ($privateRanges as $cidr) {
if (ipInCidrV6($ip, $cidr)) { // опять используем универсальную функцию
return true;
}
}
return false;
}
var_dump(isPrivateIp('192.168.1.1')); // true
var_dump(isPrivateIp('8.8.8.8')); // false
bool(true) bool(false)
При работе с большими объёмами данных (логи, геолокация) рекомендуется использовать расширения maxmind/geoip2 или встроенные функции для работы с бинарными форматами.