Trabajo con IPv6 en PHP

Recientemente obtuvimos el estatus LIR y un bloque IPv6 de /29. A continuación, surgió la necesidad de llevar un registro de las subredes asignadas. Y dado que nuestro sistema de facturación está escrito en PHP, tuvimos que profundizar en el tema y darnos cuenta de que este lenguaje no es el más amigable a la hora de trabajar con IPv6. A continuación, presentamos nuestra solución a los problemas surgidos en el manejo de direcciones y rangos. Puede que no sea la más elegante, pero cumple con las tareas planteadas.

Trabajo con IPv6 en PHP

Un poco de teoría

Descargo de responsabilidad. Si ya estás familiarizado con lo que es IPv6 y cómo se usa, esta parte puede resultarte aburrida. O tal vez no.

Para las personas que ven por primera vez una notación de IPv6, puede ser bastante desconcertante. Después de lo elegante, 64.233.177.101 de repente nos encontramos con 2607:f8b0:4002:c08::8b y podemos quedarnos confundidos. Ambos son simplemente representaciones comprensibles para el ser humano de 32 y 128 bits respectivamente. Cualquier paquete IP contiene un encabezado con un propósito estrictamente estandarizado para cada bit. Sin profundizar aún más en la estructura de los encabezados, lo que necesitamos sacar de esto es: para las operaciones con direcciones IP y rangos, es principalmente conveniente usar matemáticas binarias y operaciones bit a bit. También es más conveniente almacenar en la base de datos como BINARY(4) para IPv4 y BINARY(16) para IPv6.

Otro aspecto importante que vale la pena mencionar son las máscaras de red y la notación CIDR. CIDR es un acrónimo de Classless Inter-Domain Routing (direccionamiento sin clases). Este concepto reemplazó el clasificado en la determinación de qué parte de la dirección IP es el prefijo de la red, y cuál es la dirección de la interfaz de red dentro de esa red. En la práctica, los primeros n bits, correspondientes al prefijo, se establecerán en 1, y los demás en 0.

En términos comprensibles para el ser humano, esto se escribe como ip.add.re.ss/cidr. Por ejemplo, 64.233.177.0/24 lo que significa que los primeros 24 bits corresponden al prefijo. Los últimos 8 bits, que son el último número en la notación comprensible, pertenecen a la dirección dentro de la subred. Un par de ejercicios más. 64.233.177.101/32 y 2607:f8b0:4002:c08::8b/128 es una dirección específica. 2607:f8b0:4002:c08::/64 los primeros 64 bits (las primeras 4 secciones) son el prefijo, los otros 64 bits son la parte local. Por cierto, si a alguien le confunde el "::" en la notación, los dos puntos dobles reemplazan cualquier cantidad de secciones que contengan 0. Solo puede aparecer en la notación una vez. En otras palabras, 2607:f8b0:4002:c08::8b = 2607:f8b0:4002:c08:0:0:0:8b.

¿Qué podemos extraer de todo esto? En primer lugar, se puede obtener la primera y última dirección de la subred utilizando las operaciones binarias AND y OR, conociendo la máscara en forma binaria. En segundo lugar, la siguiente subred del tamaño (es decir, con CIDR) n se puede calcular sumando 1 a n-ésima posición en la representación binaria. Por representación binaria, me refiero al resultado de utilizar las funciones pack() y inet_pton() y luego utilizar operadores a nivel de bits, por binario, me refiero a la representación en el sistema binario que se puede obtener, por ejemplo, con base_convert().

Nota históricaA la segmentación de direcciones con clases le precedía la segmentación de direcciones sin clases. En aquellos días lejanos, nadie suponía que habría tantas subredes, se asignaban a diestro y siniestro en grandes bloques: clase A — el prefijo eran los primeros 8 bits (es decir, el primer número), con el bit líder 0; clase B — los primeros 16 (los dos primeros números), bits líderes 10; clase C — los primeros 24 bits, bits líderes 110. Estos bits líderes definían los rangos en los que se asignaba la dirección de cada clase: 0.0.0.0 — 127.255.255.255 para clase A, 128.0.0.0 — 191.255.255.255 — clase B, 192.0.0.0 — 223.255.255.255 — clase C. A medida que Internet se extendía por el planeta, los reguladores se dieron cuenta de que se habían equivocado, y a principios de los años 90 desarrollaron un concepto sin clases que permitió desvincularse de los bits líderes. Se pueden encontrar más detalles, por ejemplo, en el gran y omnisciente.

Pasemos a la práctica

En la práctica, realizaremos las tres tareas más probables, según mi opinión:

  1. obtener la primera y última dirección del rango;
  2. obtener el siguiente rango de tamaño especificado (CIDR);
  3. verificar si una dirección pertenece al rango.

La implementación será para IPv6, pero si es necesario, la lógica se puede adaptar fácilmente. Algunas ideas las he extraído desde aquí, pero las he implementado de manera ligeramente diferente. Además, en los ejemplos no hay verificación de errores de entrada. Así que, ¡comencemos!

Como ya mencioné, se puede determinar la primera y última dirección del rango mediante operaciones a nivel de bits, conociendo el inicio del rango y la máscara binaria de la subred. Por lo tanto, lo primero que debemos hacer es convertir el CIDR en una máscara binaria. Para ello, recopilaremos su representación hexadecimal y la empaquetaremos en binario.

function cidrToMask ($cidr) {
    $mask = str_repeat('f', ceil($cidr / 4));
    $mask .= dechex(4 * ($cidr % 4));
    $mask = str_pad($mask, 32, '0');
    return pack('H*', $mask);
}

mountsnoop.py pack(‘H*’, $mask) empaqueta la representación hexadecimal de la misma manera que inet_pton(). Con la única diferencia de que al llamarlo pack() todos los 0 deben estar en su lugar, y no debe haber dos puntos en la anotación, a diferencia de la anotación legible por humanos.

El siguiente paso es calcular el inicio y el final del rango. Y aquí surgirán matices. Las operaciones a nivel de bits están limitadas por la arquitectura del procesador. Por lo tanto, en mi CubieTruck de 32 bits, que a veces utilizo para pruebas varias, no se podrá procesar toda la dirección de 128 bits en una sola operación. Sin embargo, nada impide que la dividamos en grupos de 32 bits (por si acaso, quién sabe en qué procesadores vamos a ejecutarlo).

function getRangeBoundary ($ip, $cidr, $which, $ipIsBin = false, $returnBin = false) {
    $mask = cidrToMask($cidr);
    if (!$ipIsBin) {
        $ip = inet_pton($ip);
    }
    $ipParts   = str_split($ip, 4);
    $maskParts = str_split($mask, 4);
    $rangeParts  = [];
    for ($i = 0; $i < count($ipParts); $i++) {
        if ($which == 'start') {
            /* Побитовый & адреса и маски оставит только биты префикса. */
            $rangeParts[$i] = $ipParts[$i] & $maskParts[$i];
        } else {
            /* Побитовый | с обратной маской (~) оставит биты префикса и установит все биты локальной части в 1. */
            $rangeParts[$i] = $ipParts[$i] | ~$maskParts[$i];
        }
    }
    $rangeBoundary = implode($rangeParts);
    if ($returnBin) {
        return $rangeBoundary;
    } else {
        return inet_ntop($rangeBoundary);
    }
}

Para su uso futuro, preveamos la posibilidad de transmitir una IP y recibir el resultado tanto en binario como en un formato legible por humanos. El parámetro $which aquí indica si queremos obtener el inicio o el final del rango (los valores 'start' o 'end' correspondientemente).

La siguiente tarea (además, la más práctica para nuestra empresa) es calcular el siguiente rango. Para esta tarea, no se me ocurrió nada mejor que descomponer la dirección en una cadena binaria y añadir 1 en la posición correcta, tras lo cual reensamblar todo. Para evitar la aparición de artefactos, decidí dividir la dirección por byte al descomponerla y volver a montarla.

