forked from extern/shorewall_code
33214aab85
git-svn-id: https://shorewall.svn.sourceforge.net/svnroot/shorewall/trunk@4115 fbd18981-670d-0410-9b5c-8dc0c1a9a2bb
441 lines
10 KiB
Perl
441 lines
10 KiB
Perl
#!/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;
|
|
}
|
|
|
|
|
|
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;
|