Lucrul cu IPv6 în PHP

Recent, am obținut statutul de LIR și un bloc IPv6 /29. Apoi a apărut necesitatea de a ține evidența subrețelelor alocate. Și cum sistemul nostru de facturare este scris în PHP, a trebuit să ne adâncim un pic în problemă și să realizăm că acest limbaj nu este cel mai prietenos pentru lucrul cu IPv6. Sub titlu — soluția noastră pentru problemele apărute în lucrul cu adresele și intervalele. Poate că nu este cea mai elegantă, dar îndeplinește sarcinile stabilite.

Lucrul cu IPv6 în PHP

Puțină teorie

Declinarea responsabilității. Dacă ești familiarizat cu ceea ce este IPv6 și cu ce se mănâncă, această parte poate fi plictisitoare pentru tine. Sau poate nu.

Oamenii care văd pentru prima dată o notare IPv6 pot fi destul de descurajați. După elegantele 64.233.177.101 ne confruntăm brusc cu 2607:f8b0:4002:c08::8b și putem fi confuzi. Ambele sunt doar reprezentări ușor de înțeles ale a 32 și 128 de biți, respectiv. Fiecare pachet IP conține o antet cu scopul standardizat pentru fiecare bit. Fără a intra prea adânc în structura antetelor, ceea ce trebuie să reținem este că pentru operațiunile cu adrese IP și intervale este mai convenabil să utilizăm matematica binară și operații pe biți. Este, de asemenea, cel mai convenabil să le stocăm în baza de date ca BINARY(4) pentru IPv4 și BINARY(16) pentru IPv6.

Un alt aspect important care ar merita să fie abordat este măștile de rețea și notația CIDR. CIDR este un acronim pentru Classless Inter-Domain Routing (adresare fără clase). Această concepție a înlocuit clasică în problema definirii, care parte a adresei IP este prefixul rețelei și care este adresa interfeței de rețea din acea rețea. În practică, primii n biți corespunzători prefixului vor avea valorile setate la 1, ceilalți la 0.

Într-o formă ușor de înțeles, este scris ca ip.add.re.ss/cidr. De exemplu, 64.233.177.0/24 indică faptul că primii 24 de biți sunt rezervati pentru prefix. Ultimii 8 biți, care sunt și ultimul număr din notarea ușor de înțeles, sunt adresa din interiorul subrețelei. Iată câteva exerciții. 64.233.177.101/32 și 2607:f8b0:4002:c08::8b/128 — o adresă specifică. 2607:f8b0:4002:c08::/64 — primii 64 de biți (primele 4 grupuri) sunt prefixul, ceilalți 64 de biți sunt partea locală. Apropo, dacă pe cineva îl deranjează "::" în notare, punctele de două ori înlocuiesc un număr aleatoriu de secțiuni care conțin 0. Acesta poate apărea în notare doar o dată. Cu alte cuvinte, 2607:f8b0:4002:c08::8b = 2607:f8b0:4002:c08:0:0:0:8b.

Ce trebuie să extragem din toate acestea? În primul rând, prima și ultima adresă a subrețelei pot fi obținute folosind AND și OR binar, cunoscând masca în format binar. În al doilea rând, următoarea subrețea de o anumită dimensiune (adică cu CIDR) n poate fi calculată prin adăugarea de 1 la n-a poziție în reprezentarea binară. Prin forma binară, mă refer la rezultatul utilizării funcțiilor pack() și inet_pton() și utilizarea ulterioară a operatoarelor bitwise, prin binar înțelegând reprezentarea în sistemul binar, care poate fi obținută, de exemplu, cu ajutorul base_convert().

Context istoricCoresponderea clasică a fost precedată de segmentarea adreselor. În acele vremuri îndepărtate, nimeni nu se aștepta ca subrețelele să fie atât de numeroase, fiind distribuite pe direcții și în stânga și în dreapta în blocuri mari: clasa A - prefixul erau primele 8 biți (adică primul număr), cu bitul de lider 0; clasa B - primele 16 (primele două numere), biții conducători 10; clasa C - primii 24 de biți, biții de conducere 110. Acești biți de lider stabileau intervalele în care se emitea adresa unei clase anume: 0.0.0.0 — 127.255.255.255 pentru clasa A, 128.0.0.0 — 191.255.255.255 - clasa B, 192.0.0.0 - 223.255.255.255 - clasa C. Pe măsură ce internetul s-a extins pe planetă, reglatorii au realizat că au greșit și la începutul anilor '90 au dezvoltat conceptul fără clasă, permițându-le să nu se mai lege de biții de conducere. Câteva detalii suplimentare pot fi găsite, de exemplu, în marea și omniscienta.

Să trecem la practică

