#!/usr/bin/perl

### Copyright notice at the bottom of this file ###

# $Id: addrtool,v 1.25 2012/01/08 16:34:10 adamm Exp $
#
# $Source: /home/adamm/src/addrtool/RCS/addrtool,v $


use strict;
use warnings;
use English qw( -no_match_vars );
use File::Basename;
use Pod::Usage;


my $ADDR = '\d{1,3} \. \d{1,3} \. \d{1,3} \. \d{1,3}';
my $BLOCK = '( /[0-9] | /[123][0-9] )';
my $PROG = basename $PROGRAM_NAME;


sub main {
    my $action;

    # Before anything else, deal with --help and --man
    if (scalar(@ARGV) == 1) {
	if (
	    ($ARGV[0] eq "--help")
	    ||
	    ($ARGV[0] eq "--man")
	) {
	    pod2usage(-exitstatus => 0, -verbose => 2);
	}
    }

    $action = assertArgsOK(1, 3, @ARGV);

    if ($action eq "mask -> block") {
	assertQuadOK($ARGV[0]);
	maskToBlock($ARGV[0]);

    } elsif ($action eq "block -> mask") {
	assertBlockOK($ARGV[0]);
	blockToMask($ARGV[0]);

    } elsif ($action eq "show range 1") {
	assertCidrOK($ARGV[0]);
	showRange(splitCidr($ARGV[0]));

    } elsif ($action eq "show range 2") {
	assertQuadOK($ARGV[0]);
	assertQuadOK($ARGV[1]);
	showRange($ARGV[0], maskToBlock($ARGV[1]));

    } elsif ($action eq "show blocks 2 block") {
	assertCidrOK($ARGV[0]);
	assertBlockOK($ARGV[1]);
	showNets(splitCidr($ARGV[0]), $ARGV[1]);

    } elsif ($action eq "show blocks 2 mask") {
	assertCidrOK($ARGV[0]);
	assertQuadOK($ARGV[1]);
	showNets(splitCidr($ARGV[0]), maskToBlock($ARGV[1]));

    } elsif ($action eq "show blocks 3 block") {
	assertQuadOK($ARGV[0]);
	assertQuadOK($ARGV[1]);
	assertBlockOK($ARGV[2]);
	showNets($ARGV[0], maskToBlock($ARGV[1]), $ARGV[2]);

    } elsif ($action eq "show blocks 3 mask") {
	assertQuadOK($ARGV[0]);
	assertQuadOK($ARGV[1]);
	assertQuadOK($ARGV[2]);
	showNets($ARGV[0], maskToBlock($ARGV[1]), maskToBlock($ARGV[2]));

    } else {
	printf STDERR
	    "%s: INTERNAL ERROR! (action = '%s')\n", $PROG, $action;
	exit 2;
    }

    exit 0;
} # main


sub assertArgsOK {
    my @args;
    my $maxArgs;
    my $minArgs;
    my $numArgs;

    ($minArgs, $maxArgs, @args) = @_;

    # Check the number of args
    $numArgs = scalar @args;
    if (($numArgs < $minArgs) || ($numArgs > $maxArgs)) {
	printf STDERR
	    "%s: expected %d - %d args, got %d; aborting\n\n",
	    $PROG, $minArgs, $maxArgs, $numArgs;
	pod2usage(-exitstatus => 2, -verbose => 0);
    }

    # Based on how many args we got and what they look like,
    # decide what to do

    if ($numArgs == 1) {
	# Did we get "/#" or "/##"?
	if ($ARGV[0] =~ m, \A ${BLOCK} \z ,x) {
	    return "block -> mask";
	}

	# What about "#.#.#.#"?
	if ($ARGV[0] =~ m, \A ${ADDR} \z ,x) {
	    return "mask -> block";
	}

	# Or "#.#.#.#/#" or "#.#.#.#/##"?
	if ($ARGV[0] =~ m, \A ${ADDR} ${BLOCK} \z ,x) {
	    return "show range 1";
	}

	printf STDERR
	    "%s: expected '/b' or 'm.m.m.m', got '%s'; aborting\n\n",
	    $PROG, $ARGV[0];
	pod2usage(-exitstatus => 2, -verbose => 0);
    }

    if ($numArgs == 2) {
	if (
	    ($ARGV[0] =~ m, \A ${ADDR} \z ,x)
	    &&
	    ($ARGV[1] =~ m, \A ${ADDR} \z ,x)
	) {
	    return "show range 2";
	}

	if ($ARGV[0] =~ m, \A ${ADDR} ${BLOCK} \z ,x) {
	    if ($ARGV[1] =~ m, \A ${BLOCK} \z ,x) {
		return "show blocks 2 block";
	    }

	    if ($ARGV[1] =~ m, \A ${ADDR} \z ,x) {
		return "show blocks 2 mask";
	    }
	}

	printf STDERR
	    "%s: expected one of the following combinations:\n",
	    $PROG;
	printf STDERR "%s:     %s\n", $PROG, "a.a.a.a    m.m.m.m";
	printf STDERR "%s:     %s\n", $PROG, "a.a.a.a/b  /b";
	printf STDERR "%s:     %s\n", $PROG, "a.a.a.a/b  m.m.m.m";
	printf STDERR
	    "%s: got '%s %s'; aborting\n\n", $PROG, $ARGV[0], $ARGV[1];
	pod2usage(-exitstatus => 2, -verbose => 0);
    }

    if ($numArgs == 3) {
	if (
	    ($ARGV[0] =~ m, \A ${ADDR} \z ,x)
	    &&
	    ($ARGV[1] =~ m, \A ${ADDR} \z ,x)
	) {
	    if ($ARGV[2] =~ m, \A ${ADDR} \z ,x) {
		return "show blocks 3 mask";
	    }

	    if ($ARGV[2] =~ m, \A ${BLOCK} \z ,x) {
		return "show blocks 3 block";
	    }
	}

	printf STDERR
	    "%s: expected one of the following combinations:\n", $PROG;
	printf STDERR "%s:     %s\n", $PROG, "a.a.a.a  m.m.m.m  m.m.m.m";
	printf STDERR "%s:     %s\n", $PROG, "a.a.a.a  m.m.m.m  /b";
	printf STDERR
	    "%s: got '%s %s %s'; aborting\n\n",
	    $PROG, $ARGV[0], $ARGV[1], $ARGV[2];
	pod2usage(-exitstatus => 2, -verbose => 0);
    }

    # HUH?!?!
    printf STDERR "%s: INTERNAL ERROR! (numArgs = '%s')\n", $PROG, $numArgs;
    exit 3;
} # assertArgsOK


