Работа с 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 или встроенные функции для работы с бинарными форматами.

IP-адрес в PHP - comments

En
Php ip адрес (php)