În practică, voi implementa cele trei probleme cele mai probabile, așa cum mi s-a părut:

  1. obținerea primei și ultimei adrese a intervalului;
  2. obținerea următorului interval de dimensiune specificată (CIDR);
  3. verificarea apartenenței unei adrese intervalului.

Implementarea va fi pentru IPv6, dar, dacă este necesar, logica poate fi adaptată cu ușurință. Unele idei le-am obținut de aici, dar am implementat-o puțin diferit. De asemenea, în exemple nu există verificări pentru erorile de intrare. Așadar, să începem.

Așa cum am menționat, prima și ultima adresă a intervalului pot fi determinate folosind operații bitwise, știind începutul intervalului și masca binară a subrețelei. Prin urmare, prima noastră sarcină este să transformăm CIDR în masca binară. Pentru aceasta, să adunăm reprezentarea sa hex și să o împachetăm în binară.

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

Apel pack(‘H*’, $mask) împachetează reprezentarea hex într-un mod similar cu inet_pton(). Cu mențiunea că la apelarea pack() toate cele 0 trebuie să fie la locul lor, iar două puncte în înregistrare nu ar trebui să existe, spre deosebire de o înregistrare ușor de înțeles.

Următorul pas este calculul începutului și sfârșitului intervalului. Și aici vor apărea nuanțe. Operatiunile pe biți sunt limitate de arhitectura procesorului. În consecință, pe CubieTruck-ul meu de 32 de biți, pe care îl folosesc uneori pentru diverse teste, nu putem procesa toți cei 128 de biți ai unei adrese într-o singură operație. Totuși, nimic nu ne împiedică să-l împărțim în grupuri de 32 de biți (pentru că nu se știe pe ce procesoare vom rula).

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

Pentru utilizări viitoare, vom prevedea posibilitatea de a transmite IP-ul și de a obține rezultatul atât în formă binară, cât și în formă ușor de citit. Parametrul $which aici stabilește dacă dorim să obținem începutul sau sfârșitul intervalului (valorile 'start' sau 'end' respectiv).

Următoarea sarcină (de altfel, cea mai practică pentru compania noastră) este calculul următorului interval. Pentru această sarcină, nu mi-a venit în minte nimic mai bun decât să descompun adresa într-un șir binar și să adaug 1 la poziția dorită, după care să reasamblez totul. Pentru a evita apariția artefactelor, am decis să descompun și să asamblez adresa pe byte.

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

La ieșire, vom obține prefixul următorului interval de dimensiunea specificată în $cidr. Această funcție ne permite să alocăm blocuri de adrese clienților noștri.

În cele din urmă, verificăm dacă adresa aparține intervalului. De exemplu, am alocat un bloc /48 pentru a distribui clienților blocuri /64, și trebuie să ne asigurăm că atunci când alocăm nu depășim limitele blocului alocat (în practică, acest lucru nu se va întâmpla prea curând, dar tot există o probabilitate). Aici totul e simplu. Obținem începutul și sfârșitul intervalului în formă binară și verificăm dacă adresa se află în limite.

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

Sper că a fost util. Ce alte funcții pentru lucrul cu adrese ar putea fi utile, după părerea dumneavoastră? Orice adăugiri, comentarii și revizii de cod sunt foarte binevenite în comentarii.

Dacă sunteți deja clientul nostru sau doar vă gândiți să deveniți, cu ocazia publicării acestui articol, vă oferim un bloc /64 complet gratuit pentru toate serviciile VPS sau pentru un server dedicat în centrul de date Equinix Tier IV, Olanda, la cererea departamentului de vânzări, oferind linkul către acest articol în ticket. Oferta este valabilă până în martie 2020.

Puțin publicitate 🙂

Mulțumim că rămâneți cu noi. Vă plac articolele noastre? Doriți să vedeți mai multe materiale interesante? Susțineți-ne, efectuând o comandă sau recomandându-ne prietenilor, VPS cloud pentru dezvoltatori de la 4,99 $, un echivalent unic pentru serverele entry-level, care a fost creat de noi pentru voi: Toată adevărul despre VPS (KVM) E5-2697 v3 (6 nuclee) 10GB DDR4 480GB SSD 1Gbps de la 19 $ sau cum să împărțiți corect un server? (sunt disponibile opțiuni cu RAID1 și RAID10, până la 24 nuclee și până la 40GB DDR4).

Dell R730xd la jumătate de preț în centrul de date Equinix Tier IV din Amsterdam? Numai la noi 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TB de la 199 $ în Olanda! Dell R420 — 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — de la 99 $! Citiți despre Cum să construiți o infrastructură de clasă enterprise folosind servere Dell R730xd E5-2650 v4 la prețuri foarte mici de 9000 €?

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster