Praca z IPv6 w PHP

Ostatnio uzyskaliśmy status LIR i blok IPv6 /29. A zaraz potem pojawiła się potrzeba prowadzenia rejestru przypisanych podsieci. Skoro nasz system billingowy jest napisany w PHP, musieliśmy się trochę zgłębić w temat i uświadomić sobie, że ten język nie jest zbyt przyjazny jeśli chodzi o pracę z IPv6. Poniżej przedstawiamy nasze rozwiązania dotyczące pracy z adresami i zakresami. Może nie jest to najdoskonalsze, ale realizuje postawione zadania.

Praca z IPv6 w PHP

Trochę teorii

Zastrzeżenie. Jeśli znasz już, czym jest IPv6 i co z tym się wiąże, ta część może wydać Ci się nudna. Możliwe, że tak nie będzie.

Ludzi, którzy po raz pierwszy widzą adnotację IPv6, może to całkowicie zaskoczyć. Po eleganckich 64.233.177.101 nagle stykamy się z 2607:f8b0:4002:c08::8b i możemy się zagubić. Jedno i drugie to tylko ludzkie odpowiedniki 32 i 128 bitów odpowiednio. Każdy pakiet IP zawiera nagłówek z ściśle znormalizowanym przeznaczeniem każdego bitu. Nie zagłębiając się jeszcze bardziej w strukturę nagłówków, musimy stąd wyciągnąć jedną rzecz: do operacji na adresach IP i zakresach głównie wygodniej jest używać matematyki binarnej i operacji bitowych. Przechowywanie ich w bazie danych również jest najwygodniejsze jako BINARY(4) dla IPv4 i BINARY(16) dla IPv6.

Kolejnym ważnym aspektem, który warto poruszyć, są maski sieci i notacja CIDR. CIDR to akronim od Classless Inter-Domain Routing (adresowanie bezklasowe). Ta koncepcja zastąpiła klasyczną w kwestii określenia, która część adresu IP jest prefiksem sieci, a która jest adresem interfejsu sieciowego w tej sieci. W praktyce pierwsze n bitów, odpowiadające prefiksowi, będą miały wartość 1, a pozostałe - 0.

W ludzkim zrozumieniu zapisuje się to w formie ip.add.re.ss/cidr. Na przykład, 64.233.177.0/24 oznacza, że pierwsze 24 bity odnoszą się do prefiksu. Ostatnie 8 bitów, czyli ostatnia liczba w adnotacji ludzkiej, odnosi się do adresu wewnątrz podsieci. Jeszcze kilka ćwiczeń. 64.233.177.101/32 i 2607:f8b0:4002:c08::8b/128 to jeden konkretny adres. 2607:f8b0:4002:c08::/64 to pierwsze 64 bity (pierwsze 4 grupy) - prefiks, pozostałe 64 bity to część lokalna. Zresztą, jeśli kogoś niepokoi "::" w zapisie, podwójny dwukropek zastępuje dowolną liczbę sekcji zawierających 0. Może występować w adnotacji tylko 1 raz. Innymi słowy, 2607:f8b0:4002:c08::8b = 2607:f8b0:4002:c08:0:0:0:8b.

Co powinniśmy z tego wszystkiego wyciągnąć? Po pierwsze, pierwszy i ostatni adres podsieci można uzyskać za pomocą bitowych operacji AND i OR, znając maskę w postaci binarnej. Po drugie, następną podsieć o określonej wielkości (tzn. w CIDR) n można obliczyć, dodając 1 do n-tej pozycji w binarnej reprezentacji. Pod pojęciem binarnym rozumiem wynik użycia funkcji pack() i inet_pton() i dalsze wykorzystanie operatorów bitowych, przez binarne - reprezentację w systemie binarnym, którą można uzyskać, powiedzmy, przy użyciu base_convert().

Kontekst historycznySkrócona adresacja klasowa wyprzedza bezklasową. W tych dawnych latach nikt nie przypuszczał, że będzie tak wiele podsieci, rozdawano je na prawo i lewo w dużych blokach: klasa A - prefiksem były pierwsze 8 bitów (tzn. pierwsza liczba), z wiodącym bitem 0; klasa B - pierwsze 16 (pierwsze dwie liczby), wiodące bity 10; klasa C - pierwsze 24 bity, wiodące bity 110. Te wiodące bity określały zakresy, w których przydzielano adres danej klasy: 0.0.0.0 — 127.255.255.255 dla klasy A, 128.0.0.0 — 191.255.255.255 — klasa B, 192.0.0.0 — 223.255.255.255 — klasa C. W miarę jak internet rozprzestrzeniał się po planecie, regulatorzy zrozumieli, że się pomylili i na początku lat 90-tych opracowali koncepcję bezklasową, która pozwalała na niezwiązanie się z wiodącymi bitami. Nieco więcej szczegółów można znaleźć, powiedzmy, w wielkiej i wszechwiedzącej.

Przejdźmy do praktyki