sub assertBlockOK {
    my $block = shift @_;

    if ($block =~ m, \A / [1-9] \z,x) {
	return;
    }

    if ($block =~ m, \A / [12][0-9] \z,x) {
	return;
    }

    if ($block =~ m, \A / 3[01] \z,x) {
	return;
    }

    printf STDERR
	"%s: expected '/1'..'/31', '%s'; aborting\n\n", $PROG, $block;
    pod2usage(-exitstatus => 2, -verbose => 0);
} # assertBlockOK


sub assertCidrOK {
    my $cidr;
    my $block;
    my $quad;

    $cidr = shift @_;
    ($quad, $block) = splitCidr($cidr);
    assertBlockOK($block);
    assertQuadOK($quad);

    return;
} # assertCidrOK


sub assertQuadOK {
    my @octets;
    my $quad;

    $quad = shift @_;
    @octets = split /\./, $quad;

    foreach my $o (@octets) {
	if ($o > 255) {
	    printf STDERR
		"%s: invalid dotted quad: '%s'; aborting\n\n", $PROG, $quad;
	    pod2usage(-exitstatus => 2, -verbose => 0);
	}
    }

    return;
} # assertQuadOK


sub bitsToBlock {
    my $bits = shift @_;

    $bits = ($bits & 0x55555555) + (($bits & 0xaaaaaaaa) >> 1);
    $bits = ($bits & 0x33333333) + (($bits & 0xcccccccc) >> 2);
    $bits = ($bits & 0x0f0f0f0f) + (($bits & 0xf0f0f0f0) >> 4);
    $bits = ($bits & 0x00ff00ff) + (($bits & 0xff00ff00) >> 8);
    $bits = ($bits & 0x0000ffff) + ($bits >> 16);

    return $bits;
} # bitsToBlock


sub bitsToQuad {
    my $b;
    my @tmp;

    $b = shift @_;;

    $tmp[0] = ($b >> 24) & 0xff;
    $tmp[1] = ($b >> 16) & 0xff;
    $tmp[2] = ($b >> 8)  & 0xff;
    $tmp[3] = $b & 0xff;

    return @tmp;
} # bitsToQuad


sub blockToBits {
    my $bits = shift @_;

    $bits = 32 - $bits;
    return ((2 ** 32) - 1) - ((2 ** $bits) - 1);
} # blockToBits


sub blockToMask {
    my $block = shift @_;
    $block =~ s{\A /}{}x;

    printf "%d.%d.%d.%d\n", bitsToQuad(blockToBits($block));
    return;
} # blockToMask


sub maskToBlock {
    my $block;
    my $mask;

    $mask = shift @_;
    $block = sprintf "/%d", bitsToBlock(quadToBits($mask));

    return $block;
} # maskToBlock


sub quadToBits {
    my $quad;
    my @octets;

    $quad = shift @_;
    @octets = split /\./, $quad;

    return
	($octets[0] << 24)
	| ($octets[1] << 16)
	| ($octets[2] << 8)
	| $octets[3];
} # quadToBits


