Working with IPv6 in PHP

Recently, we obtained LIR status and a /29 IPv6 block. Consequently, we needed to keep track of the assigned subnets. Since our billing system is written in PHP, we had to delve into the matter and realize that this language is not the most user-friendly when it comes to working with IPv6. Below is our solution to the tasks that arose regarding addresses and ranges. It may not be the most elegant, but it accomplishes the set tasks.

Working with IPv6 in PHP

A Bit of Theory

Disclaimer. If you are familiar with what IPv6 is and how it works, this section may be boring for you. Or it might not be.

People seeing the IPv6 notation for the first time can be quite bewildered. After the elegant 64.233.177.101 we suddenly encounter 2607:f8b0:4002:c08::8b and may feel lost. Both are simply human-readable representations of 32 and 128 bits, respectively. Every IP packet contains a header with a strictly standardized purpose for each bit. Without delving deeper into the header structure, we need to take away one thing: for operations with IP addresses and ranges, it's generally convenient to use binary math and bitwise operations. Storing them in a database is also easiest as BINARY(4) for IPv4 and BINARY(16) for IPv6.

Another important aspect worth mentioning is network masks and CIDR notation. CIDR stands for Classless Inter-Domain Routing (classless addressing). This concept replaced the class-based approach in determining which part of an 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, while the remaining bits will be set to 0.

In human-readable form, this is recorded as ip.add.re.ss/cidr. For example, 64.233.177.0/24 indicates that the first 24 bits belong to the prefix. The last 8 bits, which is also the last number in the human-readable notation, correspond to the address within the subnet. A couple more exercises. 64.233.177.101/32 and 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, and the remaining 64 bits are the local part. By the way, if anyone is confused by "::" in the notation, the double colon replaces an arbitrary number of sections containing 0. It can appear in the notation only once. In other words, 2607:f8b0:4002:c08::8b = 2607:f8b0:4002:c08:0:0:0:8b.

What do we need to take from all this? First, the first and last subnet address can be obtained using binary AND and OR operations, knowing the mask in binary form. Second, the next subnet size (i.e., with CIDR) n can be calculated by adding 1 to nthe -th position in the binary representation. By binary representation, I mean the result of using the functions pack() and inet_pton() and further using bitwise operators, binary refers to representation in binary system, which can be obtained, for example, using base_convert().

Historical BackgroundClassful addressing preceded classless addressing. In those distant years, no one anticipated that there would be so many subnets; they were distributed freely in large blocks: class A — the prefix was the first 8 bits (i.e., the first number), with a leading bit of 0; class B — the first 16 (the first two numbers), leading bits 10; class C — the first 24 bits, leading bits 110. These leading bits defined the ranges in which addresses of each class were 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 across the planet, regulators realized they had made a mistake, and in the early '90s developed a classless concept that allowed not to be tied to leading bits. A bit more detail can be found, for example, in the great and all-knowing.

Let's move on to practice

In practice, we will implement the three most likely tasks, as I see it:

  1. obtaining the first and last address of the range;
  2. obtaining the next range of a specified size (CIDR);
  3. checking whether an address belongs to a range.

The implementation will be for IPv6, but if necessary, the logic can be easily adapted. Some ideas I drew from from here, but implemented somewhat differently. Also, the examples do not include input error checking. So, let's go.

As I mentioned, the first and last address of the range can be determined using bitwise operations, knowing the start of the range and the binary subnet mask. Accordingly, the first thing we need to do is convert CIDR into a binary mask. For this, we will gather its hex representation and pack it into binary.

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

Call pack(‘H*’, $mask) packs the hex representation in the same way as inet_pton(). With the only difference that when called pack() All 0s should be in their places, and there should be no colons in the notation, unlike human-readable notation.

The next step is to calculate the beginning and end of the range. Here, nuances will arise. Bitwise operations are limited by the processor's bitness. Consequently, on my 32-bit CubieTruck, which I sometimes use for various testing fun, it's not possible to process all 128 bits of the address in one operation. However, nothing prevents us from breaking it down 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 allow the IP to be passed and the result obtained in both binary and human-readable forms. The parameter $which here indicates whether we want to get the start or end of the range (the values 'start' or 'end' respectively).

The next task (and the most practical for our company) is to calculate the next range. For this task, nothing better came to mind than to decompose the address into a binary string and add 1 in the desired position, after which to fold it back. To avoid artifacts, I decided to break the address down 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);
    }
}

We will get the prefix of the next range of the size 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 to distribute /64 blocks to clients, and we need to ensure that when allocating, we do not go beyond the allocated block (in practice, this won't happen soon, but there is still a probability). It's straightforward. We get the start and end of the range in binary form and check whether the address is within bounds.

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 this was helpful. What other functions for working with addresses do you think would be useful? Any additions, comments, and code reviews are warmly welcomed in the comments.

Whether you are already our client or are considering becoming one, to celebrate the release of this article, we offer you a free /64 block for all VPS or dedicated server services in the Equinix Tier IV data center in the Netherlands upon your request to the sales department, providing a link to this article in your ticket. The offer is valid until March 2020.

A little advertisement 🙂

Thank you for staying with us. Do you enjoy our articles? Want to see more interesting content? Support us by placing an order or recommending us to your friends, cloud VPS for developers starting at $4.99, a unique entry-level server alternative that we have created for you: The whole truth about VPS (KVM) E5-2697 v3 (6 Cores) 10GB DDR4 480GB SSD 1Gbps from $19 or how to properly share a server? (options available with RAID1 and RAID10, up to 24 cores and up to 40GB DDR4).

Dell R730xd at half the price in the Equinix Tier IV data center in Amsterdam? Only with us 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100TB starting at $199 in the Netherlands! Dell R420 — 2x E5-2430 2.2GHz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — from $99! Read about how To build a corporate-class infrastructure using Dell R730xd E5-2650 v4 servers costing 9000 euros for peanuts?

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster