#!/usr/bin/perl -w # # shoregen: Generate shorewall configuration for a host from central # configuration files. # # # (c) Copyright 2004-2006 Paul D. Gear # # 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 # 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 \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 tcrules 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 ); 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;