sub showNets {
    my $a;
    my $addr;
    my $block1;
    my $block2;
    my $mask;
    my $nets;
    my $str;

    ($addr, $block1, $block2) = @_;

    $addr = quadToBits($addr);
    $block1 =~ s{\A /}{}x;
    $block2 =~ s{\A /}{}x;

    # Quick sanity check
    if ($block2 <= $block1) {
	printf STDERR
	    "%s: /%d <= /%d; aborting\n\n", $PROG, $block2, $block1;
	pod2usage(-exitstatus => 2, -verbose => 0);
    }

    $mask = blockToBits($block2);
    $nets = 2 ** ($block2 - $block1);

    printf "%14s  %24s  %20s\n", "Network", "H  o  s  t  s", "Broadcast";
    printf "%15s  %24s  %24s\n", "-" x 15,  "---------------", "-" x 15;

    for (my $n = 0; $n < $nets; $n++) {
	$a = $addr + ($n << (32 - $block2));

	$str = sprintf "%d.%d.%d.%d", bitsToQuad($a & $mask);
	printf "%15s", $str;

	$str = sprintf "%d.%d.%d.%d", bitsToQuad(($a & $mask) + 1);
	printf "  %15s", $str;

	$str = sprintf "%d.%d.%d.%d", bitsToQuad(($a | ~$mask) - 1);
	printf " - %-15s", $str;

	$str = sprintf "%d.%d.%d.%d", bitsToQuad($a | ~$mask);

	printf "  %s", $str;
	printf "\n";
    }

    printf "\nnetmask = %d.%d.%d.%d\n", bitsToQuad($mask);
    printf "addresses per subnet = %d\n", ~$mask - 1;
} # ShowNets


sub showRange {
    my $addr;
    my $block;
    my $mask;

    ($addr, $block) = @_;

    $addr = quadToBits($addr);
    $block =~ s{\A /}{}x;

    $mask = blockToBits($block);

    printf "            Address: %d.%d.%d.%d/%d\n",
	bitsToQuad($addr), $block;
    printf "            Netmask: %d.%d.%d.%d\n", bitsToQuad($mask);
    printf "    Network address: %d.%d.%d.%d\n", bitsToQuad($addr & $mask);
    printf "  Broadcast address: %d.%d.%d.%d\n", bitsToQuad($addr | ~$mask);
    printf " First host address: %d.%d.%d.%d\n",
	bitsToQuad(($addr & $mask) + 1);
    printf "  Last host address: %d.%d.%d.%d\n",
	bitsToQuad(($addr | ~$mask) - 1);
    printf "Available addresses: %ld\n", ~$mask - 1;
} # showRange


sub splitCidr {
    my $cidr = shift @_;
    my $block;
    my $quad;

    ($block = $cidr) =~ s{.*/}{/};
    ($quad = $cidr) =~ s{/.*}{};
    return ($quad, $block);
} # splitCidr


main();


#==============================================================================
#==============================================================================


__END__

=head1 NAME

B<addrtool> - perform IP address/CIDR/netmask calculations

=head1 SYNOPSIS

=over 4

=item B<addrtool> /b

show the netmask for the specified CIDR block

=item B<addrtool> m.m.m.m

show the CIDR block for the specified netmask

=item B<addrtool> a.a.a.a/b

=item B<addrtool> a.a.a.a    m.m.m.m

for the specified address, show the corresponding network and broadcast
addresses, the first and last possible host addresses, and the number of
available host addresses

=item B<addrtool> a.a.a.a/b  /b

=item B<addrtool> a.a.a.a/b  m.m.m.m

=item B<addrtool> a.a.a.a    m.m.m.m  /b

=item B<addrtool> a.a.a.a    m.m.m.m  m.m.m.m

for the specified network (or address), show the network, broadcast,
first host, and last host addresses for all subnets specified by the
second netmask of CIDR block

=item B<addrtool> --help

=item B<addrtool> --man

this man page

=back

=head1 DESCRIPTION

B<addrtool> performs simple calculations related to IP addresses, subnet
masks, and CIDR blocks. It can convert between netmasks and CIDR-style
I</b> notations, show the network and broadcast addresses for a specific
IP address, and split a network into subnets.

In the SYNOPSIS, I<a.a.a.a> is an IP address, I<a.a.a.a/b> is an IP
address with its CIDR block, I</b> is a CIDR block, and I<m.m.m.m> is a
subnet mask.

=head1 EXIT STATUS

B<addrtool> returns 0 on success, 1 for invalid input, and 2 for certain
internal errors

=head1 AUTHOR

Adam Moskowitz, <adamm@menlo.com>

=cut

###
### Copyright, 2009 - 2012, Adam Moskowitz. All rights reserved.
###
### Any redistribution of this software must retain the above copyright
### notice, this list of conditions, and the following disclaimer.
###
### Without specific prior written permission from the copyright holder,
### you may not charge a fee for the redistribution of this software.
###
### All other redistribution and use is hereby permitted.
###
### This software is provided "as is" and without any express or implied
### warranties, including, without limitation, the implied warranties of
### merchantability and fitness for a particular purpose.
###
