Lavorare con IPv6 in PHP

Recentemente abbiamo ottenuto lo stato di LIR e un blocco IPv6 /29. Di conseguenza, è sorta la necessità di tenere traccia delle subnet assegnate. E dato che il nostro sistema di billing è scritto in PHP, abbiamo dovuto approfondire l'argomento e rendersi conto che questo linguaggio non è il più facile da usare per lavorare con IPv6. Di seguito, la nostra soluzione alle problematiche emerse nella gestione degli indirizzi e degli intervalli. Forse non è la più elegante, ma svolge le funzioni richieste.

Lavorare con IPv6 in PHP

Un po' di teoria

Disclaimer. Se sei a conoscenza di cosa sia IPv6 e come funziona, questa parte potrebbe risultarti noiosa. O magari no.

Le persone che vedono per la prima volta una nota su IPv6 potrebbero sentirsi disorientate. Dopo l'eleganza di 64.233.177.101 ci troviamo all'improvviso ad affrontare 2607:f8b0:4002:c08::8b e potremmo sentirci confusi. Entrambi sono semplicemente rappresentazioni comprensibili per gli esseri umani di 32 e 128 bit rispettivamente. Qualsiasi pacchetto IP contiene un'intestazione con uno scopo rigorosamente standardizzato per ciascun bit. Senza approfondire ulteriormente la struttura delle intestazioni, possiamo concludere una cosa: per operazioni con indirizzi IP e intervalli è principalmente comodo utilizzare la matematica binaria e le operazioni bit a bit. È anche più comodo memorizzarli nel database come BINARY(4) per IPv4 e BINARY(16) per IPv6.

Un altro aspetto importante che merita di essere menzionato è quello delle maschere di rete e della notazione CIDR. CIDR è l'acronimo di Classless Inter-Domain Routing (indirizzamento senza classi). Questo concetto ha sostituito quello di classi nella questione di determinare quale parte dell'indirizzo IP sia il prefisso di rete e quale sia l'indirizzo dell'interfaccia di rete all'interno di quella rete. Nella pratica, i primi n bit corrispondenti al prefisso saranno impostati a 1, mentre gli altri saranno impostati a 0.

In forma comprensibile si scrive come ip.add.re.ss/cidr. Ad esempio, 64.233.177.0/24 indica che i primi 24 bit appartengono al prefisso. Gli ultimi 8 bit, ovvero l'ultimo numero nella notazione comprensibile, si riferiscono all'indirizzo all'interno della subnet. Altri due esempi. 64.233.177.101/32 e 2607:f8b0:4002:c08::8b/128 è un indirizzo specifico. 2607:f8b0:4002:c08::/64 indica che i primi 64 bit (prime 4 sezioni) costituiscono il prefisso, mentre gli altri 64 bit sono la parte locale. A proposito, se qualcuno è confuso dal "::" nella notazione, il doppio due punti sostituisce un numero arbitrario di sezioni contenenti 0. Può apparire nella nota solo una volta. In altre parole, 2607:f8b0:4002:c08::8b = 2607:f8b0:4002:c08:0:0:0:8b.

Cosa dobbiamo trarre da tutto questo? In primo luogo, il primo e l'ultimo indirizzo della sottorete possono essere ottenuti utilizzando gli operatori binari AND e OR, conoscendo la maschera in formato binario. In secondo luogo, la successiva sottorete di dimensione (cioè con CIDR) n può essere calcolata aggiungendo 1 alla n-esima posizione nella rappresentazione binaria. Con 'rappresentazione binaria' intendo il risultato dell'uso delle funzioni pack() e inet_pton() e il successivo utilizzo di operatori bitwise, mentre 'binario' si riferisce alla rappresentazione nel sistema binario, che può essere ottenuta, ad esempio, tramite base_convert().

Nota storicaPrima dell'approccio senza classi c'era la segmentazione classi. In quegli anni lontani nessuno immaginava quante sottoreti ci sarebbero state, venivano distribuite a destra e a manca in grandi blocchi: classe A — il prefisso erano i primi 8 bit (cioè il primo numero), con il bit principale 0; classe B — i primi 16 (i primi due numeri), bit principali 10; classe C — i primi 24 bit, bit principali 110. Questi bit principali definivano gli intervalli in cui veniva rilasciato l’indirizzo di un determinato classe: 0.0.0.0 — 127.255.255.255 per la classe A, 128.0.0.0 — 191.255.255.255 — classe B, 192.0.0.0 — 223.255.255.255 — classe C. Man mano che Internet si diffondeva sul pianeta, i regolatori si resero conto di aver commesso un errore, e all'inizio degli anni '90 svilupparono il concetto senza classi, che permise di non legarsi ai bit principali. Maggiori dettagli possono essere trovati, ad esempio, in grande e onnisciente.

Passiamo alla pratica

