Dealing with human readable network addresses

Posted 18 October 2009

Let’s say we need to build a list of network addresses to whitelist access to a restricted area, or blacklist a troublesome spammer. It’s handy to be able to enter data in a variety of formats, including hostnames, IP addresses, and IP ranges.

Usually we will store this information as a long, then we can easily see if a requesting IP address falls within our list of networks simply by numerical comparison, specifically: start_range < requesting_ip < end_range.

However, our user doesn’t want to enter the list of IPs as a long so we need tools to convert between an attractive input format, and an array of start and end ranges.

To accomplish this, we define two functions network_expand_range and network_compact_range. Let’s have a look:

<?php
/**
 * Take small human readable network and give dotted quad range
 *
 * Acceptable inputs:
 *   1.1.1.1 - 1.1.255.255
 *   2.2.2.0-255
 *   3.3.3.*
 *   4.4.*.*
 *   5.5.5.20-40
 *   google.com
 *
 * @author Aidan Lister <aidan@php.net>
 * @link http://aidanlister.com/2009/10/dealing-with-human-readable-network-addresses/
 * @param input $string A newline separated list of network ranges
 * @return array Start and end addresses in dotted quads, or false
 */
function network_expand_range($network)
{
    // Sanity check    
    if (empty($network)) {
        return false;
    }

    $has_star = strpos($network, '*');
    $has_dash = strpos($network, '-');
    
    // Last character of a domain must be in the alphabet
    if (ctype_alpha($network[strlen($network)-1])) {
        if (!$address = gethostbyname($network)) {
          return false;
        }
        return array($address, $address);
    } 

    // Simple
    if ($has_star === false && $has_dash === false) {    
        $res   = long2ip(ip2long($network));
        $start = $end = $res;
        if ($res === '0.0.0.0') {
            return false;
        }
    }
        
    // Using a star
    if ($has_star !== false) {
        $start = long2ip(ip2long(str_replace('*', '0', $network)));
        $end   = long2ip(ip2long(str_replace('*', '255', $network)));
        if ($start === '0.0.0.0' || $end === '0.0.0.0') {
            return false;
        }                
    }
        
    // Using a dash
    if ($has_dash !== false) {
        list($start, $end) = explode('-', $network);
        
        // Check whether end is a full IP or just the last quad
        if (strpos($end, '.') !== false) {
            $end = long2ip(ip2long(trim($end)));
        } else {
            // Get the first 3 quads of the start address
            $classc = substr($start, 0, strrpos($start, '.'));
            $classc .= '.' . $end;
            $end = long2ip(ip2long(trim($classc)));
        }
        
        // Check for failure
        $start = long2ip(ip2long(trim($start)));
        if ($start === '0.0.0.0' || $end === '0.0.0.0') {
            return false;
        }
    }

    return array($start, $end);
}
?>

Next, the sister function to reverse the expansion process:

<?php
/**
 * Convert start and end address into small human readable string
 *
 * For example, $s=1.1.1.1 and $e=1.1.255.255 into 1.1.*.*
 *
 * @author Aidan Lister <aidan@php.net>
 * @link http://aidanlister.com/2009/10/dealing-with-human-readable-network-addresses/
 * @param int $start The start address
 * @param int $end The end address
 * @return string A start and end range as the small formatted string
 */
function network_compact_range($start, $end)
{
    if ($start === $end) {
        $output = $start;
    } else {
        $s = explode('.', $start);
        $e = explode('.', $end);
        if ($s[0] === $e[0] && $s[1] === $e[1] && $s[2] === $e[2]) {
            if ($s[3] === '0' && $e[3] === '255') {
                $s[3] = '*';
                $output = implode('.', $s);
            } else {
                $s[3] = sprintf('%s-%s', $s[3], $e[3]);
                $output = implode('.', $s);
            }
        } else {
            $output = $start .' - '. $end;
        }
    }
            
    return $output;
}
?>

To show how it all works, let’s look at some examples:

<?php
// Example input (maybe from a textarea, database or file)
$input = array();
$input[] = '1.1.1.1 - 1.1.255.255';
$input[] = '2.2.2.0-255';
$input[] = '3.3.3.*';
$input[] = '4.4.*.*';
$input[] = '5.5.5.20-40';
$input[] = 'google.com';

echo "The input is:\n";
foreach ($input as $network) {
    echo "$network\n";
}

echo "\n";
echo "Expanded into ranges:\n";
foreach ($input as $network) {
    list($start, $end) = network_expand_range($network);
    echo "$start - $end\n";
}

echo "\n";
echo "As a long:\n";
foreach ($input as $network) {
    list($start, $end) = network_expand_range($network);
    echo ip2long($start) . ' - ' . ip2long($end) . "\n";
}

echo "\n";
echo "Back into compacted form:\n";
foreach ($input as $network) {
    list($start, $end) = network_expand_range($network);
    echo network_compact_range($start, $end), "\n";
}
?>

The output of this would be something like:

The input is: 1.1.1.1 - 1.1.255.255 2.2.2.0-255 3.3.3.* 4.4.*.* 5.5.5.20-40 google.com

Expanded into ranges: 1.1.1.1 - 1.1.255.255 2.2.2.0 - 2.2.2.255 3.3.3.0 - 3.3.3.255 4.4.0.0 - 4.4.255.255 5.5.5.20 - 5.5.5.40 74.125.53.100 - 74.125.53.100

As a long: 16843009 - 16908287 33686016 - 33686271 50529024 - 50529279 67371008 - 67436543 84215060 - 84215080 1249719652 - 1249719652

Back into compacted form: 1.1.1.1 - 1.1.255.255 2.2.2.* 3.3.3.* 4.4.0.0 - 4.4.255.255 5.5.5.20-40 74.125.53.100 </code>

Hope you find this useful.