shorewall_code/contrib/shoregen/shoregen
2007-04-16 00:19:02 +00:00

444 lines
10 KiB
Perl
Executable File

#!/usr/bin/perl -w
#
# shoregen: Generate shorewall configuration for a host from central
# configuration files.
#
#
# (c) Copyright 2004-2006 Paul D. Gear <paul@gear.dyndns.org>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA, or go to
# <http://www.gnu.org/copyleft/gpl.html> on the World Wide Web.
#
use strict;
my $VERBOSE = 1;
my $DEBUG = 1;
my $DATE = scalar localtime;
my $HEADER = "#\n# Shorewall %s - constructed by $0 on $DATE\n#\n\n";
my $ret = 0; # return code to shell
if ($#ARGV != 0) {
print STDERR "Usage: $0 <hostname>\n";
exit 1;
}
my $base = ".";
my $host = $ARGV[ 0 ];
my $spool = "$base/SPOOL";
my $dir = "$spool/$host";
#
# Messaging routines for use by the program itself - any errors that are
# generated externally (e.g. file opening problems) are reported using the
# usual perl 'die' or 'warn' functions.
#
sub info
{
print "$0: @_\n";
}
sub mesg
{
my $type = shift;
print STDERR "$0: $type - @_\n";
}
sub warning
{
mesg "WARNING", @_;
}
sub error
{
mesg "ERROR", @_;
++$ret;
}
sub fatal
{
mesg "FATAL", @_;
++$ret;
exit $ret;
}
#
# These bits make the files that actually get copied to the target host
#
sub stripfile
{
open( my $file, $_[ 0 ] ) or die "Can't open $_[ 0 ] for reading: $!";
my @file;
for (<$file>) {
s/\s*#.*$//g; # remove all comments
next if m/^\s*$/; # skip blank lines
push @file, $_;
}
close $file or warn "Can't close $_[ 0 ] after reading: $!";
return @file;
}
#
# Construct a configuration file given a number of input files
#
sub constructfile
{
my $confname = shift;
my $dst = shift;
my $foundone = 0;
info "Constructing $confname" if $VERBOSE > 1;
open( my $DST, ">$dst" ) or die "Can't create $dst: $!";
printf $DST $HEADER, $confname;
for my $file (@_) {
if (-r $file) {
$foundone = 1;
print $DST "##$file\n" if $DEBUG > 1;
print $DST stripfile $file;
}
}
close $DST or warn "Can't close $dst: $!";
if (!$foundone) {
warning "\"$confname\" not present. " .
"Existing file on $host will be preserved." if $VERBOSE > 2;
unlink $dst;
}
}
#
# main
#
my $fw; # Firewall zone for this host
my $router; # Is this host a router?
my @globalzones; # All known zones
my %globalzones;
my %hostzones; # zones applicable to this host
my $outfile; # filename holders
my $conf; # config file we're processing at present
my %warnban; # meta-rules/policies
# Change to the base configuration directory
die "Configuration directory $base doesn't exist!" if ! -d $base;
chdir $base or die "Can't change directory to $base: $!";
# Create spool directories if necessary
if (! -d "$spool") {
mkdir "$spool" or die "Can't create spool directory $spool: $!";
}
if (! -d $dir) {
mkdir $dir or die "Can't create host spool directory $dir: $!";
}
#
# Construct all the simple config files.
#
# Config files for which the host-specific file is included *first*
my @hostfirstconfigs = qw(
accounting
actions
blacklist
bogons
continue
ecn
hosts
interfaces
maclist
masq
nat
netmap
proxyarp
rfc1918
routestopped
route_rules
start
started
stop
stopped
tcclasses
tcdevices
tos
tunnels
);
# Config files for which the host-specific file is included *last*
my @hostlastconfigs = qw(
common
configpath
init
initdone
ipsec
modules
params
providers
shorewall.conf
tcrules
);
for my $conf (@hostfirstconfigs) {
constructfile "$conf", "$dir/$conf", "$conf/$host", "$conf/COMMON";
}
for my $conf (@hostlastconfigs) {
constructfile "$conf", "$dir/$conf", "$conf/COMMON", "$conf/$host";
}
#
# The remaining config files (policy, rules, zones) are processed uniquely.
#
# Find the firewall name of this host
open( my $infile, "$dir/shorewall.conf" ) or
die "Can't open $dir/shorewall.conf: $!";
for (<$infile>) {
if (/^\s*FW=(\S+)/) {
$fw = $1 unless defined $fw;
}
if (/^\s*IP_FORWARDING=(\S+)/) {
$router = $1 unless defined $router;
}
}
close $infile;
# The firewall name must be defined
unless (defined $fw) {
fatal "Can't find firewall name (FW variable) for $host in $dir/shorewall.conf";
}
# Router must be defined
unless (defined $router) {
fatal "Can't find IP_FORWARDING setting for $host in $dir/shorewall.conf";
}
if ($router =~ m/On|Yes/i) {
$router = 1;
}
else {
$router = 0;
}
print "fw=$fw, router=$router\n" if $DEBUG > 3;
# Find all valid zones
unless (-r "zones") {
fatal "You must provide a global zone file";
}
for (stripfile "zones") {
chomp;
my ($zone, $details) = split /[\s:]+/, $_, 2;
push @globalzones, $zone;
$globalzones{ $zone } = $details;
}
#
# Work out which zones apply to this host from the combination of hosts &
# interfaces. The first field in both files is the zone name, and the
# second (minus any trailing ips) is the interface, which we save as well
# for later reference.
#
for my $infile ("$dir/hosts", "$dir/interfaces") {
if (-r $infile) {
for (stripfile $infile) {
chomp;
my @F = split;
next if $#F < 0;
next if $F[ 0 ] eq "-";
my @IF = split /:/, $F[ 0 ]; # strip off parent zone, if present
$hostzones{ $IF[ 0 ] } = 1;
}
}
}
$conf = "zones";
#
# Create the zones file from the intersection of the above - note the order
# from the original zone file must be preserved, hence the need for the
# array as well as the hash.
#
open( $outfile, ">$dir/$conf" ) or
die "Can't open $dir/$conf for writing: $!";
printf $outfile $HEADER, "$conf";
my %tmpzones = %hostzones; # Take a copy of all the zones,
for my $zone (@globalzones) {
if (exists $tmpzones{ $zone }) {
print $outfile "$zone $globalzones{ $zone }\n";
delete $tmpzones{ $zone }; # deleting those found as we go along.
}
}
close $outfile or warn "Can't close $dir/$conf after writing: $!";
for my $zone (sort keys %tmpzones) { # Warn if we've got any zones left now.
#next if $zone eq "-";
warning "No entry for $zone in global zones file - ignored";
}
undef %tmpzones;
my @tmp = sort keys %hostzones;
info "FW zone for $host: $fw" if $VERBOSE > 0;
info "Other zones for $host: @tmp" if $VERBOSE > 0;
#
# Add 'all' as a valid source or destination. Added here so it doesn't get
# checked in %tmpzones check above. Also add firewall itself. (The
# numbers are not important as long as they are non-zero.)
#
$hostzones{"all"} = 1;
$hostzones{$fw} = 1;
#
# Create the policy file, including only the applicable zones.
#
$conf = "policy";
if (! -r $conf) {
fatal "You must provide a global \"$conf\" file";
}
open( $outfile, ">$dir/$conf" ) or
die "Can't open $dir/$conf for writing: $!";
printf $outfile $HEADER, "$conf";
for (stripfile $conf) {
chomp;
my ($src, $dst, $pol, $rest) = split /\s+/, $_, 4;
print "$src, $dst, $pol, $rest\n" if $DEBUG > 3;
# Both source and destination zones must be valid on this host for this
# policy to apply.
next unless defined $hostzones{$src} and defined $hostzones{$dst};
# Source and destination zones must be on different interfaces as well,
# except for the case of all2all.
#next if ($hostzones{$src} eq $hostzones{$dst} && $src ne "all");
# Save WARN & BAN details for later rules processing
if ($pol eq "WARN" or $pol eq "BAN") {
if (exists $warnban{$src}{$dst}) {
error "Duplicate WARN/BAN rule: $src,$dst,$pol - possible typo?";
}
$warnban{$src}{$dst} = $pol;
next;
}
printf $outfile "%s\n", $_;
}
close $outfile or warn "Can't close $dir/$conf for writing: $!";
#
# Create the rules file, only including the applicable zones and taking
# into account any WARN or BAN policies.
#
$conf = "rules";
if (! -r $conf) {
fatal "You must provide a global \"$conf\" file";
}
open( $outfile, ">$dir/$conf" ) or
die "Can't open $dir/$conf for writing: $!";
printf $outfile $HEADER, "$conf";
for my $infile ("$conf.COMMON", "$conf.$host", "$conf") {
next unless -r $infile;
for (stripfile $infile) {
chomp;
my ($act, $src, $dst, $rest) = split /\s+/, $_, 4;
$act =~ s/:.*//; # strip off logging directives
$src =~ s/:.*//; # strip off host & port specifiers
$dst =~ s/:.*//; # strip off host & port specifiers
print "$act, $src, $dst, $rest\n" if $DEBUG > 3;
# Both source and destination zones must be valid on this host
# for this rule to apply.
next unless defined $hostzones{$src} and defined $hostzones{$dst};
# If host is not a router, either the source or destination zone
# must be the firewall itself.
if (!$router) {
next unless $src eq $fw
or $dst eq $fw
or $src eq "all"
or $dst eq "all";
}
# Save additional WARN/BAN rules
if ($act eq "WARN" or $act eq "BAN") {
if (exists $warnban{$src}{$dst}) {
error "Duplicate WARN/BAN rule: $src,$dst,$act - possible typo?";
}
$warnban{$src}{$dst} = $act;
next;
}
# Check against WARN/BAN rules
if (exists $warnban{$src}{$dst} && $act =~ /^(ACCEPT|Allow|DNAT)/) {
if ($warnban{$src}{$dst} eq "WARN") {
warning "Rule contravenes WARN policy:\n\t$_";
}
else { # $warnban{$src}{$dst} eq "BAN"
error "Rule contravenes BAN policy (omitted):\n\t$_";
next;
}
}
# Mangle DNAT rules if the destination is the local machine
if ($act =~ /^DNAT/ && $dst eq $fw) {
$_ =~ s/\bDNAT(-)?/ACCEPT/; # change rule type
$_ =~ s/\b$fw:\S+/$dst/; # strip trailing server address/port
}
printf $outfile "%s\n", $_;
}
}
close $outfile or warn "Can't close $dir/$conf for writing: $!";
# Finished - return whatever we produced above...
exit $ret;