In pratica realizzeremo tre compiti che mi sono sembrati i più probabili:

  1. ottenere il primo e l'ultimo indirizzo dell'intervallo;
  2. ottenere il successivo intervallo di dimensione specificata (CIDR);
  3. verificare se un indirizzo appartiene all'intervallo.

L'implementazione sarà per IPv6, ma se necessario, la logica può essere facilmente adattata. Alcune idee le ho tratte da qui, ma le ho implementate in modo leggermente diverso. Inoltre, negli esempi non c'è alcun controllo degli errori di input. Quindi, cominciamo.

Come ho già accennato, il primo e l'ultimo indirizzo dell'intervallo possono essere determinati utilizzando operazioni bitwise, conoscendo l'inizio dell'intervallo e la maschera binaria della sottorete. Pertanto, la prima cosa da fare è convertire il CIDR in una maschera binaria. Per fare ciò, raccoglieremo la sua rappresentazione esadecimale e la impacchetteremo in formato 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);
}

Chiamata pack(‘H*’, $mask) packa la rappresentazione esadecimale in modo simile a inet_pton(). Con la sola differenza che alla chiamata pack() tutti 0 devono essere al loro posto, e nei registri non devono esserci due punti, a differenza di quanto avviene nei registri comprensibili per le persone.

Il passo successivo è calcolare l'inizio e la fine dell'intervallo. Qui sorgono alcune complessità. Le operazioni bit a bit sono limitate dalla larghezza di bus del processore. Di conseguenza, sul mio CubieTruck a 32 bit, che a volte utilizzo per varie giocate di test, non sarà possibile elaborare tutti i 128 bit dell'indirizzo in un'unica operazione. Tuttavia, nulla ci impedisce di suddividerlo in gruppi di 32 bit (nel caso in cui non si sappia su quali processori stiamo eseguendo).

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);
    }
}

Per un uso futuro, prevediamo la possibilità di trasmettere IP e ricevere il risultato sia in forma binaria che in forma leggibile. Il parametro $which qui specifica se vogliamo ottenere l'inizio o la fine dell'intervallo (i valori 'start' o 'end' rispettivamente).

Il prossimo compito (che è anche il più pratico per la nostra azienda) è calcolare il prossimo intervallo. Per questo compito non ho trovato nulla di meglio che decomporre l'indirizzo in una stringa binaria e aggiungere 1 nella posizione richiesta, quindi ricomporre il tutto. Per evitare artefatti, ho deciso di suddividere l'indirizzo byte per byte durante la decomposizione e la ricomposizione.

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);
    }
}

In uscita otterremo il prefisso del prossimo intervallo di dimensione specificata in $cidr. Con questa funzione assegniamo blocchi di indirizzi ai nostri clienti.

Infine, dobbiamo verificare se l'indirizzo appartiene all'intervallo. Ad esempio, abbiamo riservato un blocco /48 per l'assegnazione di blocchi /64 ai clienti, e dobbiamo assicurarci di non uscire dai confini del blocco riservato (nella pratica ciò non accadrà presto, ma la possibilità esiste). Qui è tutto semplice. Otteniamo l'inizio e la fine dell'intervallo in formato binario e verifichiamo se l'indirizzo è compreso nei limiti.

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);
}

Spero che sia stato utile. Quali altre funzioni relative agli indirizzi pensi possano essere utili? Qualsiasi aggiunta, commento e code review sono molto apprezzati nei commenti.

Se sei già nostro cliente o stai pensando di diventarlo, in occasione della pubblicazione di questo articolo ti offriamo la possibilità di ricevere un blocco /64 completamente gratuito per tutti i servizi VPS o server dedicato nel data center Equinix Tier IV, Paesi Bassi, su richiesta al reparto vendite, fornendo il link a questo articolo nel ticket. L'offerta è valida fino a marzo 2020.

Un po' di pubblicità 🙂

Grazie per rimanere con noi. Ti piacciono i nostri articoli? Vuoi vedere più contenuti interessanti? Supportaci effettuando un ordine o raccomandandoci a qualcuno. VPS cloud per sviluppatori a partire da $4.99., unica alternativa ai server entry-level, concepita da noi per te: Tutta la verità sui VPS (KVM) E5-2697 v3 (6 Core) 10GB DDR4 480GB SSD 1Gbps a partire da $19 o come dividere correttamente un server? (sono disponibili opzioni con RAID1 e RAID10, fino a 24 core e fino a 40GB DDR4).

Dell R730xd a metà prezzo nel data center Equinix Tier IV ad Amsterdam? Solo da noi 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TB a partire da $199 nei Paesi Bassi! Dell R420 — 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — a partire da $99! Leggi di Come costruire un'infrastruttura di livello enterprise utilizzando server Dell R730xd E5-2650 v4 del valore di 9000 euro a pochi spiccioli?

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster