Return to:

Recently, I've been trying to setup IP Masquerading for a small network at home. Looking over the information available, I noticed that there really isn't a simple pre-built solution to setting up an IP Masquerading server. So I decided to write a little script to help ease setup. Once the script is finished, it will hopefully be a usefull utility that anyone can use to set up a basic ip-masq server.

This script will examine your routing table and build a basic set of rules that are suitable for ip masquerading.

If you don't want to cut the script out of this page, you can get it as a gziped file.

Warning: This script should be considered a beta release. If you notice any problems with it, please tell me.

If you want to take a look at an example of how the script will configure the IP masquerading, take a look at a trial run

Note: The trial run is from a previous version of the ip-masq program.

#!/usr/bin/perl -w
# Perl script for setting up an ip masquerading box.  This script relys on
# the /proc filesystem, ipfwadm, and ifconfig.  You must have a kernel
# with ip masquerading support.
# This script was written by Jeffrey Wong .  If you notice
# any problems with it please send me a message describing your problem and
# if possible, how you fixed it.

use strict;

################
# locataion of ipfwadm, change this if needed.
################
my($IPFWADM)='/sbin/ipfwadm';

################
# locataion of ifconfig, change this if needed.
################
my($IFCONFIG)='/sbin/ifconfig';

#################
# If set to Yes, instead of setting up ip masquerading, it will print out
# the commands it will use to set up ip masq.
#################
#my($DEBUG_ON) = "yes";
my($DEBUG_ON) = "no";

##################
# If set to Yes, dhcp packets will be allowed through the masq'd interface.
# If you use a cable modem you will have to set this to Yes.
# This will open up the 2 dhcp ports as well as allowing the dhcp limited
# broadcast address.
##################
my($ALLOW_DHCP) = "yes";
#my($ALLOW_DHCP) = "no";

my(@route); # list that will consist of the following values:
            # ( default route interface, 
            #    [ network interface, interfaces IP mask (aaa.bbb.ccc.ddd/ee) ]
            # )
            # example: (ppp0, eth0, 192.168.1.0/24, eth1, 192.168.2.0/24,
            #             ppp0,123.123.123.123/32)

my ($internet_gateway,$current_interface,$internet_ip,$num);
my (@interfaces,@netmasks,%interface_mask);

# test to see if user is root
if ( $< == 0 || die "Sorry, you're not root.\n")  {}

# test to see if we have access to ipfwadm
if ( -x $IPFWADM || die "Unable to find ipfwadm.\n" ) {}

# test to see if we have ifconfig
if ( -x $IFCONFIG || die "Unable to find ifconfig.\n" ) {}

# test to see if we have a /proc
if ( -d '/proc' || die "/proc filesystem not mounted.\n" ) {}
	
# test to see if ipchains are implemented
if ( -e '/proc/net/ip_fwchains') {
	die "Warning: This version of ip-masq can not be used on a computer supporting\n    ipchaining.\n";
} 
# get routing info
@route = getrouteinfo();

# sort routing info into more useful structures
$internet_gateway = shift(@route);

if ($internet_gateway eq "None") {
    die "Unable to find an internet connection.\n";
}

$internet_ip = get_ip($internet_gateway);

if ($internet_ip eq "Unknown") {
    die "Unable to determine IP for $internet_gateway.\n";
}

while ( $#route >= 0 ) {
    $interface_mask{$route[0]} = $route[1];
    push(@interfaces, shift(@route));
    push(@netmasks, shift(@route));
}

# now we start actually doing the actual masquerade setup.  Instead of 
# directly calling ipfwadm, I'm using a wrapper function that will 
# take its parameters.  This wrapper is used for debugging.

# start by flushing all the old rules and setting default to deny
call_ipfwadm ("-F -f");
call_ipfwadm ("-F -p deny");
call_ipfwadm ("-I -f");
call_ipfwadm ("-I -p deny");
call_ipfwadm ("-O -f");
call_ipfwadm ("-O -p reject");

# allow loopbacks
call_ipfwadm ("-I -a accept -V 127.0.0.1 -S 0.0.0.0/0 -D 0.0.0.0/0");
call_ipfwadm ("-O -a accept -V 127.0.0.1 -S 0.0.0.0/0 -D 0.0.0.0/0");

