Nyligen fick vi status som LIR och en /29 block IPv6. Och då uppstod behovet av att hålla reda på tilldelade subnät. Eftersom vår fakturering är skriven i PHP, var vi tvungna att dyka ner i frågan och inse att detta språk inte är det mest användarvänliga när det gäller arbete med IPv6. Under rubriken - vår lösning på de problem som uppstod kring adresser och intervall. Kanske inte den mest eleganta, men de uppsatta uppgifterna löses.

Lite teori
Avskedsanmärkning. Om du är bekant med vad IPv6 är och hur det används, kan den här delen vara tråkig för dig. Kanske inte.
Människor som ser en IPv6-annotation för första gången kan bli förvirrade. Efter de eleganta 64.233.177.101 konfronteras vi plötsligt med 2607:f8b0:4002:c08::8b och vi kan bli osäkra. Både och är bara mänskligt förståeliga representationer av 32 respektive 128 bitar. Varje IP-paket innehåller en rubrik med en strikt standardiserad avsikt för varje bit. Utan att gå djupare in i rubrikens struktur, behöver vi härifrån ta med oss en sak: för operationer med IP-adresser och intervall är det mest praktiskt att använda binär matematik och bitvisa operationer. Att lagra dem i databasen är också enklast som BINARY(4) för IPv4 och BINARY(16) för IPv6.
En annan viktig aspekt som är värd att nämna är nätmasker och CIDR-notering. CIDR är en förkortning för Classless Inter-Domain Routing (). Detta koncept ersatte den klassbaserade metoden för att definiera vilken del av IP-adressen som är nätverksprefix och vilken del som är nätverksgränssnittsadress inom detta nätverk. I praktiken kommer de första n bitarna som motsvarar prefixet att sättas till värdet 1, medan de övriga sätts till 0.
I ett mänskligt förståeligt format skrivs detta som ip.add.re.ss/cidr. Till exempel, 64.233.177.0/24 som anger att de första 24 bitarna tillhör prefixet. De sista 8 bitarna, alltså det sista numret i den mänskligt förståeliga skrivningen, relaterar till adressen inom subnätet. Några fler övningar. 64.233.177.101/32 och 2607:f8b0:4002:c08::8b/128 — en specifik adress. 2607:f8b0:4002:c08::/64 — de första 64 bitarna (de första 4 grupperna) är prefixet, medan de återstående 64 bitarna är den lokala delen. Förresten, om någon undrar över "::" i skrivningen, så ersätter de dubbla kolon ett godtyckligt antal sektioner som innehåller 0. Det kan förekomma i annotationen endast 1 gång. Med andra ord, 2607:f8b0:4002:c08::8b = 2607:f8b0:4002:c08:0:0:0:8b.
Vad kan vi lära oss av detta? För det första kan vi få det första och sista IP-adressen i subnätet med hjälp av binär AND och OR, genom att känna till masken i binär form. För det andra kan det nästa subnätet av storlek (dvs. med CIDR) n beräknas genom att lägga till 1 till n-te positionen i det binära representationen. Med binär avser jag resultatet av att använda funktionerna pack() och inet_pton() och vidare användning av , med binär menar jag representationen i det binära systemet som kan fås, låt oss säga, med hjälp av base_convert().
Historisk översiktKlassbaserad adressering föregick utan klass. På den tiden förutsåg ingen att det skulle bli så många subnät, de delades ut höger och vänster i stora block: klass A - prefixet var de första 8 bitarna (dvs. det första numret), med ledande bit 0; klass B - de första 16 (de första två numren), ledande bitar 10; klass C - de första 24 bitarna, ledande bitar 110. Dessa ledande bitar definierade intervallen inom vilka adresser av olika klasser utdelades: 0.0.0.0 — 127.255.255.255 för klass A, 128.0.0.0 — 191.255.255.255 — klass B, 192.0.0.0 — 223.255.255.255 — klass C. I takt med att internet spreds över planeten insåg regleringsmyndigheterna att de hade gjort ett misstag, och i början av 90-talet utvecklade de en klasslös koncept som tillät dem att inte knyta sig till de ledande bitarna. Lite mer information kan man få, låt oss säga, i .
Låt oss gå till praktiken
I praktiken implementerar vi de tre mest sannolika uppgifter som jag har observerat:
- att få den första och sista adressen i intervallet;
- att få nästa intervall av angiven storlek (CIDR);
- att kontrollera om en adress tillhör intervallet.
Implementeringen kommer att vara för IPv6, men logiken kan enkelt anpassas vid behov. Vissa idéer har jag hämtat , men jag implementerade dem lite annorlunda. Dessutom finns det ingen kontroll för inmatningsfel i exemplen. Så låt oss sätta igång.
Som jag nämnde tidigare kan den första och sista adressen i intervallet fastställas med hjälp av bitoperationer, så länge vi känner till början av intervallet och den binära subnätmasken. Först måste vi därför omvandla CIDR till en binär mask. För att göra detta ska vi samla in dess hex-representation och packa in den binärt.
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);
}Anrop pack(‘H*’, $mask) packar hex-representationen på samma sätt som inet_pton(). Med den enda skillnaden att vid anrop pack() alla 0 måste vara på sina platser, och det får inte finnas några kolon i posten, till skillnad från människovänlig notering.
Nästa steg är att beräkna början och slutet av intervallet. Och här uppstår det nyanser. Bitvisa operationer är begränsade av processorens bitdjup. Därför, på min 32-bitars CubieTruck, som jag ibland använder för olika tester, kommer jag inte att kunna behandla alla 128 bitar av adressen i en enda operation. Men ingenting hindrar oss från att dela upp den i grupper om 32 bitar (för säkerhets skull, vem vet vilka processorer vi ska köra på).
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);
}
}För framtida bruk beräknar vi möjligheten att överföra IP och få resultatet både i binär och i människovänlig form. Parametern $which här anger om vi vill ha början eller slutet av intervallet (värdena ‘start’ eller ‘end’ respektive).
Nästa uppgift (som dessutom är den mest praktiska för vårt företag) är att beräkna nästa intervall. För denna uppgift kom jag inte på något bättre än att bryta ner adressen i en binär sträng och lägga till 1 på rätt position, för att sedan rulla ihop allt igen. För att undvika artefakter beslutade jag att dela upp adressen byte för byte vid nedbrytning och ihopslagning.
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);
}
}I slutändan får vi prefixet för nästa intervall i den storlek som anges i $cidr. Med denna funktion tilldelar vi adressblock till våra kunder.
Slutligen, kontrollen av om adressen tillhör intervallet. Till exempel har vi tilldelat ett block /48 för att dela ut /64-block till kunder, och vi måste se till att vi vid tilldelning inte går utanför det tilldelade blocket (i praktiken kommer detta troligtvis inte hända snart, men det finns ändå en risk). Här är det enkelt. Vi får början och slutet av intervallet i binär form och kontrollerar om adressen ligger inom gränserna.
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);
}Jag hoppas att detta har varit till nytta. Vilka andra funktioner för IP-adresser tycker du skulle kunna vara användbara? Eventuella tillägg, kommentarer och kodgranskningar tas emot med glädje i kommentarerna.
Om du redan är vår kund eller bara överväger att bli det, erbjuder vi dig en gratis /64-block för alla VPS-tjänster eller dedikerade servrar i Equinix Tier IV-datacenter i Nederländerna som en del av denna artikel. Erbjudandet gäller till mars 2020.
Lite reklam 🙂
Tack för att ni stannar kvar hos oss. Gillar ni våra artiklar? Vill ni se fler intressanta material? Stöd oss genom att göra en beställning eller rekommendera oss till era vänner, , en unik motsvarighet till entry-level servrar som vi har skapat för er: (alternativ med RAID1 och RAID10 finns tillgängliga, upp till 24 kärnor och upp till 40GB DDR4).
Dell R730xd är två gånger billigare i datacentret Equinix Tier IV i Amsterdam? Bara hos oss i Nederländerna! Dell R420 — 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — från $99! Läs om
Källa: habr.com
