#
#    PortFW.pm - Perl module to interface with ipmasqadm portfw command.
#
#    This file is part of Fwctl.
#
#    Author: Francis J. Lacoste <francis@iNsu.COM>
#
#    Copyright (C) 1999 Francis J. Lacoste, iNsu Innovations Inc.
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms same terms as perl itself.
#
package IPChains::PortFW;

use strict;

use Carp;
use Symbol;

use vars qw( $VERSION $IPMASQADM );

BEGIN {

  ($VERSION) = '$Revision: 1.2 $' =~ /Revision: ([0-9.]+)/;

}

my %VALID_OPTIONS = map { $_ => 1 } qw( LocalAddr LocalPort RemAddr RemPort
					Proto Pref
					);

sub new {
    my $proto = shift;
    my $class = ref $proto || $proto;
    my %args = @_;
    my $self = { };

    # Look for ipmasqadm
    my ($path) = grep { -x "$_/ipmasqadm" } split /:/, "/sbin:/bin:/usr/sbin:/usr/bin:$ENV{PATH}";
    die ( "Couldn't find ipmasqadm in PATH ($ENV{PATH})\n" ) unless $path;
    $self->{ipmasqadm} = "$path/ipmasqadm";

    bless $self, $class;

    while ( my ($key,$value) = each %args ) {
	$self->attribute( $key, $value );
    }

    $self;
}

sub attribute {
    my ($self,$key,$value) = @_;

    if ( @_ == 3 ) {
	if ( $VALID_OPTIONS{$key} ) {
	    $self->{$key} = $value;
	} else {
	    carp "Unknown option : $key";
	}
    }

    return $self->{$key};
}

sub clopts {
    my ( $self ) = shift;

    foreach my $key ( keys %VALID_OPTIONS ) {
	delete $self->{$key};
    }
}

sub run_portfw {
    my ( $self, @args ) = @_;

    my ($r_fh,$w_fh) = (gensym,gensym);
    pipe $r_fh, $w_fh
      or die "can't pipe: $!\n";

    my $pid = fork;
    die "can't fork: $!\n" unless defined $pid;

    if ( $pid ) {
	# Don't need this one
	close $w_fh;

	# Collect STDOUT and STDERR
	my $output;
	while ( my $line = <$r_fh> ) {
	    $output .= $line;
	}

	# Collect exit status
	waitpid $pid,0;

	die "ipmasq exit with non zero status:\n$output\n" if $?;

	$output;
    } else {
	# Don't need this one
	close $r_fh;

	# Redirect STDOUT and STDERR to parent
	open ( STDOUT, ">&" . fileno $w_fh )
	  or die "can't redirect STDOUT to proper pipe: $!\n";
	open ( STDERR, ">&" . fileno $w_fh )
	  or die "can't redirect STDERR to proper output fd: $!\n";
	exec( $self->{ipmasqadm}, "portfw", @args )
	  or die "can't exec ipmasqadm: $!";
    }
}

sub append {
    my ( $self ) = shift;

    my @args = ( "-a" );
    croak "missing protocol"	    unless exists $self->{Proto};
    croak "invalid protocol"	    unless $self->{Proto} =~ /udp|tcp|6|17/i;
    croak "missing local address"   unless exists $self->{LocalAddr};
    croak "missing local port"	    unless exists $self->{LocalPort};
    croak "missing remote address"  unless exists $self->{RemAddr};
    croak "missing remote port"	    unless exists $self->{RemPort};
    if ( exists $self->{Pref} ) {
	croak "invalid preference"  unless $self->{Pref} =~ /\d+/ &&
					   $self->{Pref} >= 0;
    }

    push @args, "-P", lc $self->{Proto}, "-L", $self->{LocalAddr},
      $self->{LocalPort}, "-R", $self->{RemAddr}, $self->{RemPort};
    push @args, "-p", $self->{Pref} if exists $self->{Pref};

    $self->run_portfw( @args );
}

sub delete {
    my ( $self ) = shift;

    my @args = ( "-d" );
    croak "missing protocol"	    unless exists $self->{Proto};
    croak "invalid protocol"	    unless $self->{Proto} =~ /udp|tcp|6|17/i;
    croak "missing local address"   unless exists $self->{LocalAddr};
    croak "missing local port"	    unless exists $self->{LocalPort};

    push @args, "-P", lc $self->{Proto}, "-L", $self->{LocalAddr},
      $self->{LocalPort};
    push @args, "-R", $self->{RemAddr}, $self->{RemPort}
      if exists $self->{RemAddr};

    $self->run_portfw( @args );

}

sub flush {
    $_[0]->run_portfw( "-f" );
}

sub list {
    my ($self, $use_dns) = @_;

    my @args = ( "-l" );
    push @args, "-n" unless defined $use_dns && $use_dns;

    my $output = $self->run_portfw( @args );
    return () unless defined $output;

    # Parse output
    my @lines = split /\n/, $output;

    # Skip header line
    shift @lines;

    my @rules = ();
    foreach my $line ( @lines ) {
	my ( $prot, $laddr, $raddr, $lport, $rport, undef, $pref ) =
	  split / +/, $line;
	push @rules, $self->new( Proto	    => lc $prot,
				  LocalAddr => $laddr,
				  RemAddr   => $raddr,
				  LocalPort => $lport,
				  RemPort   => $rport,
				);
    }

    @rules;
}

1;

__END__

=pod

=head1 NAME

IPChains::PortFW - Perl module to manipulate portfw masquerading table.

=head1 SYNOPSIS

  my $masq = new IPChains::PortFW( option => value, ... );
  $masq->append();

=head1 DESCRIPTION

IPChains::PortFW is an perl interface to the linux kernel port forwarding
facility. You must have ipmasqadm and the portfw module installed for this
module to work. A kernel compiled with CONFIG_IP_MASQUERADE_IPPORTFW would
also helps.

It has a similar interface than the IPChains(3) module. You create an
IPChains::PortFW object with new(), you can query or set attributes with
the attribute() method and you add or deletes the port forwarding rules using
append() or delete().

=head2 ATTRIBUTES

Here are the attributes valids for IPChains::PortFW.

=over

=item LocalAddr

This is the local address from which packets will be redirected.

=item LocalPort

This is the port from which packets will be redirected.

=item RemAddr

This is the address to which the packets will be forwarded to.

=item RemPort

This is the port to which the packets will be forwarded to.

=item Pref

This is a preferences value used for load balancing in the case when
there are many possible remote destinations.

=back

=head2 METHODS

=over

=item new( [options], ... )

Create a new IPChains::PortFW object and sets its attributes.


=item attribute( attribute [, value] )

Get or sets an attribute. Use undef to delete a value.

=item clopts()

Unset all attributes.

=item append()

Append a rule to the port forwarding masquerade table as specified by
the attributes of the current objects.

=item delete()

Deletes entries in the port forwarding masquerade table. The entries
matching the attributes will be deleted.

=item flush()

Removes all entries from the port forwarding masquerade table.

=item list()

Returns an array of IPChains::PortFW objects. One for each entries
in the port forwarding table.

=back

=head1 EXAMPLE

Redirecting http protocol to internal web server.

my $portfw = new IPChains::PortFW( Proto     => 'udp', 
				   LocalAddr => '199.168.1.10',
				   LocalPort => 80,
				   RemAddr   => '10.0.0.1',
				   RemPort   => 80 );

$portfw->append;

=head1 AUTHOR

Francis J. Lacoste <francis@insu.com>

=head1 COPYRIGHT

Copyright (c) 1999 Francis J. Lacoste and iNsu Innovations Inc.
All rights reserved.

This program is free software; you can redistribute it and/or modify
it under the terms as perl itself.

=head1 SEE ALSO

IPChains(3)

=cut