# now for each interface, we establish rules for it
while ($#interfaces >= 0 ) {
    $current_interface = $interfaces[0];
    if ($current_interface eq $internet_gateway) {
	    # its an interface to the internet

	    # loop through the other internal networks
	for ($num = 0; $num <= $#netmasks; $num++) {

	    if ($netmasks[$num] eq $interface_mask{$current_interface} ) {

		# masked machine trying to get out, allow
		call_ipfwadm (" -O -a accept -W $current_interface -S $internet_ip/32 -D 0.0.0.0/0");

		#outside machine trying to talk to masq/firewall machine, allow
		call_ipfwadm (" -I -a accept -W $current_interface -S 0.0.0.0/0 -D $internet_ip/32");
	    } else {

		# outside machine claiming to be an internal machine, spoofed
		call_ipfwadm (" -I -i deny -W $current_interface -S $netmasks[$num] -D 0.0.0.0/0 -o");

		# internal machine trying to get out, should have been masqed,
		# but it wasn't
		call_ipfwadm (" -O -i deny -W $current_interface -S $netmasks[$num] -D 0.0.0.0/0 -o");

		# outside machine trying to talk to an internal machine 
		# directly, don't let it
		call_ipfwadm (" -I -i deny -W $current_interface -S 0.0.0.0/0 -D $netmasks[$num] -o");

		# internal machine looking for external machine with internal
		# ip number, don't let it
		call_ipfwadm (" -O -i deny -W $current_interface -S 0.0.0.0/0 -D $netmasks[$num] -o");

		# masq anything from an internal ip going out.
		call_ipfwadm (" -F -a m -W $current_interface -S $netmasks[$num] -D 0.0.0.0/0");
		if ($ALLOW_DHCP =~ /yes/i) {
		     # special exception: allow any incoming port 67 or 68 for 
		     # dhcp renewals
		     call_ipfwadm (" -I -i accept -P udp -W $current_interface -S 0.0.0.0/0 -D 0.0.0.0/0 67:68");
		     call_ipfwadm (" -I -i accept -P tcp -W $current_interface -S 0.0.0.0/0 -D 0.0.0.0/0 67:68");
		     call_ipfwadm (" -O -i accept -P udp -W $current_interface -S 0.0.0.0/0 -D 0.0.0.0/0 67:68");
		     call_ipfwadm (" -O -i accept -P tcp -W $current_interface -S 0.0.0.0/0 -D 0.0.0.0/0 67:68");
		     # allow dhcp limited broadcast attempts
		     call_ipfwadm (" -I -i accept -W $current_interface -S 0.0.0.0/0 -D 255.255.255.255/32");
		     call_ipfwadm (" -O -i accept -W $current_interface -S 255.255.255.255/0 -D 0.0.0.0/0");
		}
	    }
	}
    } else {
	# its an interface to the internal network.
	
	# its coming from a machine on this interface, its okay
	call_ipfwadm (" -I -a accept -W $current_interface -S $interface_mask{$current_interface} -D 0.0.0.0/0");
	
	# Unwanted packets to this interface should have been flushed out
	# by -I rules, so if it hasn't been caught yet, it should be okay
	call_ipfwadm (" -O -a accept -W $current_interface -S 0.0.0.0/0 -D $interface_mask{$current_interface}");

    }
    shift (@interfaces);
}

# and we should be done now
exit (0);

sub getrouteinfo {
    # This sets up the array of routing info.

    my (@route);  # routing info as described eariler
    my ($default_interface) = "None";
    my ($mask,$iface);

    open (ROUTE, "/proc/net/route" || die "Unable to find /proc/net/route.\n");

    # first line is a header
    ;
    while (  ) {
	chomp;
	# pull out interface name, destination, and mask
	/(\w+)\s*(\w+)\s*\w+\s*\w+\s*\w+\s*\w+\s*\w+\s*(\w+)\s*\w+\s*\w+\s*\w+/;

	$iface = $1;
	$mask = get_mask( $2, $3 );
	if ( $mask =~ /^127./ ) {
	    # we can skip loopback devices.  All loopback devices match the
	    # network of 127.0.0.0/8
	} elsif ( $mask =~ /^0./ ) {
	    # default routes always match the network 0.0.0.0/8
	    $default_interface = $iface;
	} else {
	    # this should be a host of a network
	    @route = (@route, $iface, $mask);
	}

    }
    unshift(@route,$default_interface);
    return (@route);
}    

sub get_mask {
    # This takes 2 args, the first is an ip in hex format and the second
    # is a netmask in hex format.  It returns the ip in dotted decimal
    # notation followed by a '/' and the number of bits.
    my ($Destination, $Mask) = @_;
    my ($dotted_number,$mask_bits);

    $dotted_number = dehex($Destination);
    $mask_bits = count_bits ($Mask);
    return ("$dotted_number/$mask_bits");
}

sub dehex {
    # this returns a dotted decimal ip from a hex string.  It assumes
    # that the hex string is a 8 hex-digit, little-endian byte ordered,
    # big-endian bit ordered string.
    
    my ($hex_string) = @_;
    my ($decimal_string);
    $hex_string =~ /([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})/i;
    $decimal_string = (hex $4) . "." . (hex $3) . "." . (hex $2) . 
	"." . (hex $1);
    return ($decimal_string)
}

sub count_bits {
    my ($hex_string) = @_;
    my ($bit_count) = 0;
    my ($num,$next_char);
    for ($num = 0; $num < 8; $num++) {
	$next_char = substr($hex_string,$num,1);
	if ($next_char eq '0') {
	} elsif ($next_char eq '8') {
	    $bit_count += 1;
	} elsif ($next_char eq 'C') {
	    $bit_count += 2;
	} elsif ($next_char eq 'E') {
	    $bit_count += 3;
	} elsif ($next_char eq 'F') {
	    $bit_count += 4;
	} else {
	    # anything else is an invalid subnet mask
	    die "Illegal subnet mask ($hex_string) found.\n"
	}
    }
	return ($bit_count);
}

sub call_ipfwadm {
    my ($arg) = @_;
    unless ( $DEBUG_ON =~ /yes/i ) {
	print `$IPFWADM $arg`;
    } else {
	print "$IPFWADM $arg \n";
    }
}

sub get_ip {
    # given an interface, return the ip associated with it or "Unknown"

    my ($iface) = @_;
    my ($ip) = "Unknown";

    open (CONFIG, "$IFCONFIG|" || die "Unable to use ifconfig.\n");
    while (  ) {
	if ( /^$iface/ ) {
	    $_ = ;
	    if ( /inet\saddr:\s*(\S+)/ ) {
		$ip = $1;
	    }
	}
    }
    return ($ip);
}


Last changed on: 19:23 Sat Mar 27, 1999
This website was designed by: Jeffrey Wong
Questions or comments? Feel free to email me at: jmwong@hoku.net