Article feb2006.tar

Getting to Know Your Network -- Part I

Luis Enrique Muñoz

If your job is like mine, you've heard the words "vulnerability management" a lot during the past couple of years. Generally speaking, "VM" comprises all the tasks we must do -- such as patching, device and machine inventories, and security audits -- to keep our networks working despite the various software flaws exploited by malware.

There is an endless stream of vulnerability information generated daily from a variety of sources. Some of those vulnerabilities affect specific combinations of software and operating systems. When you just have a few dozen machines to worry about, assessing whether a particular vulnerability is the next big one or just a blip on the radar is relatively easy. Throw in a few thousand machines in a nationwide corporate network, however, and it becomes more of a challenge.

Some types of information are very useful in managing this task -- a network map describing (in general terms) where each IP subnet in our network is and a complete device inventory, for example. In this series of articles, I will gather all this information with the use of a few open source tools, a bit of Perl, and very little effort. It turns out that a lot of this information could be found if we had a tool allowing us to assemble commands on the fly, send them to network devices, and work on the results. I'll show how to build exactly that.

We will need just a couple of Perl modules from the CPAN toolbox, which we will be using throughout these articles. These modules will help us with connecting and sending commands to the Cisco devices that we will be focusing on this series -- parsing configuration files, manipulating IP addresses, and abstracting database tables and rows into Perl objects, respectively.

Installing these modules is easy. Just do this as root:

# perl -MCPAN -e shell
cpan> install Net::Telnet::Cisco
      ...
cpan> install Cisco::Reconfig
      ...
cpan> install NetAddr::IP
      ...
cpan> install Class::DBI
      ...
To put those modules to use, we will need a script allowing us to organize or generate the commands and execute them automatically. For this, I'll take a much longer script made for a similar task and trim it down for our purposes. I'll call this script "aconfig" or "Automatic CONFIGuration". aconfig will take as input a file of IP addresses or devices named with optional user-names and passwords to log into the devices; it will perform commands from scripts we will give it through the command line. A sample device list would look like:

10.10.10.10 user password
10.10.10.11 user2 password2
...
The aconfig script will then execute automatic configuration scripts -- let's call them "ascripts" -- against each of the network devices provided. Ascripts can be quite powerful, featuring a simple language that allows us to send commands to the device and also act on the result. The language is defined as follows:

  • Blank lines are ignored.
  • Comments are preceded with %# and extend to the end of the line.
  • %INCLUDE file% is replaced by the contents of "file".
  • %LABEL mylabel% is replaced by the empty string and defines the named label, usable for flow control.
  • %GOTO mylabel% causes the ascript to continue at the location of the named label.
  • %SGOTO scalar% causes the ascript to continue at the label named "scalar" if the Perl scalar "$main::scalar" evaluates to true.
  • %RGOTO scalar% causes the ascript to continue at the label whose name is contained in the Perl scalar "$main::scalar".
  • %ENABLE% issues commands to put the device in "enable" mode.
  • %EXEC% issues commands to put the device in the "exec" mode.
  • The %[ ... ]% block evaluates its contents as if within Perl double-quotes. The block must be contained on a single line.
  • The %{ ... }% block evaluates everything inside it as Perl code in the "main" name space.

Also, a few variables are available to Perl code:

  • $main::TELNET -- This contains the Net::Telnet::Cisco object used to interact with the current device.
  • $main::ADDR -- The address or name of the device to which the ascript is now connected.
  • $main::LAST -- The results of the last successful command.

Let's take a quick look at the script providing all of this. We will go through it superficially, because this is only an accessory to what we'll be doing, and you can download the complete script from the Sys Admin Web site at: http://www.sysadminmag.com.

The first few lines are quite common. Note that at line 3, we place the script within its own name space. This is done to maintain some isolation between the script's name space, and the name space where the snippets of code within our ascripts will be executing. In this case, we want to ensure that should one of those snippets of code want to touch aconfig's variables, it can. At line 14, we turn off line buffering, causing any output to be flushed immediately:

 1: #!/usr/bin/perl
 2:
 3: package aconfig;
 4: use strict;
 5: use warnings;
 6:
 7: use IO::File;
 8: use Getopt::Std;
 9: use Net::Telnet::Cisco;
...
14: $|++;
On line 12, we define a small piece of Perl code that will be prepended to each snippet to be executed. This places that code, by default, in the "main" name space and disables "strict" and "warnings", as most of those snippets will resemble one-liners and will likely benefit from the non-strictness:

12: use constant PREAMBLE => qq{ no strict; no warnings; package main; };
The following lines take care of parsing any command-line options passed to aconfig and setting some default values automatically. On line 20, we allow our timeout to be "absolute" by prepending a "+" to its value, in seconds:

10: use vars qw($opt_c $opt_e $opt_l $opt_t $opt_V $opt_v);
...
16: getopts('c:el:t:Vv');
17: $opt_t ||= 60;            # Default timeout for commands
18: $opt_l ||= 20;            # Default login timeout
20: $opt_t += time if $opt_t =~ /^\+\d+/;
For convenience, we'll define handlers for "warn" and "die" that add useful debug information, such as the device's address and the time the event occurred. This is done in lines 24 to 32:

24: sub warn_handler {
25:     warn scalar(localtime(time))
26:         . " [" . ($main::ADDR || $ip || 'no ip') . "]: ", @_;
27: }
28:
29: sub die_handler {
30:     die scalar(localtime(time))
31:         . " [" . ($main::ADDR || $ip || 'no ip') . "]: ", @_;
32: }
Lines 38-52 define a function that will read commands from a file and return them in a list. This is used at lines 54-66 to handle the %INCLUDE ...% directive and recursively build the complete ascript we will be using:

38: sub _read_commands ($) {
39:     my @c = ();
40:
41:     for my $f (split(/,+/, shift))
42:     {
43:         my $fh = new IO::File $f
44:             or die "Cannot open script file $f: $!\n";
45:
46:         push @c, $fh->getlines;
47:
48:         $fh->close;
49:     }
50:                     # Strip comments away
51:     return map { $_ =~ s/%#.*$//g; $_ } @c;
52: }
54: our @commands = _read_commands $opt_c;
56: for (my $line = 0; $line <= $#commands; $line ++)
57: {
58:
59:     if ($commands[$line] =~ m!%INCLUDE ([^%]+)%!) {
60:         die "Failed to include '$1' as referenced: $!\n"
61:             unless -f $1;
62:
63:         splice(@commands, $line, 1, _read_commands $1);
64:     }
65:
66: }
Next, aconfig goes in a loop through all the devices, in which it makes a copy of the ascript, creates and initializes the Net::Telnet::Cisco object, and executes its contents line by line. This happens in lines 68 through 226:

Now let's do a little testing. For now, we'll work from the comfort of our home directory. Let's create the following directories:

~/aconfig
~/aconfig/ascripts
~/aconfig/ascripts/include
~/aconfig/output
The idea is that useful and common ascript snippets should be placed at ~/aconfig/ascripts/include, while our ascripts would be stored at ~/aconfig/ascripts, thus keeping everything neatly organized. The ~/aconfig/output directory is where we will be putting the result of our work.

Let's create a small ascript "include" that performs some basic session configuration. We will call this script ~/aconfig/ascripts/include/setup, and its contents will be:

terminal length 0
Simple enough. If needed, we can add commands to this later. Now, our first script will be called ~/aconfig/ascripts/save-version and it should store the result of the "show version" command on a file under ~/aconfig/output. This will be useful later when we start building our device database. The contents of the ascript would be:

%INCLUDE ascripts/include/setup%

show version
%{
    package save::version;
    use IO::File;
    my $fh = new IO::File './output/' . $main::ADDR, "w"
      or die "Failed to create output file: $!\n";
    print $fh $main::LAST;
    close $fh;
    undef;
}%
Note that at the end of the Perl block, we return undef to mean that no command must be sent to the device as a result. Now, let's put the addresses or names, user names, and passwords of the devices we want to work with into the file aconfig.hosts. Be very careful with the permissions of this file, as likely you do not want anybody reading it. Once you're ready, you can simply invoke aconfig from the ~/aconfig directory as shown:

$ ./aconfig -Vv -c ascripts/save-version aconfig.hosts
Sat Oct 15 15:55:31 2005 [device-1]: begin
terminal length 0
show version
Sat Oct 15 15:55:32 2005 [device-1]: done
Sat Oct 15 15:55:32 2005 [device-2]: begin
terminal length 0
show version
Sat Oct 15 15:55:34 2005 [device-2]: done
...
And then, under ~/aconfig/output, you'll have one file for each device, with the contents of the show version command indicating software version and platform for each device. Normally, you would run this without the -Vv flags out of a crontab. This would keep a nicely updated directory of device configuration.

Let's take this a step further and add the configuration information to what we're harvesting from our network. For this, let's modify our scripts.

Let's write a simple "data-store" ascript that will invoke the different components we will need. This ascript would look like this:

$ cat ascripts/data-store

%INCLUDE ascripts/include/setup%

%INCLUDE ascripts/include/save-version%

%INCLUDE ascripts/include/save-config%

We will remove the %INCLUDE from our previous "save-version" and put it under ascripts/include/save-version. We will use this as a base to write ascripts/include/save-config, which will look like:

$ cat ascripts/include/save-config
show run
%{
    package save::config;
    use IO::Zlib;
    my $fh = new IO::File './output/' . $main::ADDR . '.config', "w";
    die "Failed to create output file: $!\n"
      unless $fh;
    print $fh $main::LAST;
    close $fh;
    undef;
}%
As you can see, both save-config and save-version share most of their code. Only the actual filename of the resulting file changes. You might want to factor the common code in its own include file, and then simply use it to store the results of your command. I'll leave this as an exercise for the reader.

All we have to do now is simply run this new script with:

$ ./aconfig -c ascripts/save-version aconfig.hosts
After a while, you'll end up with a lot of files under your ./output directory. The ones with the ".version" extension contain the output of the show version command. The ".config" extension surprisingly will contain the output of the show run shorthand command we used in our ascript. This directory will be our main data source, as here we have a record of each device, each interface, and each IP subnet in our network.

Note that you could also use many other data sources. You could look at Cisco Discovery Protocol data on each device, or you could issue more intricate commands. You could also peek in the routing configuration, VLAN configuration, and many other variables. In this regard, this article has simply provided a nice tool. The end uses are up to you.

In the next article in this series, I'll show how to put this information to use. Meanwhile, you may want to experiment with this new tool, as there are many interesting things you can do.

Luis has been working in various areas of computer science since the late 1980s. Some people blame him for conspiring to bring the Internet into his home country, where currently he spends most of his time teaching others about Perl and taking care of network security at the largest ISP there as its CISO. He also believes that being a sys admin is supposed to be fun.