Managing IP space with Perl
Home » Perl
Do you want to thank me for my contributions?

A while ago, I wrote NetAddr::IPno alt definedto help me work out tedious tasks such as finding out which addresses fell within a certain subnet or allocating IP space to network devices.

This article discusses a tipical challenges along with solutions usingNetAddr::IPno alt defined. Since Perl lacks a native type to represent either an IP address or an IP subnet, I feel this module has been quite helpful for fellow Perl programmers who like me, need to work in this area.

Note however that neither the module itself nor this tutorials are intended as replacements to your knowledge about how to work with chunks of IP space. The module was written as a tool to help with the boring tasks (after all, we're plentiful with good laziness, aren't we?) and this article, was written to help you get your feet wet based in the most common questions I get. Both the module and this tutorial expect you to be fluent in basic networking and somewhat fluent in Perl. You should not be writing Perl code to manage your subnetting otherwise.

Specifying an IP Address or a subnet

A NetAddr::IPno alt definedobject represents a subnet. This involves storing an IP address within the subnet along with the subnet's netmask. Of course, using a host netmask (/32 or in decimal notation, 255.255.255.255) allows for the specification of a single IP address.

You can create a NetAddr::IPno alt defined object with an incantation like the following:

    1: use NetAddr::IP;
    2: my $ip = new NetAddr::IP '127.0.0.1';

which will create an object representing the 'address' 127.0.0.1 or the 'subnet' 127.0.0.1/32.

Creating a subnet is equally easy. Just specify the address and netmask in almost any common notation, as in the following examples:

    1: use NetAddr::IP;
    2: my $loopback = new NetAddr::IP '127.0.0.1', '255.0.0.0';
    3: my $rfc1918 = new NetAddr::IP '10.0.0.0/8';
    4: my $another = new NetAddr::IP '1.2.0.0/255.255.0.0';
    5: my $loopback2 = new NetAddr::IP 'loopback';
    6: my $google = new NetAddr::IP 'www.google.com';

The following is a list of the acceptable arguments to ->new(), which as you might have guessed, is the method to invoke to create a new object, and their meanings:

->new('broadcast')

Equivalent to the address 255.255.255.255/32 which is often used to denote a broadcast address.

->new('default')

Synonym to the address 0.0.0.0/0 which is universally used to represent a default route. This subnet is guaranteed to ->contains() any other subnet. More on that later.

For the benefit of many Cisco users out there, any is considered a synonym of default.

->new('loopback')

The same as the address 127.0.0.1/8 which is the standard loopback address or subnet.

->new('10.10.10.10') or ->new('foo.bar.com')

This represents a single host. When no netmask is supplied, a netmask of /32 is assumed. When supplying a name, the host name will be looked up using gethostbyname()no alt defined, which will in turn use whatever name resolution is configured in your system to obtain the IP address associated with the supplied name.

->new('10.10.1')

An ancient notation that allows the middle zeroes to be skipped. The example is equivalent to ->new('10.10.0.1'). I've not seen this used much in real life, but it's considered a well known notation.

->new('10.10.1.')

Note the trailing dot. This format allows the omission of the netmask for classful subnets. The example is equivalent to ->new('10.10.1.0/24').

->new('10.10.10.0 - 10.10.10.255')

This is also known as range notation. Both ends of an address range are specified. Note that this notation is only supported if the specified subnet can be represented in valid CIDR notation.

->new('10.10.10.0-255')

This notation is a shorthand for the range notation discussed above. It provides for the specification of an address range where both of its ends share the first octets. This notation is only supported when the specified range of hosts defined a proper CIDR subnet.

->new(1024)

Whenever the address is specified as a numeric value greater than 255, it is assumed to contain an IP address encoded as an unsigned int.

->new('f34::123/40')

In the latest version of the module, this will be understood as an IPv6 address in shorthand notation, which to my knowledge is the only well known syntax so far.

->new() with two arguments

Whenever two arguments are specified to ->new(), the first is always going to be interpreted as the IP address and the second will always be the netmask, in any of the formats discussed so far.

Netmasks can be specified in dotted-quad notation, as the number of one-bits or as the equivalent unsigned int. Also, special keywords such as broadcast, default or host can be used as netmasks.

The semantics and notations depicted above, are supposed to comply strictly with the Do What I Mean! approach which is so popular with Perl. The general idea is that you should be able to stick almost anything resembling an IP address or a subnet specification into the ->new() method to get an equivalent object. I've implemented all the syntaxes and notations that I've seen around, but I am always open to include new ones as I find them, so please take a look at the latest version of the module to be sure what formats are supported.

Simple operations with subnets

There is a number of operations that have been simplified along the different versions of the module. The current version, as of this writing, provides support for the following operations:

Scalarization

A NetAddr::IPno alt defined object will become its CIDR representation whenever it is evaluated in scalar context. For instance, you can very well do something the following:

bash-2.05a$ perl -MNetAddr::IP
$ip = new NetAddr::IP "www.google.com";
print "Google is at $ip\n";
^D
Google is at 216.239.51.101/32
Numerical comparison

Two objects can be compared using any of the numerical comparison operators. Later versions of the module, compare the netmask as well.

Increments and decrements

Adding or substracting a scalar from an object will change the address in the subnet, but always keeping it within the subnet. This is very useful to write loops, like the following:

    1: use NetAddr::IP;
    2: my $ip = new NetAddr::IP('10.0.0.0/30');
    3: while ($ip < $ip->broadcast) {
    4:   print "ip = $ip\n";
    5:   $ip ++;
    6: }

which will produce the following output:

ip = 10.0.0.0/30
ip = 10.0.0.1/30
ip = 10.0.0.2/30
List expansion

When required, a NetAddr::IPno alt defined will expand automatically to a list containing all the addresses within a subnet, conveniently leaving the subnet and the broadcast addresses out. The following code shows this:

    1: use NetAddr::IP;
    2: my $ip = new NetAddr::IP('10.0.0.0/30');
    3: print join(' ', @$ip), "\n";

And the output would be

10.0.0.1/32 10.0.0.2/32

This feature is dangerous unless lazy lists (lists that produce elements as requested) are generally available in Perl. This will likely achieve a TODO status after Perl 6 is released, but I digress. For instance, expanding an object representing a /8 subnet, will likely consume loads of memory and CPU in your machine. As we'll see later, there's usually a better way to do the following with a short loop instead of the expansion.

Common (and not so common) tasks

Below I will try to provide an example for each major feature of NetAddr::IPno alt defined, along with a description of what is being done, where appropiate.

Optimising the address space

This is one of the reasons for writing NetAddr::IPno alt defined in the first place. Let's say you have a few chunks of IP space and you want to find the optimum CIDR representation for them. By optimum, I mean the least amount of CIDR subnets that exactly represent the given IP address space. The code below is an example of this:

    1: use NetAddr::IP;
    2: 
    3: push @addresses, NetAddr::IP->new($_) for <DATA>;
    4: print join(", ", NetAddr::IP::compact(@addresses)), "\n";
    5: __DATA__
    6: 10.0.0.0/18
    7: 10.0.64.0/18
    8: 10.0.192.0/18
    9: 10.0.160.0/19

Which will, of course, output 10.0.0.0/17, 10.0.160.0/19, 10.0.192.0/18. Let's see how this is done...

First, the line 3 creates a list of objects representing all the subnets read in via the <DATA> filehandle. There should be no surprises here.

Then, we call NetAddr::IP::compact at line 4, with the list of subnets build earlier. This function accepts a list of subnets as its input (actually, an array of objects). It processes them internally and outputs another array of objects, as summarized as possible. This array is join()ed into a neat string.

Using compact() as in the example is fine when you're dealing with a few subnets or are writing a throw-away one-liner. If you think your script will be handling more than a few tens of subnets, you might find compactref() useful. It works exactly as shown before, but takes (and returns) references to arrays. I've seen huge speed improvements when working with large lists of subnets (i.e., thousands of subnets).

If you don't want to bother fully qualifying the call to compact(), the most recent versions of the module allows the conditional exporting. Note however that the exported method is actually Compact(), with a capital C. You could invoke it as:

    1: use NetAddr::IP qw/Compact/;

Assigning address space

This problem can be tought as the complement to the prior one. Let's say a couple of network segments need to be connected to your network. You can carve slices out of your address space easily, such as in the following code:

    1: use NetAddr::IP;
    2: 
    3: print "My address space contains the following /24s:\n", 
    4:         join("\n", NetAddr::IP->new('10.0.0.0/22')->split(24)), "\n";

Which will divide your precious address space (the one specified in the NetAddr::IP->new()) in subnets with a netmask of 24 bytes. This magic is accomplished by the ->split() method, which takes the number of bits in the mask as its only parameter. It returns a list of subnets contained in the original object.

Again, in situations where the split might return a large number of subnets, you might prefer the use of ->splitref(), which returns a reference to an array instead.

Returning to our example, you might assign a /24 to each new subnet. Ok, perhaps assigning a /24 is not that good an example, as this falls on an octet boundary but trust me, when you have to split a /16 in /20s, to be allocated in chunks of /22s in a network spanning the whole country, it's nice to know your subnetting is well done.

Cisco's wildcard notation (and other dialects)

Those of you who have had to write an ACL in a Cisco router, know about the joys of this peculiar format in which the netmask works the opposite of what custom says. An easy way to convert between traditional notation and Cisco's wildcard notation, is to use the eloquently named ->wildcard() method, as this example shows:

    1: use NetAddr::IP;
    2: 
    3: print join(' ', NetAddr::IP->new('10.0.0.0/25')->wildcard());

As you might have guessed, ->wildcard() returns an array whose first element is the address and its second element is the netmask, in wildcard notation. If scalar context is forced using scalar, only the netmask will be returned, as this is most likely what you want.

In case you wonder, the example outputs 10.0.0.0 0.0.0.127.

Just for the record, below is a number of outputs from different methods for the above example:

Range (The ->range() method)

Outputs 10.0.0.0 - 10.0.0.127. Note that this range goes from the network address to the broadcast address.

CIDR notation (The ->cidr() method)

As expected, it outputs 10.0.0.0/25.

Prefix notation (The ->prefix() method)

Similar to ->range(), this method produces 10.0.0.1-127. However, note that the first address is not the network address but the first host address.

n-Prefix notation (The ->nprefix() method)

Produces 10.0.0.1-126. Note how the broadcast address is not within the range.

Numeric (The ->numeric() method)

In scalar context, produces and unsigned int that represents the address in the subnet. In array context, both the address and netmask are returned. For the example, the array output is (167772160, 4294967168). This is very useful when serializing the object for storage. You can pass those two numbers back to ->new() and get your object back. At the time of this writing, this is not entirely true for IPv6 addresses.

Just the IP address (The ->addr() method)
Just the netmask as a dotted quad (The ->mask() method)
The length in bits of the netmask (The ->masklen() method)

Matching against your address space

Let's say you have a log full of IP addresses and you want to know which ones belong to your IP space. A simple way to achieve this is shown below:

    1: use NetAddr::IP;
    2: 
    3: my $space = new NetAddr::IP ('10.128.0.0/17');
    4: 
    5: for my $ip (map { NetAddr::IP->new($_) } <DATA>)
    6: {
    7:     print $ip, "\n"
    8:         if $space->contains($ip);
    9: }
   10: 
   11: __DATA__
   12: 172.16.1.1
   13: 172.16.1.5
   14: 172.16.1.11
   15: 172.16.1.10
   16: 172.16.1.9
   17: 172.16.1.3
   18: 172.16.1.2
   19: 172.16.1.7
   20: 172.16.1.4
   21: 172.16.1.1
   22: 10.128.0.1
   23: 10.128.0.12
   24: 10.128.0.13
   25: 10.128.0.41
   26: 10.128.0.17
   27: 10.128.0.19

This code will output only the addresses belonging to your address space, represented by $space. The only interesting thing here is the use of the ->contains() method. As used in our example, it returns a true value if $ip is completely contained within the $space subnet.

Alternatively, the condition could have been written as $ip->within($space). Remember that TIMTOWTDI.

Walking through the network without leaving the office

Some of the nicest features of NetAddr::IPno alt defined can be better put to use when you want to perform actions with your address space. I've selected some examples that allow me to show specific features.

One of the most efficient ways to walk your address space is building a for loop, as this example shows:

    1: use NetAddr::IP;
    2: 
    3: push @space, NetAddr::IP->new($_) for <DATA>;
    4: 
    5: for my $netblock (NetAddr::IP::compact @space) 
    6: {
    7:     for (my $ip = $netblock->first;
    8:          $ip <= $netblock->last;
    9:          $ip++)
   10:     {
   11:         # Do something with $ip
   12:     }
   13: }
   14: __DATA__
   15: 10.0.0.0/16
   16: 172.16.0.0/24

The nicest thing about this way of walking your IP space, is that even if you are lucky enough to have lots of it, you won't eat all your memory by generating a huge list of objects. In this example, only one object is created in every iteration of the loop.

Everything up to line 6 should be pretty clear by now, so I'll just ignore it. Since a couple of new friends were introduced in the inner loop of our example starting at line 7, an explanation is in order.

This C-like for loop at line 7, uses the ->first() function to find the first subnet address. The first subnet address is defined as that having all of its host bits but the rightmost set to zero and the rightmost, set to one.

At line 8, I use the numerical comparison discussed earlier to see if the value of $ip is less than or equal to whatever ->last() returns. ->last() returns an address with all of its host bits set to one but the rightmost. If this condition holds, we execute the loop and post-increment $ip to get the next IP address in the subnet, at line 9.

I started the discussion on this topic with the approach that insures less wasted resources. However, in the purest Perl tradition, this is not the only way to do it. There's another way, reserved for the true lazy (or those with memory to burn, but we all know you never have enough memory, right?).

This other way is invoked with the ->hostenum() or the ->hostenumref() methods. They return either an array or a reference to an array respectively, containing one object for each host address in the subnet. Note that only valid host addresses will be returned (as objects) since the network and broadcast addresses are seldom useful.

With no further preamble, I introduce an example that kids shouldn't attempt at home, or at least in production code. (If you find this warning superfluous, try adding 64.0.0.0/8 to the __DATA__ section and see if your machine chews through it all).

    1: use NetAddr::IP;
    2: push @space, NetAddr::IP->new($_) for <DATA>;
    3: for my $ip (map { $_->hostenum } NetAddr::IP::compact @space) 
    4: {
    5:    # Do something with $ip
    6: }
    7: __DATA__
    8: 10.0.0.0/16
    9: 172.16.0.0/24

If you really have enough memory, you'll see that each host address in your IP space is generated into a huge array. This is much more costly (read, slow) than the approach presented earlier, but provides for more compact one-liners or quickies.

Finding out how big is your network

Have you wondered just how many IP addresses can you use in your current subnet plan? If the answer to this (or to a similar question) is yes, then read on.

There is a method called ->num() that will tell you exactly how many addresses can you use in a given subnet. For the quick observers out there, you can also use something like scalar $subnet->hostenum, but this is a really expensive way of doing it.

A more conservative (in resources) approach is depicted below:

    1: use NetAddr::IP;
    2: 
    3: my $hosts = 0;
    4: push @space, NetAddr::IP->new($_) for <DATA>;
    5: $hosts += $_->num for @space;
    6: print "You have $hosts\n";
    7: __DATA__
    8: 10.0.0.0/16
    9: 172.16.0.0/24

This pretty much wraps it up about usability. At this other article, I made a comparison among other modules that provide an abstraction to manipulate subnets or addresses.

Valid XHTML 1.0! Valid CSS! Powered by Template Toolkit 2 Powered by GNU Emacs