We recently received LIR status and a /29 IPv6 block. And then there was a need to keep track of the assigned subnets. And since our billing is written in PHP, we had to get a little imbued with the issue and realize that this language is not the friendliest in terms of working with IPv6. Under the cut is our solution to the problems that have arisen with working with addresses and ranges. Perhaps not the most elegant, but it does the job.

Some theory
Disclaimer If you are familiar with what IPv6 is and how it is eaten, this part may be boring for you. Maybe not.
For people seeing the IPv6 annotation for the first time, it may well be confusing. After elegant 64.233.177.101 we are suddenly faced with 2607:f8b0:4002:c08::8b and we can get lost. Both are just human-readable representations of 32 and 128 bits, respectively. Any IP packet contains a header with a strictly standardized assignment of each bit. Without going even deeper into the structure of headers, we need to take away one thing from here: for operations with IP addresses and ranges, it is generally convenient to use binary mathematics and bitwise operations. It is also most convenient to store them in the database as BINARY(4) for IPv4 and BINARY(16) for IPv6.
Another important aspect worth mentioning is netmasks and CIDR notation. CIDR is an acronym for Classless Inter-Domain Routing (). This concept has replaced the class concept in the matter of determining which part of the IP address is the network prefix, and which part is the address of the network interface within this network. In practice, the first n bits corresponding to the prefix will be set to 1, the rest to 0.
In human-readable form, this is written as ip.add.re.ss/cidr. For example, 64.233.177.0/24 means that the first 24 bits refer to the prefix. The last 8 bits, they are also the last number in human-readable notation, refer to the address within the subnet. A couple more exercises. 64.233.177.101/32 и 2607:f8b0:4002:c08::8b/128 - one specific address. 2607:f8b0:4002:c08::/64 - the first 64 bits (the first 4 groups) are the prefix, the remaining 64 bits are the local part. By the way, if anyone is confused by the "::" in the notation, the double colon replaces an arbitrary number of sections containing 0. It can only occur once in the annotation. In other words, 2607:f8b0:4002:c08::8b = 2607:f8b0:4002:c08:0:0:0:8b.
What do we need to learn from all this? First, the first and last address of the subnet can be obtained using binary AND and OR, knowing the mask in binary form. Second, the next size subnet (i.e. with CIDR) n can be calculated by adding 1 to n-that position in the binary representation. By binary, I mean the result of using functions pack() и inet_pton() and further use , under binary - a representation in the binary system, which can be obtained, say, using base_convert().
Historical informationClassless addressing preceded class segregation. In those distant years, no one imagined that there would be so many subnets, they were handed out to the right and left in large blocks: class A - the first 8 bits (i.e. the first number) were the prefix, with the leading bit 0; class B - first 16 (first two numbers), leading bits 10; class C - the first 24 bits, the leading bits are 110. These very leading bits set the ranges in which the address of a particular class was issued: 0.0.0.0 - 127.255.255.255 for class A, 128.0.0.0 - 191.255.255.255 - class B, 192.0.0.0 - 223.255.255.255 - class C. As the Internet spread around the planet, regulators realized that they had missed, and in the early 90s they developed a classless concept that allowed them not to become attached to the leading bits. A little more detail can be gleaned, for example, in .
Let's move on to practice
In practice, we implement the three most likely, as it seemed to me, tasks:
- getting the first and last address of the range;
- getting the next specified size range (CIDR);
- checking if the address belongs to a range.
The implementation will be for IPv6, but the logic can be easily adapted if necessary. Some ideas I got , but implemented a little differently. Also, there is no check for input errors in the examples. So let's go.
As I already mentioned, the first and last address of a range can be determined using bitwise operations, knowing the beginning of the range and the binary subnet mask. Accordingly, the first thing we need to do is turn CIDR into a binary mask. To do this, let's collect its hex representation and pack it into a binary one.
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);
}Вызов pack('H*', $mask) packs the hex representation in the same way as inet_pton(). The only difference is that when you call pack() all 0s must be in their places, and there should be no colons in the entry, unlike human-readable notation.
The next step is to calculate the start and end of the range. And this is where the nuances come in. Bitwise operations are limited by the processor capacity. Accordingly, on my 32-bit CubieTruck, which I sometimes use for all kinds of test pranks, all 128 bits of the address cannot be processed in one operation. However, nothing prevents us from splitting it into groups of 32 bits (just in case, who knows what processors we will run on).
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);
}
}For future use, we will provide the ability to transmit IP and receive the result in both binary and human-readable form. Parameter $which here sets whether we want to get the beginning or end of the range (values 'start' or 'end' respectively).
The next task (and the most practical for our company) is the calculation of the next range. For this task, nothing better came to mind, except to decompose the address into a binary string and add 1 in the desired position, and then roll everything back. So that artifacts would not appear anywhere, I decided to split the address by byte during decomposition and assembly.
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);
}
}The output will be the prefix of the next size range specified in $cidr. With this function, we allocate blocks of addresses to our clients.
Finally, checking if the address belongs to the range. For example, we allocated one block /48 for distributing blocks to clients /64, and we need to make sure that when assigning we do not go beyond the allocated block (in practice this will not happen soon, but there is still a possibility). Everything is simple here. We get the beginning and end of the range in binary form and check whether the address is within the limits.
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);
}I hope it was helpful. What other functions for working with addresses can be useful in your opinion? Any additions, comments and code reviews are warmly welcome in the comments.
If you are already our client or are just thinking about becoming one, on the occasion of the publication of this article we offer you to receive a block /64 completely free for all vps services or dedicated server in the Equinix Tier IV data center, the Netherlands upon request to the sales department by providing a link to this article in the ticket. The offer is valid until March 2020.
Some ads 🙂
Thank you for staying with us. Do you like our articles? Want to see more interesting content? Support us by placing an order or recommending to friends, , a unique analogue of entry-level servers, which was invented by us for you: (available with RAID1 and RAID10, up to 24 cores and up to 40GB DDR4).
Dell R730xd 2 times cheaper in Equinix Tier IV data center in Amsterdam? Only here in the Netherlands! Dell R420 - 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB - from $99! Read about
Source: habr.com