W praktyce zrealizujemy trzy najprawdopodobniejsze, jak mi się wydaje, zadania:

  1. uzyskanie pierwszego i ostatniego adresu w zakresie;
  2. uzyskanie następnego zakresu o określonej wielkości (CIDR);
  3. sprawdzenie, czy adres należy do zakresu.

Realizacja będzie dla IPv6, ale w razie potrzeby logika jest łatwa do dostosowania. Niektóre pomysły zaczerpnąłem stąd, ale wdrożyłem je nieco inaczej. W przykładach również nie ma sprawdzania błędów wejściowych. Zatem, ruszamy.

Jak już wspomniałem, pierwszy i ostatni adres w zakresie można określić za pomocą operacji bitowych, znając początek zakresu i binarną maskę podsieci. W związku z tym, w pierwszej kolejności musimy przekształcić CIDR w binarną maskę. Do tego zbierzemy jej reprezentację hex i zapakujemy w binarną.

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

Wywołanie pack(‘H*’, $mask) pakowanie heksadecymalnej reprezentacji w taki sam sposób, jak i inet_pton(). Z tą różnicą, że przy wywołaniu pack() wszystkie 0 muszą być na swoich miejscach, a w zapisie nie powinno być dwukropków, w przeciwieństwie do zapisu czytelnego dla człowieka.

Następnym krokiem jest obliczenie początku i końca zakresu. I tu pojawią się niuanse. Operacje bitowe są ograniczone rozdzielczością procesora. Odpowiednio, na moim 32-bitowym CubieTrucku, który czasem używam do różnych testowych zabaw, nie da się przetworzyć wszystkich 128 bitów adresu w jednej operacji. Jednak nic nie stoi na przeszkodzie, by podzielić go na grupy po 32 bity (na wszelki wypadek, kto wie, na jakich procesorach będziemy uruchamiać).

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

Dla przyszłego użycia przewidzimy możliwość przekazywania IP i otrzymywania rezultatu zarówno w postaci binarnej, jak i czytelnej dla człowieka. Parametr $which określa, czy chcemy otrzymać początek, czy koniec zakresu (wartości ‘start’ lub ‘end’ odpowiednio).

Następnym zadaniem (co więcej, najbardziej praktycznym dla naszej firmy) jest obliczenie następnego zakresu. Dla tego zadania nie przyszło mi do głowy nic lepszego niż rozłożyć adres na ciąg binarny i dodać 1 w odpowiedniej pozycji, a następnie złożyć wszystko z powrotem. Aby nigdzie nie pojawiły się artefakty, postanowiłem przy rozkładaniu i składaniu dzielić adres na bajty.

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

Na wyjściu uzyskamy prefiks następnego zakresu o rozmiarze określonym w $cidr. Tą funkcją wydzielamy bloki adresów naszym klientom.

W końcu, sprawdzenie, czy adres należy do zakresu. Na przykład, wydzieliliśmy jeden blok /48 do przydzielania klientom bloków /64 i musimy upewnić się, że przy przypisywaniu nie wychodzimy poza przydzielony blok (w praktyce zdarzy się to niedługo, ale jednak ryzyko jest). Tutaj wszystko jest proste. Otrzymujemy początek i koniec zakresu w postaci binarnej i sprawdzamy, czy adres mieści się w granicach.

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

Mam nadzieję, że to było przydatne. Jakie inne funkcje dotyczące adresów, Pana zdaniem, mogą być pomocne? Każde dodatkowe uwagi, komentarze i przeglądy kodu są mile widziane w komentarzach.

Jeśli już jesteś naszym klientem lub tylko rozważasz zostanie nim, z okazji wydania tego artykułu oferujemy Ci blok /64 zupełnie za darmo dla wszystkich usług VPS lub dedykowanego serwera w centrum danych Equinix Tier IV w Holandii na życzenie działu sprzedaży, przedstawiając link do tego artykułu w zgłoszeniu. Oferta ważna do marca 2020 roku.

Trochę reklamy 🙂

Dziękujemy, że jesteś z nami. Podobają Ci się nasze artykuły? Chcesz zobaczyć więcej interesujących materiałów? Wspieraj nas składając zamówienie lub polecając nas znajomym, chmurowe VPS dla programistów od 4,99 $, unikatowy odpowiednik serwerów entry-level, który został stworzony przez nas dla Ciebie: Cała prawda o VPS (KVM) E5-2697 v3 (6 rdzeni) 10GB DDR4 480GB SSD 1Gbps od 19 $ lub jak prawidłowo podzielić serwer? (dostępne opcje z RAID1 i RAID10, do 24 rdzeni i do 40GB DDR4).

Dell R730xd dwa razy tańszy w centrum danych Equinix Tier IV w Amsterdamie? Tylko u nas 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100TB od 199 dolarów w Holandii! Dell R420 — 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — od 99 dolarów! Czytaj o tym Jak zbudować infrastrukturę klasy korporacyjnej z zastosowaniem serwerów Dell R730xd E5-2650 v4 kosztujących 9000 euro za grosze?

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster