#
# Shorewall 4.4 -- /usr/share/shorewall/Shorewall/Zones.pm
#
#     This program is under GPL [http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt]
#
#     (c) 2007,2008,2009,2010 - Tom Eastep (teastep@shorewall.net)
#
#       Complete documentation is available at http://shorewall.net
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of Version 2 of the GNU General Public License
#       as published by the Free Software Foundation.
#
#       This program is distributed in the shope 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
#   This module contains the code which deals with /etc/shorewall/zones,
#   /etc/shorewall/interfaces and /etc/shorewall/hosts.
#
package Shorewall::Zones;
require Exporter;
use Shorewall::Config qw(:DEFAULT :internal);
use Shorewall::IPAddrs;

use strict;

our @ISA = qw(Exporter);
our @EXPORT = qw( NOTHING
		  NUMERIC
		  NETWORK
		  IPSECPROTO
		  IPSECMODE
		  FIREWALL
		  VSERVER
		  IP
		  BPORT
		  IPSEC

		  determine_zones
		  zone_report
		  dump_zone_contents
		  find_zone
		  firewall_zone
		  defined_zone
		  zone_type
		  zone_interfaces
		  all_zones
		  all_parent_zones
		  complex_zones
		  vserver_zones
		  off_firewall_zones
		  non_firewall_zones
		  single_interface
		  chain_base
		  validate_interfaces_file
		  all_interfaces
		  all_bridges
		  interface_number
		  find_interface
		  known_interface
		  get_physical
		  physical_name
		  have_bridges
		  port_to_bridge
		  source_port_to_bridge
		  interface_is_optional
		  find_interfaces_by_option
		  find_interfaces_by_option1
		  get_interface_option
		  set_interface_option
		  verify_required_interfaces
		  compile_updown
		  validate_hosts_file
		  find_hosts_by_option
		  find_zones_by_option
		  all_ipsets
		  have_ipsec
		 );

our @EXPORT_OK = qw( initialize );
our $VERSION = '4.4_15';

#
# IPSEC Option types
#
use constant { NOTHING    => 'NOTHING',
	       NUMERIC    => '0x[\da-fA-F]+|\d+',
	       NETWORK    => '\d+.\d+.\d+.\d+(\/\d+)?',
	       IPSECPROTO => 'ah|esp|ipcomp',
	       IPSECMODE  => 'tunnel|transport'
	       };
#
# Zone Table.
#
#     @zones contains the ordered list of zones with sub-zones appearing before their parents.
#
#     %zones{<zone1> => {type = >      <zone type>       FIREWALL, IP, IPSEC, BPORT;
#                        options =>    { complex => 0|1
#                                        nested  => 0|1
#                                        super   => 0|1
#                                        in_out  => < policy match string >
#                                        in      => < policy match string >
#                                        out     => < policy match string >
#                                      }
#                        parents =>    [ <parents> ]      Parents, Children and interfaces are listed by name
#                        children =>   [ <children> ]
#                        interfaces => { <interfaces1> => 1, ... }
#                        bridge =>     <bridge>
#                        hosts { <type> } => [ { <interface1> => { ipsec   => 'ipsec'|'none'
#                                                                  options => { <option1> => <value1>
#                                                                               ...
#                                                                             }
#                                                                  hosts   => [ <net1> , <net2> , ... ]
#                                                                  exclusions => [ <net1>, <net2>, ... ]
#                                                                }
#                                                <interface2> => ...
#                                              }
#                                            ]
#                       }
#             <zone2> => ...
#           }
#
#     $firewall_zone names the firewall zone.
#
our @zones;
our %zones;
our $firewall_zone;

our %reservedName = ( all => 1,
		      any => 1,
		      none => 1,
		      SOURCE => 1,
		      DEST => 1 );

#
#     Interface Table.
#
#     @interfaces lists the interface names in the order that they appear in the interfaces file.
#
#     %interfaces { <interface1> => { name        => <name of interface>
#                                     root        => <name without trailing '+'>
#                                     options     => { port => undef|1
#                                                      <option1> = <val1> ,          #See %validinterfaceoptions
#                                                      ...
#                                                    }
#                                     zone        => <zone name>
#                                     nets        => <number of nets in interface/hosts records referring to this interface>
#                                     bridge      => <bridge>
#                                     broadcasts  => 'none', 'detect' or [ <addr1>, <addr2>, ... ]
#                                     number      => <ordinal position in the interfaces file>
#                                     physical    => <physical interface name>
#                                     base        => <shell variable base representing this interface>
#                                   }
#                 }
#
#    The purpose of the 'base' member is to ensure that the base names associated with the physical interfaces are assigned in
#    the same order as the interfaces are encountered in the configuration files.
#
our @interfaces;
our %interfaces;
our @bport_zones;
our %ipsets;
our %physical;
our %basemap;
our %mapbase;
our $family;
our $have_ipsec;
our $baseseq;

use constant { FIREWALL => 1,
	       IP       => 2,
	       BPORT    => 3,
	       IPSEC    => 4,
	       VSERVER  => 5 };

use constant { SIMPLE_IF_OPTION   => 1,
	       BINARY_IF_OPTION   => 2,
	       ENUM_IF_OPTION     => 3,
	       NUMERIC_IF_OPTION  => 4,
	       OBSOLETE_IF_OPTION => 5,
	       IPLIST_IF_OPTION   => 6,
	       STRING_IF_OPTION   => 7,

	       MASK_IF_OPTION     => 7,

	       IF_OPTION_ZONEONLY => 8,
	       IF_OPTION_HOST     => 16,
	       IF_OPTION_VSERVER  => 32,
	   };

our %validinterfaceoptions;

our %defaultinterfaceoptions = ( routefilter => 1 , wait => 60 );

our %maxoptionvalue = ( routefilter => 2, mss => 100000 , wait => 120 );

our %validhostoptions;

#
# Rather than initializing globals in an INIT block or during declaration,
# we initialize them in a function. This is done for two reasons:
#
#   1. Proper initialization depends on the address family which isn't
#      known until the compiler has started.
#
#   2. The compiler can run multiple times in the same process so it has to be
#      able to re-initialize its dependent modules' state.
#
sub initialize( $ ) {
    $family = shift;
    @zones = ();
    %zones = ();
    $firewall_zone = '';
    $have_ipsec = undef;

    @interfaces = ();
    %interfaces = ();
    @bport_zones = ();
    %ipsets = ();
    %physical = ();
    %basemap = ();
    %mapbase = ();
    $baseseq = 0;

    if ( $family == F_IPV4 ) {
	%validinterfaceoptions = (arp_filter  => BINARY_IF_OPTION,
				  arp_ignore  => ENUM_IF_OPTION,
				  blacklist   => SIMPLE_IF_OPTION + IF_OPTION_HOST,
				  bridge      => SIMPLE_IF_OPTION,
				  detectnets  => OBSOLETE_IF_OPTION,
				  dhcp        => SIMPLE_IF_OPTION,
				  maclist     => SIMPLE_IF_OPTION + IF_OPTION_HOST,
				  logmartians => BINARY_IF_OPTION,
				  nets        => IPLIST_IF_OPTION + IF_OPTION_ZONEONLY + IF_OPTION_VSERVER,
				  norfc1918   => OBSOLETE_IF_OPTION,
				  nosmurfs    => SIMPLE_IF_OPTION + IF_OPTION_HOST,
				  optional    => SIMPLE_IF_OPTION,
				  proxyarp    => BINARY_IF_OPTION,
				  required    => SIMPLE_IF_OPTION,
				  routeback   => SIMPLE_IF_OPTION + IF_OPTION_ZONEONLY + IF_OPTION_HOST + IF_OPTION_VSERVER,
				  routefilter => NUMERIC_IF_OPTION ,
				  sourceroute => BINARY_IF_OPTION,
				  tcpflags    => SIMPLE_IF_OPTION + IF_OPTION_HOST,
				  upnp        => SIMPLE_IF_OPTION,
				  upnpclient  => SIMPLE_IF_OPTION,
				  mss         => NUMERIC_IF_OPTION,
				  physical    => STRING_IF_OPTION + IF_OPTION_HOST,
				  wait        => NUMERIC_IF_OPTION,
				 );
	%validhostoptions = (
			     blacklist => 1,
			     maclist => 1,
			     nosmurfs => 1,
			     routeback => 1,
			     tcpflags => 1,
			     broadcast => 1,
			     destonly => 1,
			     sourceonly => 1,
			    );
    } else {
	%validinterfaceoptions = (  blacklist   => SIMPLE_IF_OPTION + IF_OPTION_HOST,
				    bridge      => SIMPLE_IF_OPTION,
				    dhcp        => SIMPLE_IF_OPTION,
				    maclist     => SIMPLE_IF_OPTION + IF_OPTION_HOST,
				    nets        => IPLIST_IF_OPTION + IF_OPTION_ZONEONLY + IF_OPTION_VSERVER,
				    nosmurfs    => SIMPLE_IF_OPTION + IF_OPTION_HOST,
				    optional    => SIMPLE_IF_OPTION,
				    proxyndp    => BINARY_IF_OPTION,
				    required    => SIMPLE_IF_OPTION,
				    routeback   => SIMPLE_IF_OPTION + IF_OPTION_ZONEONLY + IF_OPTION_HOST + IF_OPTION_VSERVER,
				    sourceroute => BINARY_IF_OPTION,
				    tcpflags    => SIMPLE_IF_OPTION + IF_OPTION_HOST,
				    mss         => NUMERIC_IF_OPTION,
				    forward     => BINARY_IF_OPTION,
				    physical    => STRING_IF_OPTION + IF_OPTION_HOST,
				    wait        => NUMERIC_IF_OPTION,
				 );
	%validhostoptions = (
			     blacklist => 1,
			     maclist => 1,
			     routeback => 1,
			     tcpflags => 1,
			    );
    }
}

#
# Parse the passed option list and return a reference to a hash as follows:
#
# => mss   = <MSS setting>
# => ipsec = <-m policy arguments to match options>
#
sub parse_zone_option_list($$\$)
{
    my %validoptions = ( mss          => NUMERIC,
			 blacklist    => NOTHING,
			 strict       => NOTHING,
			 next         => NOTHING,
			 reqid        => NUMERIC,
			 spi          => NUMERIC,
			 proto        => IPSECPROTO,
			 mode         => IPSECMODE,
			 "tunnel-src" => NETWORK,
			 "tunnel-dst" => NETWORK,
		       );

    use constant { UNRESTRICTED => 1, NOFW => 2 , COMPLEX => 8 };
    #
    # Hash of options that have their own key in the returned hash.
    #
    my %key = ( mss => UNRESTRICTED | COMPLEX , blacklist => NOFW );

    my ( $list, $zonetype, $complexref ) = @_;
    my %h;
    my $options = '';
    my $fmt;

    if ( $list ne '-' ) {
	for my $e ( split_list $list, 'option' ) {
	    my $val    = undef;
	    my $invert = '';

	    if ( $e =~ /([\w-]+)!=(.+)/ ) {
		$val    = $2;
		$e      = $1;
		$invert = '! ';
	    } elsif ( $e =~ /([\w-]+)=(.+)/ ) {
		$val = $2;
		$e   = $1;
	    }

	    $fmt = $validoptions{$e};

	    fatal_error "Invalid Option ($e)" unless $fmt;

	    if ( $fmt eq NOTHING ) {
		fatal_error "Option \"$e\" does not take a value" if defined $val;
	    } else {
		fatal_error "Missing value for option \"$e\""        unless defined $val;
		fatal_error "Invalid value ($val) for option \"$e\"" unless $val =~ /^($fmt)$/;
	    }

	    my $key = $key{$e};

	    if ( $key ) {
		fatal_error "Option '$e' not permitted with this zone type " if $key & NOFW && ($zonetype == FIREWALL || $zonetype == VSERVER);
		$$complexref = 1 if $key & COMPLEX;
		$h{$e} = $val || 1;
	    } else {
		fatal_error "The \"$e\" option may only be specified for ipsec zones" unless $zonetype == IPSEC;
		$options .= $invert;
		$options .= "--$e ";
		$options .= "$val "if defined $val;
		$$complexref = 1;
	    }
	}
    }

    $h{ipsec} = $options ? "$options " : '';

    \%h;
}

#
# Set the super option on the passed zoneref and propagate to its parents
#
sub set_super( $ );

sub set_super( $ ) {
    my $zoneref = shift;

    unless ( $zoneref->{options}{super} ) {
	$zoneref->{options}{super} = 1;
	set_super( $zones{$_} ) for @{$zoneref->{parents}};
    }
}

#
# Process a record in the zones file
#
sub process_zone( \$ ) {
    my $ip = $_[0];

    my @parents;

    my ($zone, $type, $options, $in_options, $out_options ) = split_line 1, 5, 'zones file';

    if ( $zone =~ /(\w+):([\w,]+)/ ) {
	$zone = $1;
	@parents = split_list $2, 'zone';

	for my $p ( @parents ) {
	    fatal_error "Invalid Parent List ($2)" unless $p;
	    fatal_error "Unknown parent zone ($p)" unless $zones{$p};
	    fatal_error 'Subzones of firewall zone not allowed' if $zones{$p}{type} == FIREWALL;
	    fatal_error 'Subzones of a Vserver zone not allowed' if $zones{$p}{type} == VSERVER;
	    push @{$zones{$p}{children}}, $zone;
	}
    }

    fatal_error "Invalid zone name ($zone)"      unless $zone =~ /^[a-z]\w*$/i && length $zone <= $globals{MAXZONENAMELENGTH};
    fatal_error "Invalid zone name ($zone)"      if $reservedName{$zone} || $zone =~ /^all2|2all$/;
    fatal_error( "Duplicate zone name ($zone)" ) if $zones{$zone};

    if ( $type =~ /^ip(v([46]))?$/i ) {
	fatal_error "Invalid zone type ($type)" if $1 && $2 != $family;
	$type = IP;
	$$ip = 1;
    } elsif ( $type =~ /^ipsec([46])?$/i ) {
	fatal_error "Invalid zone type ($type)" if $1 && $1 != $family;
	$type = IPSEC;
    } elsif ( $type =~ /^bport([46])?$/i ) {
	fatal_error "Invalid zone type ($type)" if $1 && $1 != $family;
	warning_message "Bridge Port zones should have a parent zone" unless @parents;
	$type = BPORT;
	push @bport_zones, $zone;
    } elsif ( $type eq 'firewall' ) {
	fatal_error 'Firewall zone may not be nested' if @parents;
	fatal_error "Only one firewall zone may be defined ($zone)" if $firewall_zone;
	$firewall_zone = $zone;
	$params{FW} = $zone;
	$type = FIREWALL;
    } elsif ( $type eq 'vserver' ) {
	fatal_error 'Vserver zones may not be nested' if @parents;
	$type = VSERVER;
    } elsif ( $type eq '-' ) {
	$type = IP;
	$$ip = 1;
    } else {
	fatal_error "Invalid zone type ($type)";
    }

    if ( $type eq IPSEC ) {
	require_capability 'POLICY_MATCH' , 'IPSEC zones', '';
	for ( @parents ) {
	    set_super( $zones{$_} ) unless $zones{$_}{type} == IPSEC;
	}
    }

    my $complex = 0;

    my $zoneref = $zones{$zone} = { type       => $type,
				    parents    => \@parents,
				    bridge     => '',
				    options    => { in_out  => parse_zone_option_list( $options , $type, $complex ) ,
						    in      => parse_zone_option_list( $in_options , $type , $complex ) ,
						    out     => parse_zone_option_list( $out_options , $type , $complex ) ,
						    complex => ( $type == IPSEC || $complex ) ,
						    nested  => @parents > 0 ,
						    super   => 0 ,
						  } ,
				    interfaces => {} ,
				    children   => [] ,
				    hosts      => {}
				  };

    if ( $zoneref->{options}{in_out}{blacklist} ) {
	for ( qw/in out/ ) {
	    unless ( $zoneref->{options}{$_}{blacklist} ) {
		$zoneref->{options}{$_}{blacklist} = 1;
	    } else {
		warning_message( "Redundant 'blacklist' in " . uc( $_ ) . '_OPTIONS' );
	    }
	}
    }

    return $zone;

}
#
# Parse the zones file.
#
sub determine_zones()
{
    my @z;
    my $ip = 0;

    if ( my $fn = open_file 'zones' ) {
	first_entry "$doing $fn...";
	push @z, process_zone( $ip ) while read_a_line;
    } else {
	fatal_error q(The 'zones' file does not exist or has zero size);
    }

    fatal_error "No firewall zone defined" unless $firewall_zone;
    fatal_error "No IP zones defined" unless $ip;
    #
    # Topological sort to place sub-zones before all of their parents
    #
    my %ordered;

  PUSHED:
    {
      ZONE:
	for my $zone ( @z ) {
	    unless ( $ordered{$zone} ) {
		for ( @{$zones{$zone}{children}} ) {
		    next ZONE unless $ordered{$_};
		}
		$ordered{$zone} = 1;
		push @zones, $zone;
		redo PUSHED;
	    }
	}
    }

    assert( scalar @zones == scalar @z );

}

#
# Return true of we have any ipsec zones
#
sub haveipseczones() {
    for my $zoneref ( values %zones ) {
	return 1 if $zoneref->{type} == IPSEC;
    }

    0;
}

#
# Report about zones.
#
sub zone_report()
{
    progress_message2 "Determining Hosts in Zones...";

    my @translate;

    if ( $family == F_IPV4 ) {
	@translate = ( undef, 'firewall', 'ipv4', 'bport4', 'ipsec4', 'vserver' );
    } else {
	@translate = ( undef, 'firewall', 'ipv6', 'bport6', 'ipsec6', 'vserver' );
    }

    for my $zone ( @zones )
    {
	my $zoneref   = $zones{$zone};
	my $hostref   = $zoneref->{hosts};
	my $type      = $zoneref->{type};
	my $optionref = $zoneref->{options};

	progress_message_nocompress "   $zone ($translate[$type])";

	my $printed = 0;

	if ( $hostref ) {
	    for my $type ( sort keys %$hostref ) {
		my $interfaceref = $hostref->{$type};

		for my $interface ( sort keys %$interfaceref ) {
		    my $iref     = $interfaces{$interface};
		    my $arrayref = $interfaceref->{$interface};
		    for my $groupref ( @$arrayref ) {
			my $hosts      = $groupref->{hosts};
			if ( $hosts ) {
			    my $grouplist  = join ',', ( @$hosts );
			    my $exclusions = join ',', @{$groupref->{exclusions}};
			    $grouplist = join '!', ( $grouplist, $exclusions) if $exclusions;

			    if ( $family == F_IPV4 ) {
				progress_message_nocompress "      $iref->{physical}:$grouplist";
			    } else {
				progress_message_nocompress "      $iref->{physical}:<$grouplist>";
			    }
			    $printed = 1;
			}
		    }

		}
	    }
	}

	unless ( $printed ) {
	    fatal_error "No bridge has been associated with zone $zone" if $type == BPORT && ! $zoneref->{bridge};
	    warning_message "*** $zone is an EMPTY ZONE ***" unless $type == FIREWALL;
	}

    }
}

#
# This function is called to create the contents of the ${VARDIR}/zones file
#
sub dump_zone_contents()
{
    my @xlate;

    if ( $family == F_IPV4 ) {
	@xlate = ( undef, 'firewall', 'ipv4', 'bport4', 'ipsec4', 'vserver' );
    } else {
	@xlate = ( undef, 'firewall', 'ipv6', 'bport6', 'ipsec6', 'vserver' );
    }

    for my $zone ( @zones )
    {
	my $zoneref    = $zones{$zone};
	my $hostref    = $zoneref->{hosts};
	my $type       = $zoneref->{type};
	my $optionref  = $zoneref->{options};

	my $entry      =  "$zone $xlate[$type]";

	$entry .= ":$zoneref->{bridge}" if $type == BPORT;

	if ( $hostref ) {
	    for my $type ( sort keys %$hostref ) {
		my $interfaceref = $hostref->{$type};

		for my $interface ( sort keys %$interfaceref ) {
		    my $iref     = $interfaces{$interface};
		    my $arrayref = $interfaceref->{$interface};
		    for my $groupref ( @$arrayref ) {
			my $hosts     = $groupref->{hosts};

			if ( $hosts ) {
			    my $grouplist  = join ',', ( @$hosts );
			    my $exclusions = join ',', @{$groupref->{exclusions}};

			    $grouplist = join '!', ( $grouplist, $exclusions ) if $exclusions;

			    if ( $family == F_IPV4 ) {
				$entry .= " $iref->{physical}:$grouplist";
			    } else {
				$entry .= " $iref->{physical}:<$grouplist>";
			    }
			}
		    }
		}
	    }
	}

	emit_unindented $entry;
    }
}

#
# If the passed zone is associated with a single interface, the name of the interface is returned. Otherwise, the funtion returns '';
#
sub single_interface( $ ) {
    my $zone = $_[0];
    my $zoneref = $zones{$zone};

    assert( $zoneref );

    my @keys = keys( %{$zoneref->{interfaces}} );

    @keys == 1 ? $keys[0] : '';
}

sub add_group_to_zone($$$$$)
{
    my ($zone, $type, $interface, $networks, $options) = @_;
    my $hostsref;
    my $typeref;
    my $interfaceref;
    my $zoneref  = $zones{$zone};
    my $zonetype = $zoneref->{type};

    $zoneref->{interfaces}{$interface} = 1;

    my @newnetworks;
    my @exclusions = ();
    my $new = \@newnetworks;
    my $switched = 0;
    my $allip    = 0;

    for my $host ( @$networks ) {
	$interfaceref = $interfaces{$interface};

	$interfaceref->{nets}++;

	fatal_error "Invalid Host List" unless defined $host and $host ne '';

	if ( substr( $host, 0, 1 ) eq '!' ) {
	    fatal_error "Only one exclusion allowed in a host list" if $switched;
	    $switched = 1;
	    $host = substr( $host, 1 );
	    $new = \@exclusions;
	}

	unless ( $switched ) {
	    if ( $type == $zonetype ) {
		fatal_error "Duplicate Host Group ($interface:$host) in zone $zone" if $interfaces{$interface}{zone} eq $zone;
		if ( $host eq ALLIP ) {
		    fatal_error "Duplicate Host Group ($interface:$host) in zone $zone" if @newnetworks;
		    $interfaces{$interface}{zone} = $zone;
		    #
		    # Make 'find_hosts_by_option()' work correctly for this zone
		    #
		    for ( qw/blacklist maclist nosmurfs tcpflags/ ) {
			$options->{$_} = $interfaceref->{options}{$_} if $interfaceref->{options}{$_};
		    }

		    $allip = 1;
		}
	    }
	}

	if ( substr( $host, 0, 1 ) eq '+' ) {
	    fatal_error "Invalid ipset name ($host)" unless $host =~ /^\+[a-zA-Z]\w*$/;
	    require_capability( 'IPSET_MATCH', 'Ipset names in host lists', '');
	} else {
	    validate_host $host, 0;
	}

	push @$new, $host;
    }

    $zoneref->{options}{in_out}{routeback} = 1 if $options->{routeback};

    my $gtype = $type == IPSEC ? 'ipsec' : 'ip';

    $hostsref     = ( $zoneref->{hosts}           || ( $zoneref->{hosts} = {} ) );
    $typeref      = ( $hostsref->{$gtype}         || ( $hostsref->{$gtype} = {} ) );
    $interfaceref = ( $typeref->{$interface}      || ( $typeref->{$interface} = [] ) );

    fatal_error "Duplicate Host Group ($interface:" . ALLIP . ") in zone $zone" if $allip && @$interfaceref;

    $zoneref->{options}{complex} = 1 if @$interfaceref || ( @newnetworks > 1 ) || ( @exclusions ) || $options->{routeback};

    push @{$interfaceref}, { options => $options,
			     hosts   => \@newnetworks,
			     ipsec   => $type == IPSEC ? 'ipsec' : 'none' ,
			     exclusions => \@exclusions };
}

#
# Verify that the passed zone name represents a declared zone. Return a
# reference to its zone table entry.
#
sub find_zone( $ ) {
    my $zone = $_[0];

    my $zoneref = $zones{$zone};

    fatal_error "Unknown zone ($zone)" unless $zoneref;

    $zoneref;
}

sub zone_type( $ ) {
    find_zone( $_[0] )->{type};
}

sub zone_interfaces( $ ) {
    find_zone( $_[0] )->{interfaces};
}

sub defined_zone( $ ) {
    $zones{$_[0]};
}

sub all_zones() {
    @zones;
}

sub off_firewall_zones() {
   grep ( ! ( $zones{$_}{type} == FIREWALL || $zones{$_}{type} == VSERVER )  ,  @zones );
}

sub non_firewall_zones() {
   grep ( $zones{$_}{type} != FIREWALL  ,  @zones );
}

sub all_parent_zones() {
    #
    # Although the firewall zone is technically a parent zone, we let the caller decide
    # if it is to be included or not.
    #
    grep ( ! @{$zones{$_}{parents}} , off_firewall_zones );
}

sub complex_zones() {
    grep( $zones{$_}{options}{complex} , @zones );
}

sub vserver_zones() {
    grep ( $zones{$_}{type} == VSERVER, @zones );
}

sub firewall_zone() {
    $firewall_zone;
}

#
# Determine if the passed physical device is a bridge
#
sub is_a_bridge( $ ) {
    which 'brctl' && qt( "brctl show | tail -n+2 | grep -q '^$_[0]\[\[:space:\]\]'" );
}

#
# Transform the passed interface name into a legal shell variable name.
#
sub chain_base($) {
    my $chain = $_[0];
    my $name  = $basemap{$chain};
    #
    # Return existing mapping, if any
    #
    return $name if $name;
    #
    # Remember initial value
    #
    my $key = $chain;
    #
    # Handle VLANs and wildcards
    #
    $chain =~ s/\+$//;
    $chain =~ tr/./_/;

    if ( $chain eq '' || $chain =~ /^[0-9]/ || $chain =~ /[^\w]/ ) {
	#
	# Must map. Remove all illegal characters
	#
	$chain =~ s/[^\w]//g;
	#
	# Prefix with if_ if it begins with a digit
	#
	$chain = join( '' , 'if_', $chain ) if $chain =~ /^[0-9]/;
	#
	# Create a new unique name
	#
	1 while $mapbase{$name = join ( '_', $chain, ++$baseseq )};
    } else {
	#
	# We'll store the identity mapping if it is unique
	#
	$chain = join( '_', $key , ++$baseseq ) while $mapbase{$name = $chain};
    }
    #
    # Store the reverse mapping
    #
    $mapbase{$name} = $key;
    #
    # Store the mapping
    #
    $basemap{$key} = $name;
}

#
# Process a record in the interfaces file
#
sub process_interface( $$ ) {
    my ( $nextinum, $export ) = @_;
    my $netsref = '';
    my ($zone, $originalinterface, $bcasts, $options ) = split_line 2, 4, 'interfaces file';
    my $zoneref;
    my $bridge = '';

    if ( $zone eq '-' ) {
	$zone = '';
    } else {
	$zoneref = $zones{$zone};

	fatal_error "Unknown zone ($zone)" unless $zoneref;
	fatal_error "Firewall zone not allowed in ZONE column of interface record" if $zoneref->{type} == FIREWALL;
    }

    my ($interface, $port, $extra) = split /:/ , $originalinterface, 3;

    fatal_error "Invalid INTERFACE ($originalinterface)" if ! $interface || defined $extra;

    if ( defined $port && $port ne '' ) {
	fatal_error qq("Virtual" interfaces are not supported -- see http://www.shorewall.net/Shorewall_and_Aliased_Interfaces.html) if $port =~ /^\d+$/;
	require_capability( 'PHYSDEV_MATCH', 'Bridge Ports', '');
	fatal_error "Your iptables is not recent enough to support bridge ports" unless have_capability( 'KLUDGEFREE' );

	fatal_error "Invalid Interface Name ($interface:$port)" unless $port =~ /^[\w.@%-]+\+?$/;
	fatal_error "Duplicate Interface ($port)" if $interfaces{$port};

	fatal_error "$interface is not a defined bridge" unless $interfaces{$interface} && $interfaces{$interface}{options}{bridge};
	fatal_error "Bridge Ports may only be associated with 'bport' zones" if $zone && $zoneref->{type} != BPORT;

	if ( $zone ) {
	    if ( $zoneref->{bridge} ) {
		fatal_error "Bridge Port zones may only be associated with a single bridge" if $zoneref->{bridge} ne $interface;
	    } else {
		$zoneref->{bridge} = $interface;
	    }

	    fatal_error "Vserver zones may not be associated with bridge ports" if $zoneref->{type} == VSERVER;
	}

	$bridge = $interface;
	$interface = $port;
    } else {
	fatal_error "Duplicate Interface ($interface)" if $interfaces{$interface};
	fatal_error "Zones of type 'bport' may only be associated with bridge ports" if $zone && $zoneref->{type} == BPORT;
	fatal_error "Vserver zones may not be associated with interfaces" if $zone && $zoneref->{type} == VSERVER;

	$bridge = $interface;
    }

    my $wildcard = 0;
    my $root;

    if ( $interface =~ /\+$/ ) {
	$wildcard = 1;
	$root = substr( $interface, 0, -1 );
    } else {
	$root = $interface;
    }

    fatal_error "Invalid interface name ($interface)" if $interface =~ /\*/;

    my $physical = $interface;
    my $broadcasts;

    unless ( $bcasts eq '-' || $bcasts eq 'detect' ) {
	my @broadcasts = split_list $bcasts, 'address';

	for my $address ( @broadcasts ) {
	    fatal_error 'Invalid BROADCAST address' unless $address =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
	}

	if ( have_capability( 'ADDRTYPE' ) ) {
	    warning_message 'Shorewall no longer uses broadcast addresses in rule generation when Address Type Match is available';
	} else {
	    $broadcasts = \@broadcasts;
	}
    }

    my %options;

    $options{port} = 1 if $port;

    my $hostoptionsref = {};

    if ( $options eq 'ignore' ) {
	fatal_error "Ignored interfaces may not be associated with a zone" if $zone;
	$options{ignore} = 1;
	$options = '-';
    }

    if ( $options ne '-' ) {

	my %hostoptions = ( dynamic => 0 );

	for my $option (split_list1 $options, 'option' ) {
	    next if $option eq '-';

	    ( $option, my $value ) = split /=/, $option;

	    fatal_error "Invalid Interface option ($option)" unless my $type = $validinterfaceoptions{$option};

	    if ( $zone ) {
		fatal_error qq(The "$option" option may not be specified for a Vserver zone") if $zoneref->{type} == VSERVER && ! ( $type & IF_OPTION_VSERVER );
	    } else {
		fatal_error "The \"$option\" option may not be specified on a multi-zone interface" if $type & IF_OPTION_ZONEONLY;
	    }

	    my $hostopt = $type & IF_OPTION_HOST;

	    fatal_error "The \"$option\" option is not allowed on a bridge port" if $port && ! $hostopt;

	    $type &= MASK_IF_OPTION;

	    if ( $type == SIMPLE_IF_OPTION ) {
		fatal_error "Option $option does not take a value" if defined $value;
		if ( $option eq 'blacklist' ) {
		    if ( $zone ) {
			$zoneref->{options}{in}{blacklist} = 1;
		    } else {
			warning_message "The 'blacklist' option is ignored on multi-zone interfaces";
		    }
		} else {
		    $options{$option} = 1;
		    $hostoptions{$option} = 1 if $hostopt;
		}
	    } elsif ( $type == BINARY_IF_OPTION ) {
		$value = 1 unless defined $value;
		fatal_error "Option value for '$option' must be 0 or 1" unless ( $value eq '0' || $value eq '1' );
		fatal_error "The '$option' option may not be used with a wild-card interface name" if $wildcard;
		$options{$option} = $value;
		$hostoptions{$option} = $value if $hostopt;
	    } elsif ( $type == ENUM_IF_OPTION ) {
		if ( $option eq 'arp_ignore' ) {
		    fatal_error q(The 'arp_ignore' option may not be used with a wild-card interface name) if $wildcard;
		    if ( defined $value ) {
			if ( $value =~ /^[1-3,8]$/ ) {
			    $options{arp_ignore} = $value;
			} else {
			    fatal_error "Invalid value ($value) for arp_ignore";
			}
		    } else {
			$options{arp_ignore} = 1;
		    }
		} else {
		    assert( 0 );
		}
	    } elsif ( $type == NUMERIC_IF_OPTION ) {
		$value = $defaultinterfaceoptions{$option} unless defined $value;
		fatal_error "The '$option' option requires a value" unless defined $value;
		my $numval = numeric_value $value;
		fatal_error "Invalid value ($value) for option $option" unless defined $numval && $numval <= $maxoptionvalue{$option};
		$options{$option} = $numval;
		$hostoptions{$option} = $numval if $hostopt;
	    } elsif ( $type == IPLIST_IF_OPTION ) {
		fatal_error "The '$option' option requires a value" unless defined $value;
		#
		# Add all IP to the front of a list if the list begins with '!'
		#
		$value = join ',' , ALLIP , $value if $value =~ /^!/;

		if ( $option eq 'nets' ) {
		    fatal_error q("nets=" may not be specified for a multi-zone interface) unless $zone;
		    fatal_error "Duplicate $option option" if $netsref;
		    if ( $value eq 'dynamic' ) {
			require_capability( 'IPSET_MATCH', 'Dynamic nets', '');
			$hostoptions{dynamic} = 1;
			#
			# Defer remaining processing until we have the final physical interface name
			#
			$netsref = 'dynamic';
		    } else {
			$hostoptions{multicast} = 1;
			#
			# Convert into a Perl array reference
			#
			$netsref = [ split_list $value, 'address' ];
		    }
		    #
		    # Assume 'broadcast'
		    #
		    $hostoptions{broadcast} = 1;
		} else {
		    assert(0);
		}
	    } elsif ( $type == STRING_IF_OPTION ) {
		fatal_error "The '$option' option requires a value" unless defined $value;

		if ( $option eq 'physical' ) {
		    fatal_error "Invalid Physical interface name ($value)" unless $value && $value !~ /%/;

		    fatal_error "Duplicate physical interface name ($value)" if ( $physical{$value} && ! $port );

		    fatal_error "The type of 'physical' name ($value) doesn't match the type of interface name ($interface)" if $wildcard && ! $value =~ /\+$/;
		    $physical = $value;
		} else {
		    assert(0);
		}
	    } else {
		warning_message "Support for the $option interface option has been removed from Shorewall";
	    }
	}

	fatal_error "Invalid combination of interface options" if $options{required} && $options{optional};

	if ( $netsref eq 'dynamic' ) {
	    my $ipset = "${zone}_" . chain_base $physical;
	    $netsref = [ "+$ipset" ];
	    $ipsets{$ipset} = 1;
	}

	if ( $options{bridge} ) {
	    require_capability( 'PHYSDEV_MATCH', 'The "bridge" option', 's');
	    fatal_error "Bridges may not have wildcard names" if $wildcard;
	    $hostoptions{routeback} = $options{routeback} = 1;
	}

	$hostoptions{routeback} = $options{routeback} = is_a_bridge( $physical ) unless $export || $options{routeback};

	$hostoptionsref = \%hostoptions;
    } else {
	#
	# No options specified -- auto-detect bridge
	#
	$hostoptionsref->{routeback} = $options{routeback} = is_a_bridge( $physical ) unless $export;
    }

    $physical{$physical} = $interfaces{$interface} = { name       => $interface ,
						       bridge     => $bridge ,
						       nets       => 0 ,
						       number     => $nextinum ,
						       root       => $root ,
						       broadcasts => $broadcasts ,
						       options    => \%options ,
						       zone       => '',
						       physical   => $physical ,
						       base       => chain_base( $physical )
						     };

    if ( $zone ) {
	$netsref ||= [ allip ];
	add_group_to_zone( $zone, $zoneref->{type}, $interface, $netsref, $hostoptionsref );
	add_group_to_zone( $zone,
			   $zoneref->{type},
			   $interface,
			   $family == F_IPV4 ? [ IPv4_MULTICAST ] : [ IPv6_MULTICAST ] ,
			   { destonly => 1 } ) if $hostoptionsref->{multicast} && $interfaces{$interface}{zone} ne $zone;
    }

    progress_message "  Interface \"$currentline\" Validated";

    return $interface;
}

#
# Parse the interfaces file.
#
sub validate_interfaces_file( $ ) {
    my $export = shift;
    
    my @ifaces;
    my $nextinum = 1;

    if ( my $fn = open_file 'interfaces' ) {
	first_entry "$doing $fn...";
	push @ifaces, process_interface( $nextinum++, $export ) while read_a_line;
    } else {
	fatal_error q(The 'interfaces' file does not exist or has zero size);
    }

    #
    # We now assemble the @interfaces array such that bridge ports immediately precede their associated bridge
    #
    for my $interface ( @ifaces ) {
	my $interfaceref = $interfaces{$interface};

	if ( $interfaceref->{options}{bridge} ) {
	    my @ports = grep $interfaces{$_}{options}{port} && $interfaces{$_}{bridge} eq $interface, @ifaces;

	    if ( @ports ) {
		push @interfaces, @ports;
	    } else {
		$interfaceref->{options}{routeback} = 1; #so the bridge will work properly
	    }
	}

	push @interfaces, $interface unless $interfaceref->{options}{port};
    }
    #
    # Be sure that we have at least one interface
    #
    fatal_error "No network interfaces defined" unless @interfaces;

    if ( vserver_zones ) {
	#
	# While the user thinks that vservers are associated with a particular interface, they really are not.
	# We create an interface to associated them with.
	#
	my $interface = '%vserver%';

	$interfaces{$interface} = { name       => $interface ,
				    bridge     => $interface ,
				    nets       => 0 ,
				    number     => $nextinum ,
				    root       => $interface ,
				    broadcasts => undef ,
				    options    => {} ,
				    zone       => '',
				    physical   => 'lo',
				  };

	push @interfaces, $interface;
    }
}

#
# Map the passed name to the corresponding physical name in the passed interface
#
sub map_physical( $$ ) {
    my ( $name, $interfaceref ) = @_;
    my $physical = $interfaceref->{physical};

    return $physical if $name eq $interfaceref->{name};

    $physical =~ s/\+$//;

    $physical . substr( $name, length  $interfaceref->{root} );
}

#
# Returns true if passed interface matches an entry in /etc/shorewall/interfaces
#
# If the passed name matches a wildcard and 'cache' is true, an entry for the name is added in
# %interfaces.
#
sub known_interface($;$)
{
    my ( $interface, $cache ) = @_;
    my $interfaceref = $interfaces{$interface};

    return $interfaceref if $interfaceref;

    fatal_error "Invalid interface ($interface)" if $interface =~ /\*/;

    for my $i ( @interfaces ) {
	$interfaceref = $interfaces{$i};
	my $root = $interfaceref->{root};
	if ( $i ne $root && substr( $interface, 0, length $root ) eq $root ) {
	    my $physical = map_physical( $interface, $interfaceref );

	    my $copyref = { options  => $interfaceref->{options},
			    bridge   => $interfaceref->{bridge} ,
			    name     => $i ,
			    number   => $interfaceref->{number} ,
			    physical => $physical ,
			    base     => chain_base( $physical ) ,
			  };

	    $interfaces{$interface} = $copyref if $cache;

	    return $copyref;
	}
    }

    0;
}

#
# Return interface number
#
sub interface_number( $ ) {
    $interfaces{$_[0]}{number} || 256;
}

#
# Return the interfaces list
#
sub all_interfaces() {
    @interfaces;
}

#
# Return a list of bridges
#
sub all_bridges() {
    grep ( $interfaces{$_}{options}{bridge} , @interfaces );
}

#
# Return a reference to the interfaces table entry for an interface
#
sub find_interface( $ ) {
    my $interface    = $_[0];
    my $interfaceref = $interfaces{ $interface };

    fatal_error "Unknown Interface ($interface)" unless $interfaceref;

    $interfaceref;
}

#
# Returns the physical interface associated with the passed logical name
#
sub get_physical( $ ) {
    $interfaces{ $_[0] }->{physical};
}

#
# This one doesn't insist that the passed name be the name of a configured interface
#
sub physical_name( $ ) {
    my $device = shift;
    my $devref = known_interface $device;

    $devref ? $devref->{physical} : $device;
}

#
# Returns true if there are bridge port zones defined in the config
#
sub have_bridges() {
    @bport_zones > 0;
}

#
# Return the bridge associated with the passed interface. If the interface is not a bridge port,
# return ''
#
sub port_to_bridge( $ ) {
    my $portref = $interfaces{$_[0]};
    return $portref && $portref->{options}{port} ? $portref->{bridge} : '';
}

#
# Return the bridge associated with the passed interface.
#
sub source_port_to_bridge( $ ) {
    my $portref = $interfaces{$_[0]};
    return $portref ? $portref->{bridge} : '';
}

#
# Return the 'optional' setting of the passed interface
#
sub interface_is_optional($) {
    my $optionsref = $interfaces{$_[0]}{options};
    $optionsref && $optionsref->{optional};
}

#
# Returns reference to array of interfaces with the passed option
#
sub find_interfaces_by_option( $ ) {
    my $option = $_[0];
    my @ints = ();

    for my $interface ( @interfaces ) {
	my $interfaceref = $interfaces{$interface};

	next unless $interfaceref->{root};

	my $optionsref = $interfaceref->{options};
	if ( $optionsref && defined $optionsref->{$option} ) {
	    push @ints , $interface
	}
    }

    \@ints;
}

#
# Returns reference to array of interfaces with the passed option. Unlike the preceding function, this one:
#
# - All entries in %interfaces are searched.
# - Returns a two-element list; the second element indicates whether any members of the list have wildcard physical names
#
sub find_interfaces_by_option1( $ ) {
    my $option = $_[0];
    my @ints = ();
    my $wild = 0;

    for my $interface ( sort { $interfaces{$a}->{number} <=> $interfaces{$b}->{number} }
			keys %interfaces ) {
	my $interfaceref = $interfaces{$interface};

	next unless defined $interfaceref->{physical};

	my $optionsref = $interfaceref->{options};

	if ( $optionsref && defined $optionsref->{$option} ) {
	    $wild ||= ( $interfaceref->{physical} =~ /\+$/ );
	    push @ints , $interface
	}
    }

    return unless defined wantarray;

    wantarray ? ( \@ints, $wild ) : \@ints;
}

#
# Return the value of an option for an interface
#
sub get_interface_option( $$ ) {
    my ( $interface, $option ) = @_;

    $interfaces{$interface}{options}{$option};
}

#
# Set an option for an interface
#
sub set_interface_option( $$$ ) {
    my ( $interface, $option, $value ) = @_;

    $interfaces{$interface}{options}{$option} = $value;
}

#
# Verify that all required interfaces are available after waiting for any that specify the 'wait' option.
#
sub verify_required_interfaces( $ ) {

    my $generate_case = shift;

    my $returnvalue = 0;

    my $interfaces = find_interfaces_by_option 'wait';

    if ( @$interfaces ) {
	my $first = 1;

	emit( "local waittime\n" );

	emit( 'case "$COMMAND" in' );

	push_indent;

	emit( 'start|restart|restore)' );

	push_indent;

	for my $interface (@$interfaces ) {
	    my $wait = $interfaces{$interface}{options}{wait};

	    emit q() unless $first-- > 0;

	    if ( $wait ) {
		my $physical = get_physical $interface;

		if ( $physical =~ /\+$/ ) {
		    my $base = uc chain_base $physical;

		    $physical =~ s/\+$/*/;

		    emit( 'for interface in $(find_all_interfaces); do',
			  '    case $interface in',
			  "        $physical)",
			  "            waittime=$wait",
			  '            while [ $waittime -gt 0 ]; do',
			  '                interface_is_usable $interface && break',
			  '                waittime=$(($waittime - 1))',
			  '            done',
			  '            ;;',
			  '    esac',
			  'done',
			  '',
			);
		} else {
		    emit qq(if ! interface_is_usable $physical; then);
		    emit qq(    waittime=$wait);
		    emit  '';
		    emit  q(    while [ $waittime -gt 0 ]; do);
		    emit qq(        interface_is_usable $physical && break);
		    emit  q(        sleep 1);
		    emit   '        waittime=$(($waittime - 1))';
		    emit  q(    done);
		    emit  q(fi);
		}

		$returnvalue = 1;
	    }
	}

	emit( ";;\n" );

	pop_indent;
	pop_indent;

	emit( "esac\n" );

    }

    $interfaces = find_interfaces_by_option 'required';

    if ( @$interfaces ) {

	if ( $generate_case ) {
	    emit( 'case "$COMMAND" in' );
	    push_indent;
	    emit( 'start|restart|restore|refresh)' );
	    push_indent;
	}

	for my $interface (@$interfaces ) {
	    my $physical = get_physical $interface;

	    if ( $physical =~ /\+$/ ) {
		my $base = uc chain_base $physical;

		$physical =~ s/\+$/*/;

		emit( "SW_${base}_IS_UP=\n",
		      'for interface in $(find_all_interfaces); do',
		      '    case $interface in',
		      "        $physical)",
		      "            interface_is_usable \$interface && SW_${base}_IS_UP=Yes && break",
		      '            ;;',
		      '    esac',
		      'done',
		      '',
		      "if [ -z \"\$SW_${base}_IS_UP\" ]; then",
		      "    startup_error \"None of the required interfaces $physical are available\"",
		      "fi\n"
		    );
	    } else {
		emit qq(if ! interface_is_usable $physical; then);
		emit qq(    startup_error "Required interface $physical not available");
		emit qq(fi\n);
	    }
	}

	if ( $generate_case ) {
	    emit( ';;' );
	    pop_indent;
	    pop_indent;
	    emit( 'esac' );
	}

	$returnvalue = 1;
    }

    $returnvalue;
}

#
# Emit the updown() function
#
sub compile_updown() {
    emit( '',
	  '#',
	  '# Handle the "up" and "down" commands',
	  '#',
	  'updown() # $1 = interface',
	  '{',
	);

    push_indent;

    emit( 'local state',
	  'state=cleared',
	  '' );

    emit 'progress_message3 "$g_product $COMMAND triggered by $1"';
    emit '';

    if ( $family == F_IPV4 ) {
	emit 'if shorewall_is_started; then';
    } else {
	emit 'if shorewall6_is_started; then';
    }

    emit( '    state=started',
	  'elif [ -f ${VARDIR}/state ]; then',
	  '    case "$(cat ${VARDIR}/state)" in',
	  '        Stopped*)',
	  '            state=stopped',
	  '            ;;',
	  '        Cleared*)',
	  '            ;;',
	  '        *)',
	  '            state=unknown',
	  '            ;;',
	  '    esac',
	  'else',
	  '    state=unknown',
	  'fi',
	  ''
	);

    emit( 'case $1 in' );

    push_indent;

    my $ignore   = find_interfaces_by_option 'ignore';
    my $required = find_interfaces_by_option 'required';
    my $optional = find_interfaces_by_option 'optional';

    if ( @$ignore ) {
	my $interfaces = join '|', map $interfaces{$_}->{physical}, @$ignore;

	$interfaces =~ s/\+/*/;

	emit( "$interfaces)",
	      '    progress_message3 "$COMMAND on interface $1 ignored"',
	      '    exit 0',
	      '    ;;'
	    );
    }

    if ( @$required ) {
	my $interfaces = join '|', map $interfaces{$_}->{physical}, @$required;

	my $wildcard = ( $interfaces =~ s/\+/*/ );

	emit( "$interfaces)",
	      '    if [ "$COMMAND" = up ]; then' );

	if ( $wildcard ) {
	    emit( '        if [ "$state" = started ]; then',
		  '            COMMAND=restart',
		  '        else',
		  '            COMMAND=start',
		  '        fi' );
	} else {
	    emit( '        COMMAND=start' );
	}

	emit( '        progress_message3 "$g_product attempting $COMMAND"',
	      '        detect_configuration',
	      '        define_firewall' );

	if ( $wildcard ) {
	    emit( '    elif [ "$state" = started ]; then',
		  '        progress_message3 "$g_product attempting restart"',
		  '        COMMAND=restart',
		  '        detect_configuration',
		  '        define_firewall' );
	} else {
	    emit( '    else',
		  '        COMMAND=stop',
		  '        progress_message3 "$g_product attempting stop"',
		  '        detect_configuration',
		  '        stop_firewall' );
	}

	emit( '    fi',
	      '    ;;'
	    );
    }

    if ( @$optional ) {
	my $interfaces = join '|', map $interfaces{$_}->{physical}, @$optional;

	$interfaces =~ s/\+/*/;

	emit( "$interfaces)",
	      '    if [ "$COMMAND" = up ]; then',
	      '        echo 0 > ${VARDIR}/${1}.state',
	      '    else',
	      '        echo 1 > ${VARDIR}/${1}.state',
	      '    fi',
	      '',
	      '    if [ "$state" = started ]; then',
	      '        COMMAND=restart',
	      '        progress_message3 "$g_product attempting restart"',
	      '        detect_configuration',
	      '        define_firewall',
	      '    elif [ "$state" = stopped ]; then',
	      '        COMMAND=start',
	      '        progress_message3 "$g_product attempting start"',
	      '        detect_configuration',
	      '        define_firewall',
	      '    else',
	      '        progress_message3 "$COMMAND on interface $1 ignored"',
	      '    fi',
	      '    ;;',
	    );
    }

    emit( "*)",
	  '    case $state in',
	  '        started)',
	  '            COMMAND=restart',
	  '            progress_message3 "$g_product attempting restart"',
	  '            detect_configuration',
	  '            define_firewall',
	  '            ;;',
	  '        *)',
	  '            progress_message3 "$COMMAND on interface $1 ignored"',
	  '            ;;',
	  '    esac',
	);

    pop_indent;

    emit( 'esac' );

    pop_indent;

    emit( '}',
	  '',
	);
}

#
# Process a record in the hosts file
#
sub process_host( ) {
    my $ipsec = 0;
    my ($zone, $hosts, $options ) = split_line 2, 3, 'hosts file';

    my $zoneref = $zones{$zone};
    my $type    = $zoneref->{type};

    fatal_error "Unknown ZONE ($zone)" unless $type;
    fatal_error 'Firewall zone not allowed in ZONE column of hosts record' if $type == FIREWALL;

    my $interface;

    if ( $family == F_IPV4 ) {
	if ( $hosts =~ /^([\w.@%-]+\+?):(.*)$/ ) {
	    $interface = $1;
	    $hosts = $2;

	    if ( $hosts =~ /^\+/ ) {
		$zoneref->{options}{complex} = 1;
		fatal_error "ipset name qualification is disallowed in this file" if $hosts =~ /[\[\]]/;
		fatal_error "Invalid ipset name ($hosts)" unless $hosts =~ /^\+[a-zA-Z][-\w]*$/;
	    }

	    fatal_error "Unknown interface ($interface)" unless $interfaces{$interface}{root};
	} else {
	    fatal_error "Invalid HOST(S) column contents: $hosts";
	}
    } elsif ( $hosts =~ /^([\w.@%-]+\+?):<(.*)>\s*$/ || $hosts =~ /^([\w.@%-]+\+?):\[(.*)\]\s*$/   ) {
	$interface = $1;
	$hosts = $2;
	$zoneref->{options}{complex} = 1 if $hosts =~ /^\+/;
	fatal_error "Unknown interface ($interface)" unless $interfaces{$interface}{root};
    } else {
	fatal_error "Invalid HOST(S) column contents: $hosts";
    }

    if ( $type == BPORT ) {
	if ( $zoneref->{bridge} eq '' ) {
	    fatal_error 'Bridge Port Zones may only be associated with bridge ports' unless $interfaces{$interface}{options}{port};
	    $zoneref->{bridge} = $interfaces{$interface}{bridge};
	} elsif ( $zoneref->{bridge} ne $interfaces{$interface}{bridge} ) {
	    fatal_error "Interface $interface is not a port on bridge $zoneref->{bridge}";
	}
    }

    my $optionsref = { dynamic => 0 };

    if ( $options ne '-' ) {
	my @options = split_list $options, 'option';
	my %options = ( dynamic => 0 );

	for my $option ( @options ) {
	    if ( $option eq 'ipsec' ) {
		require_capability 'POLICY_MATCH' , q(The 'ipsec' option), 's';
		$type = IPSEC;
		$zoneref->{options}{complex} = 1;
		$ipsec = 1;
	    } elsif ( $option eq 'norfc1918' ) {
		warning_message "The 'norfc1918' host option is no longer supported"
	    } elsif ( $option eq 'blacklist' ) {
		$zoneref->{options}{in}{blacklist} = 1;
	    } elsif ( $validhostoptions{$option}) {
		fatal_error qq(The "$option" option is not allowed with Vserver zones) if $type == VSERVER && ! ( $validhostoptions{$option} & IF_OPTION_VSERVER );
		$options{$option} = 1;
	    } else {
		fatal_error "Invalid option ($option)";
	    }
	}

	fatal_error q(A host entry for a Vserver zone may not specify the 'ipsec' option) if $ipsec && $zoneref->{type} == VSERVER;

	$optionsref = \%options;
    }

    #
    # Looking for the '!' at the beginning of a list element is more straight-foward than looking for it in the middle.
    #
    # Be sure we don't have a ',!' in the original
    #
    fatal_error "Invalid hosts list" if $hosts =~ /,!/;
    #
    # Now add a comma before '!'. Do it globally - add_group_to_zone() correctly checks for multiple exclusions
    #
    $hosts =~ s/!/,!/g;
    #
    # Take care of case where the hosts list begins with '!'
    #
    $hosts = join( '', ALLIP , $hosts ) if substr($hosts, 0, 2 ) eq ',!';

    if ( $hosts eq 'dynamic' ) {
	fatal_error "Vserver zones may not be dynamic" if $type == VSERVER;
	require_capability( 'IPSET_MATCH', 'Dynamic nets', '');
	my $physical = physical_name $interface;
	$hosts = "+${zone}_${physical}";
	$optionsref->{dynamic} = 1;
	$ipsets{"${zone}_${physical}"} = 1;

    }
    #
    # We ignore the user's notion of what interface vserver addresses are on and simply invent one for all of the vservers.
    #
    $interface = '%vserver%' if $type == VSERVER;

    add_group_to_zone( $zone, $type , $interface, [ split_list( $hosts, 'host' ) ] , $optionsref);

    progress_message "   Host \"$currentline\" validated";

    return $ipsec;
}

#
# Validates the hosts file. Generates entries in %zone{..}{hosts}
#
sub validate_hosts_file()
{
    my $ipsec = 0;

    if ( my $fn = open_file 'hosts' ) {
	first_entry "$doing $fn...";
	$ipsec |= process_host while read_a_line;
    }

    $have_ipsec = $ipsec || haveipseczones;

}

#
# Return an indication of whether IPSEC is present
#
sub have_ipsec() {
    return defined $have_ipsec ? $have_ipsec : have_capability 'POLICY_MATCH';
}

#
# Returns a reference to a array of host entries. Each entry is a
# reference to an array containing ( interface , polciy match type {ipsec|none} , network , exclusions );
#
sub find_hosts_by_option( $ ) {
    my $option = $_[0];
    my @hosts;

    for my $zone ( grep $zones{$_}{type} != FIREWALL , @zones ) {
	while ( my ($type, $interfaceref) = each %{$zones{$zone}{hosts}} ) {
	    while ( my ( $interface, $arrayref) = ( each %{$interfaceref} ) ) {
		for my $host ( @{$arrayref} ) {
		    if ( $host->{options}{$option} ) {
			for my $net ( @{$host->{hosts}} ) {
			    push @hosts, [ $interface, $host->{ipsec} , $net , $host->{exclusions}];
			}
		    }
		}
	    }
	}
    }

    for my $interface ( @interfaces ) {
	if ( ! $interfaces{$interface}{zone} && $interfaces{$interface}{options}{$option} ) {
	    push @hosts, [ $interface, 'none', ALLIP , [] ];
	}
    }

    \@hosts;
}

#
# Returns a reference to a list of zones with the passed in/out option
#

sub find_zones_by_option( $$ ) {
    my ($option, $in_out ) = @_;
    my @zns;

    for my $zone ( @zones ) {
	push @zns, $zone if $zones{$zone}{options}{$in_out}{$option};
    }

    \@zns;
}

sub all_ipsets() {
    sort keys %ipsets;
}

1;