function getNextBlock ($ipStart, $cidr, $ipIsBin = false, $returnBin = false) {
    if (!$ipIsBin) {
        $ipStart = inet_pton($ipStart);
    }
    $ipParts = str_split($ipStart, 1);
    $ipBin   = '';
    foreach ($ipParts as $ipPart) {
        $ipBin .= str_pad(base_convert(unpack('H*', $ipPart)[1], 16, 2), 8, '0', STR_PAD_LEFT);
    }
    /* Добавляем 1 в нужном разряде двоичного представления строки "влоб" :) */
    $i = $cidr - 1;
    while ($i >= 0) {
        if ($ipBin[$i] == '0') {
            $ipBin[$i] = '1';
            break;
        } else {
            $ipBin[$i] = '0';
        }
        $i--;
    }
    $ipBinParts = str_split($ipBin, 8);
    foreach ($ipBinParts as $key => $ipBinPart) {
        $ipParts[$key] = pack('H*', str_pad(base_convert($ipBinPart, 2, 16), 2, '0', STR_PAD_LEFT));
    }
    $nextIp = implode($ipParts);
    if ($returnBin) {
        return $nextIp;
    } else {
        return inet_ntop($nextIp);
    }
}

El resultado será el prefijo del siguiente rango de tamaño indicado en $cidr.Con esta función, asignamos bloques de direcciones a nuestros clientes.

Finalmente, verificamos si la dirección pertenece al rango. Por ejemplo, asignamos un bloque /48 para asignar a los clientes bloques /64, y necesitamos asegurarnos de que al asignar no excedamos el bloque asignado (en la práctica, esto no ocurrirá pronto, pero la posibilidad existe). Aquí es simple. Obtenemos el inicio y el final del rango en formato binario y verificamos si la dirección está dentro de los límites.

function ipInRange ($ip, $rangeStart, $cidr) {
    $start = getRangeBoundary($rangeStart, $cidr, 'start', false, true);
    $end = getRangeBoundary($rangeStart, $cidr, 'end', false, true);
    $ipBin = inet_pton($ip);
    return ($ipBin >= $start && $ipBin <= $end);
}

Espero que haya sido útil. ¿Qué otras funciones relacionadas con direcciones consideran que podrían ser útiles? Cualquier adición, comentario y revisión de código son bienvenidos en los comentarios.

Si ya es nuestro cliente o está considerando convertirse en uno, con motivo del lanzamiento de este artículo, le ofrecemos un bloque /64 completamente gratis para todos los servicios VPS o servidores dedicados en el centro de datos Equinix Tier IV, Países Bajos, a solicitud del departamento de ventas, proporcionando el enlace a este artículo en el ticket. La oferta es válida hasta marzo de 2020.

Un poco de publicidad 🙂

Gracias por permanecer con nosotros. ¿Te gustan nuestros artículos? ¿Quieres ver más contenido interesante? Apóyanos haciendo un pedido o recomendando a tus conocidos, VPS en la nube para desarrolladores desde $4.99, un análogo único de servidores entry-level que hemos diseñado para Ti: Toda la verdad sobre VPS (KVM) E5-2697 v3 (6 núcleos) 10GB DDR4 480GB SSD 1Gbps desde $19, o cómo dividir correctamente un servidor? (disponibles opciones con RAID1 y RAID10, hasta 24 núcleos y hasta 40GB DDR4).

¿Dell R730xd a mitad de precio en el centro de datos Equinix Tier IV en Ámsterdam? Solo aquí 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TB desde $199 ¡en los Países Bajos! Dell R420 — 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — ¡desde $99! Lee sobre cómo Construir infraestructura de clase empresarial usando servidores Dell R730xd E5-2650 v4 que cuestan 9000 euros a un precio asequible?

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster