#!/usr/bin/perl -w
##########################################################################
# Copyright 2005 VMware, Inc.  All rights reserved. -- VMware Confidential
##########################################################################

#
# esxcfg-firewall--
#
# This script loads the firewall configuration
#

use lib "/usr/lib/vmware/esx-perl/perl5/site_perl/5.8.0";
use File::Basename;
use File::Find;
use Getopt::Long;
use POSIX;
use VMware::Log qw(:log :manip);
use VMware::Init qw(:init);
use VMware::System qw(LogCommand);
use VMware::Panic qw(Panic RegisterPanicCallback);
use VMware::FileSys::StandardLocator qw(StdFile);
use VMware::Config::VmacoreConfigObj;
use XML::DOM;

my $cfgDB;
my $cfgFile;

$0 =~ m/.*\/(.*)/;
my $shortAppName = $1;
my $logFile="/var/log/vmware/$shortAppName.log";
my $lockFile="/var/lock/subsys/firewall";
my $errLogFile="/var/log/vmware/$shortAppName-err.log";
my $serviceCfgDir="/etc/vmware/firewall/";

###
### Don't change these without fixing hostd too!
###
### Map errors to exit values so that hostd can map exit values
### to an internationalized string.  Yay.
my %errCodes = (
   'Unknown_error'              => -12,
   'Unknown_service'            => -13,
   'Cannot_close_unopened_port' => -14,
   'Invalid_argument'           => -15,
   'Failed_to_commit_cfgfile'   => -16,
   'Failed_to_load_cfgfile'     => -17,
   'Failed_to_lock_cfgfile'     => -18,
   'Iptables_update_failed'     => -19,
   'Port_out_of_range'          => -20,
   'Port_direction_invalid'     => -21,
   'Port_protocol_invalid'      => -22,
   'Port_not_open'              => -23,
   'User_not_root'              => -24,
   'Iprule_port_out_of_range'   => -25,
   'Iprule_protocol_invalid'    => -26,
   'Iprule_direction_invalid'   => -27,
   'Iprule_target_invalid'      => -28,
   'Iprule_not_added'           => -29,
   'Iprule_ip_out_of_range'     => -30,
   'Failed_to_create_lockfile'  => -31,
   'Failed_to_remove_lockfile'  => -32,
);

###
### Read in service configuration data
###

my %services;

# Extract the text contents from an element or one of its subelements
sub getText {
   my ($nElt, $tag) = @_;
   my ($nTxt, $result);
   if ($tag) {
      $nElt = $nElt->getElementsByTagName($tag, 0)->item(0);
   }
   $nTxt = $nElt->getChildNodes()->item(0);
   if ($nTxt) {
      $result = $nTxt->toString();
   }
   return $result;
}

# Parse the port specification for the given rule
sub parsePorts {
   my $nRule = shift;
   my (@src, @dst, @opts);
   foreach my $nPort ($nRule->getElementsByTagName("port", 0)) {
      my $port = getText($nPort);
      if ($port !~ /\d+/) {
         my $begin = getText($nPort, "begin");
         my $end = getText($nPort, "end");
         $port = "$begin:$end";
      }
      my $type = $nPort->getAttribute("type");
      if ($type eq "src") {
         push @src, $port;
      } elsif ($type eq "dst") {
         push @dst, $port;
      } else {
         next;
      }
   }
   if (@src) {
      my $port = join(',', @src);
      push @opts, "--sport $port";
   }
   if (@dst) {
      my $port = join(',', @dst);
      push @opts, "--dport $port";
   }
   return @opts;
}

# Parse the given rule
sub parseRule {
   my $nRule = shift;
   my @opts;

   my $protocol = getText($nRule, "protocol");
   push @opts, "-p $protocol";
   push @opts, parsePorts($nRule);
   foreach my $nFlags ($nRule->getElementsByTagName("flags", 0)) {
      push @opts, getText($nFlags);
   }
   push @opts, "-j ACCEPT";

   my $chain = getText($nRule, "direction");
   $chain =~ s/inbound/INPUT/;
   $chain =~ s/outbound/OUTPUT/;

   return { chain => $chain, rule => join(" ", @opts) };
}

# Parse the given chains xml file
sub parseChains {
   my $file = shift;
   my %chains = ();
   my $parser = new XML::DOM::Parser;
   my $doc = $parser->parsefile($file);
   my $nCfgRoot = $doc->getElementsByTagName("ConfigRoot")->item(0);
   foreach my $nChain ($nCfgRoot->getElementsByTagName("chain", 0)) {
      my $chain = $nChain->getAttribute("name");
      foreach my $nRule ($nChain->getElementsByTagName("rule", 0)) {
         push @{$chains{$chain}}, getText($nRule);
      }
   }
   $doc->dispose();
   return %chains;
}

my $parser = new XML::DOM::Parser;

# Parse all XML files in the service config directory
if (opendir(DIR, $serviceCfgDir)) {
   my $file;
   while (defined($file = readdir(DIR))) {
      if ($file =~ /xml$/) {
         my $doc = $parser->parsefile($serviceCfgDir . $file);
         my $nCfgRoot = $doc->getElementsByTagName("ConfigRoot")->item(0);
         foreach my $nService ($nCfgRoot->getElementsByTagName("service", 0)) {
            my (@rules, @modules);
            foreach my $nRule ($nService->getElementsByTagName("rule", 0)) {
               push @rules, parseRule($nRule);
            }
            foreach my $nModule ($nService->getElementsByTagName("module", 0)) {
               push @modules, getText($nModule);
            }
            $id = getText($nService, "id");
            $services{$id} = {'rules'=>[@rules], 'modules'=>[@modules]};
         }
         $doc->dispose();
      }
   }
   closedir(DIR);
}

#
# Custom helper chains that we use for doing more complicated filtering
#
my %customChains = parseChains($serviceCfgDir . "chains/custom.xml");

#
# default input & output rules.
#
my %defaultChains = parseChains($serviceCfgDir . "chains/default.xml");

########################################################################
#
# FWAllowEverything --
#
#       Security alerts are fun.  Lets everything in.
#
# Results:
#
# Side Effects:
#
########################################################################

sub FWAllowEverything()
{
   IpTables('-P INPUT ACCEPT');
   IpTables('-P FORWARD ACCEPT');
   IpTables('-P OUTPUT ACCEPT');
}


########################################################################
#
# FWDefaultPolicy --
# 
#       Initializes policies for the default chains.
#
# Results:
#
# Side Effects:
#
########################################################################

sub FWDefaultPolicy()
{
   if (!FWBlockIncoming()) {
      IpTables('-P INPUT ACCEPT');
   } else { 
      # Default input policy is to drop.
      IpTables('-P INPUT DROP');
   }

   # We don't support forwarding. Ever.
   IpTables('-P FORWARD DROP');

   # Default output policy depends on security level
   if (FWBlockOutgoing()) {
      # We really want REJECT here, but that
      # target can't be used for default policies.
      # Instead we ensure that the last rule in the
      # OUTPUT chain is -j REJECT (see FWFinalRules).
      IpTables('-P OUTPUT DROP');
   } else {
      IpTables('-P OUTPUT ACCEPT');
   }
}


########################################################################
#
# FWCreateCustomChains --
#
#       Creates the custom helper chains.  Note
#       the 'sort' below is a big hack.
#
# Results:
#
# Side Effects:
#
########################################################################

sub FWCreateCustomChains()
{
   LogDebug("Creating custom chains");
   foreach my $chain (sort keys %customChains) {
      LogDebug("Creating chain '$chain'");
      IpTables("-N $chain");
      foreach (@{$customChains{$chain}}) {
         IpTables("-A $chain $_");
      }
   }
}


########################################################################
#
# FWInitializeDefaultChains --
#
#       Adds the default rules to to the default chains.
#
# Results:
#
# Side Effects:
#
########################################################################

sub FWInitializeDefaultChains()
{
   LogDebug("Loading rules for default chains");
   foreach my $chain (keys %defaultChains) {
      next if ($chain eq 'INPUT' && !FWBlockIncoming());
      next if ($chain eq 'OUTPUT' && !FWBlockOutgoing());
      foreach (@{$defaultChains{$chain}}) {
         IpTables("-A $chain $_");
      }
   }
}


########################################################################
#
# FWReset --
#
#       Resets the firewall.
#
# Results:
#
# Side Effects:
#
########################################################################

sub FWReset()
{
   LogDebug("Flushing rules & deleting chains");
   # flush all rules
   IpTables('-F');
 
   # delete user defined chains
   IpTables('-X'); 

   # unload netfilter modules
   FWUnloadModules();
}


########################################################################
#
# FWListServices --
#
#       Lists the known, blessed services
#
# Results:
#
# Side Effects:
#
########################################################################

sub FWListServices()
{
   print("Known services: ");
   foreach (sort {lc $a cmp lc $b} keys %services) {
      print("$_ ");
   }
   print("\n");
}

########################################################################
#
# FWServiceEnabled --
#
#       Checks config file/blessed service list.
#
# Results:
#       1 if service is enabled, 0 otherwise.
#
# Side Effects:
#
########################################################################

sub FWServiceEnabled
{
   my $cfgOpt = shift;
   my %tmpServices = ('/firewall/services/sshServer' => 1, 
                      '/firewall/services/vpxHeartbeats' => 1,  
                      '/firewall/services/CIMSLP' => 1,  
                      '/firewall/services/CIMHttpServer' => 0,  
                      '/firewall/services/CIMHttpsServer' => 1,  
                      '/firewall/services/AAMClient' => 1,  
                      '/firewall/services/LicenseClient' => 1,
                      '/firewall/services/VCB' => 1);

   if ($cfgDB->Get($cfgOpt, -1) == -1) {
      # Option not specified in config file, so trust tmpServices
      return $tmpServices{$cfgOpt};
   }

   return $cfgDB->GetBoolean($cfgOpt, 0);
}


########################################################################
#
# FWAddServices --
#
#       Opens the necessary ports and loads modules for each of the
#       services listed in the config file.
#
# Results:
#
# Side Effects:
#
########################################################################

sub FWAddServices()
{
   LogDebug("Adding blessed services");
   my @nf_modules = GetNetfilterModules();
   foreach my $srv (keys %services) {
      my $cfgOpt = '/firewall/services/' . $srv;
      if (FWServiceEnabled($cfgOpt)) {
         LogDebug("Enabling service $srv");
         LoadNetfilterModules(@{$services{$srv}{'modules'}});
         foreach my $rule (@{$services{$srv}{'rules'}}) {
            next if ($$rule{chain} eq 'INPUT' && !FWBlockIncoming());
            next if ($$rule{chain} eq 'OUTPUT' && !FWBlockOutgoing());
            IpTables("-A " . $$rule{chain} . " " . $$rule{rule});
         }
      }
   }
}


########################################################################
#
# FWLoadCustomModules --
#
#       Loads additional netfilter modules specified in the config file.
#
# Results:
#
# Side Effects:
#
#       Intentionally skips modules that are not netfilter modules.
#
########################################################################

sub FWLoadCustomModules()
{
   my @modules = $cfgDB->GetArray('/firewall/modules', []);
   LoadNetfilterModules(@modules);
}


########################################################################
#
# FWUnloadModules --
#
#       Unloads all netfilter modules.
#
# Results:
#
#       All netfilter modules removed from running kernel, or warn on
#       error.
#
# Side Effects:
#
########################################################################

sub FWUnloadModules()
{
   LogDebug("Removing netfilter modules");

   my @nf_modules = GetNetfilterModules();
   my %loaded_modules = GetLoadedModules();
   my @modules_to_unload;

   # Recursive sub-routine to get an ordered list of a module plus its
   # dependencies.  Dependencies come before the modules that depend on them.
   sub get_module_stack {
      my $module = shift;
      my %loaded_modules = %{shift()};
      my @module_stack;
      if (!exists $loaded_modules{$module}) {
         return ();
      }
      foreach my $dep (@{$loaded_modules{$module}}) {
         push @module_stack, get_module_stack($dep, \%loaded_modules);
      }
      if (!grep($_ eq $module, @module_stack)) {
         push @module_stack, $module;
      }
      return @module_stack;
   };

   # Build the list of modules to remove, with dependencies first, and the
   # modules that require them last.  Filter out things that are already in
   # the list, so we don't ask rmmod to remove them twice.
   foreach my $nf_module (@nf_modules) {
      if (!exists $loaded_modules{$nf_module}) {
         next;
      }
      foreach my $module (get_module_stack($nf_module, \%loaded_modules)) {
         if (!grep($_ eq $module, @modules_to_unload)) {
            push(@modules_to_unload, $module);
         }
      }
   }

   # Do the actual rmmod'ing.
   my $cmd = '/sbin/rmmod ' . join(' ', @modules_to_unload);
   LogCommand($cmd) || LogWarn("Command '$cmd' failed");
}


########################################################################
#
# FWAddCustomPorts --
#
#       Opens the additionaly ports specified in the config file.
#
# Results:
#
# Side Effects:
#
########################################################################

sub FWAddCustomPorts()
{
   my $ports = LoadOpenPorts();

   foreach my $portInfo (map { $ports->{$_}; } (keys %{$ports})) {
      next if (!defined $portInfo->{port});
      foreach my $property (keys %{$portInfo}) {
         if ($property =~ m/^(tcp|udp)\.(in|out)$/) {
            my $protocol = $1;
            my $chain = ($2 =~ m/in/) ? "INPUT" : "OUTPUT";
            IpTables("-A $chain -p $protocol --dport $portInfo->{port} -j ACCEPT");
         }
      }
   }
}

########################################################################
#
# FWAddCustomIprules --
#
#       Turn on the custom Iprules specified in the config file.
#
# Results:
#
# Side Effects:
#
########################################################################

sub FWAddCustomIprules()
{
   my $hosts = LoadIprules();

   foreach my $hostInfo (map { $hosts->{$_}; } (keys %{$hosts})) {
      next if (!defined $hostInfo->{host});
      my $hostOption = $hostInfo->{host} ? "-s $hostInfo->{host}" : "";
      my $portOption = $hostInfo->{cport} ? "--destination-port $hostInfo->{cport}" : "";
      my $targetOption = $hostInfo->{target} ? "-j $hostInfo->{target}" : "";

      foreach my $property (keys %{$hostInfo}) {
         if ($property =~ m/^(tcp|udp)$/) {
            my $protocol = $property;
            my $chain = "INPUT" ;
            IpTables("-I $chain -p $protocol $hostOption $portOption $targetOption");
         }
      }
   }
}


########################################################################
#
# FWFinalRules --
#
#       Hack to make outogoing connections rejected.
#       (See comment in FWDefaultPolicy).
#
# Results:
#
# Side Effects:
#
########################################################################
sub FWFinalRules()
{
   if (FWBlockOutgoing()) {
      if ($logDrops) {
         LogWarn("Logging all drops.");
         IpTables("-A OUTPUT -j log-and-drop");
         IpTables("-A INPUT -j log-and-drop");
      } else {
         IpTables("-A OUTPUT -j REJECT");
      }
   }
}


########################################################################
#
# FWLoad --
#
#       Loads the firewall.
#
# Results:
#
# Side Effects:
#       Creates lock file.
#
########################################################################

sub FWLoad()
{
   if (!open(LOCK_FILE, ">", $lockFile)) {
      Panic("Unable to create lock file.", $errCodes{Failed_to_create_lockfile});
   } else {
      close LOCK_FILE;
   }
   FWReset();
   FWDefaultPolicy();
   if (FWBlockIncoming() || FWBlockOutgoing()) { 
      FWCreateCustomChains();
      FWInitializeDefaultChains();
      FWAddServices();
      FWLoadCustomModules();
      FWAddCustomPorts();
      FWAddCustomIprules();
   }
   FWFinalRules();
}

########################################################################
#
# FWConditionalLoad --
#
#       Loads the firewall if lockfile exists.
#
# Results:
#
# Side Effects:
#
########################################################################

sub FWConditionalLoad()
{
   if (-e $lockFile) {
      FWLoad();
   }
}

########################################################################
#
# FWUnload --
#
#       Nukes all rules & chains.  Resets default policy to
#       "please own me". 
#
# Results:
#
# Side Effects:
#       Removes lock file.
#
########################################################################

sub FWUnload()
{
   FWReset();
   FWAllowEverything();
   if (-e $lockFile && !unlink($lockFile)) {
      Panic("Unable to remove lock file.", $errCodes{Failed_to_remove_lockfile});
   }
}

########################################################################
#
# FWQuery --
#
#       Returns info about current settings.
#
# Results:
#
# Side Effects:
#
########################################################################

sub FWQuery()
{
   my $cmd = shift;
   my $arg = shift;
   if ($arg =~ /incoming/i) {
      my $blocked = FWBlockIncoming();
      print("Incoming ports ");
      print($blocked ? "blocked " : "not blocked ");
      print("by default.\n");
      exit($blocked ? 1 : 0);
   } elsif ($arg =~ /outgoing/i) {
      my $blocked = FWBlockOutgoing();
      print("Outgoing ports ");
      print($blocked ? "blocked " : "not blocked ");
      print("by default.\n");
      exit($blocked ? 1 : 0);
   } elsif ($arg) {
      if (!$services{$arg}) {
         LogError("Unknown service '$arg'\n");
         exit $errCodes{Unknown_service};
      }
      my $enabled = FWServiceEnabled('/firewall/services/' . $arg);
      print("Service " . $arg . " is ");
      print($enabled ? "enabled.\n" : "blocked.\n");
      exit($enabled ? 0 : 1);
   }

   system('/sbin/iptables -nvL');

   print("\n\n");
   if (FWBlockIncoming()) {
      print("Incoming ");
      if (FWBlockOutgoing()) {
         print("and outgoing ");
      }
      print("ports blocked by default.\n");
   } elsif (FWBlockOutgoing()) {
      print("Outgoing ports blocked by default.\n");
   } else { 
      print("Neither incoming nor outgoing blocked by default\n");
   }

   print("Enabled services: ");
   foreach my $key (keys %services) {
      print("$key ") if (FWServiceEnabled('/firewall/services/' . $key));
   }
   print("\n\n");

   print "Opened ports: \n";
   my $ports = LoadOpenPorts();
   foreach my $portInfo (map { $ports->{$_}; } (keys %{$ports})) {
      printf("\t%-20s: port %s ", $portInfo->{name}, $portInfo->{port});
      foreach my $property (sort keys %{$portInfo}) {
         next if ($property =~ "name|port");
         print "$property ";
      }
      print "\n";
   }

   print "Added Iprules: \n";
   my $hosts = LoadIprules();
   foreach my $hostInfo (map { $hosts->{$_}; } (keys %{$hosts})) {
      printf("\t%-20s: host %s cport %s %s ", $hostInfo->{name},$hostInfo->{host},
	      $hostInfo->{cport},$hostInfo->{target});
      foreach my $property (sort keys %{$hostInfo}) {
         next if ($property =~ "name|host|cport|target");
         print "$property ";
      }
      print "\n";
   }
   print "\n"
}


########################################################################
#
# IpTables --
#
#       Runs the specified iptables command.  Panics on failure.
#
# Results:
#       Panics on failure.
#
# Side Effects:
#
########################################################################

sub IpTables($)
{
   my $cmd = "/sbin/iptables " . shift;

   # check if ip_tables can be insmoded
   if(system('/sbin/insmod -np ip_tables > /dev/null 2>&1')) {
       # ip_table module is not available. ( during anaconda installation, not available)
       LogWarn("'iptables $cmd'' skipped. ip_tables module is not available.");
   } else {
       LogCommand($cmd) || Panic("Command '$cmd' failed", $errCodes{Iptables_update_failed});
   }
}


########################################################################
#
# GetNetfilterModules --
#
#       Gets a list of all netfilter modules supported by Linux kernel.
#
# Results:
#
#       Returns an array of netfilter modules.
#
# Side Effects:
#
########################################################################

sub GetNetfilterModules()
{
   my @nf_modules;
   # Use of anonymous sub here to prevent Perl warnings about
   # shared variables (namely @nf_modules) becoming unshared.
   my $is_nf_module = sub {
      if ($File::Find::dir =~ m!/netfilter/?!) {
         my $module = (/(.*)\..*/)[0];
         if (!grep($_ eq $module, @nf_modules)) {
            push @nf_modules, $module;
         }
      }
   };
   my $kernel_version = (POSIX::uname())[2];
   File::Find::find($is_nf_module, ("/lib/modules/$kernel_version"));
   return @nf_modules;
}


########################################################################
#
# GetLoadedModules --
#
#       Gets a list of all modules currently loaded in the Linux kernel.
#
# Results:
#
#       Returns a hash of loaded modules.  Each hash value is a
#       reference to an array of module names that depend on the
#       module name given by the hash key.
#
# Side Effects:
#
########################################################################

sub GetLoadedModules()
{
   my %loaded_modules;
   if (!open(MODULES_FILE, '/proc/modules')) {
      LogWarn('Could not open /proc/modules');
      return %loaded_modules;
   }
   foreach my $line (<MODULES_FILE>) {
      my ($module, $referers) = (split(/\s+/, $line, 4))[0,3];
      my @deps;
      if ($referers =~ /\[.*\]/) {
         @deps = (split(/\s+/, ($referers =~ /\[(.*)\]/)[0]));
      }
      $loaded_modules{$module} = [@deps];
   }
   close MODULES_FILE;
   return %loaded_modules;
}


########################################################################
#
# LoadNetfilterModules --
#
#       Loads the specified netfilter modules.  Warns on attempt to
#       load a module that is not an iptables module.
#
# Results:
#
# Side Effects:
#
########################################################################

sub LoadNetfilterModules(@)
{
   my @modules = @_;
   my @nf_modules = GetNetfilterModules();

   foreach $module (@modules) {
      if (grep($_ eq $module, @nf_modules)) {
         LoadModule($module);
      } else {
         LogWarn("Not loading non-iptables module $module");
      }
   }
}


########################################################################
#
# LoadModule --
#
#       Loads the specified module.  Warns on failure.
#
# Results:
#
# Side Effects:
#
########################################################################

sub LoadModule($)
{
   my $module = shift;
   my $cmd = "/sbin/modprobe -q $module";

   LogCommand($cmd) || LogWarn("Command '$cmd' failed");
}


########################################################################
# 
#  Usage --
#
#       Prints a obscure message that hints at how this program might be run.
#
# Results:
#
# Side Effects:
#
########################################################################

sub Usage
{
   my $exitVal =  shift || 0;

   print <<EOD;
$shortAppName <options>
-q|--query                                      Lists current settings.
-q|--query <service>                            Lists setting for the 
                                                specified service.
-q|--query incoming|outgoing                    Lists setting for non-required
                                                incoming/outgoing ports.
-s|--services                                   Lists known services.
-l|--load                                       Loads current settings.
-r|--resetDefaults                              Resets all options to defaults
-e|--enableService <service>                    Allows specified service
                                                through the firewall.
-d|--disableService <service>                   Blocks specified service
-o|--openPort <port,tcp|udp,in|out,name>        Opens a port.
-c|--closePort <port,tcp|udp,in|out>            Closes a port previously opened 
                                                via --openPort.
   --ipruleAdd <host,cport,tcp|udp,REJECT|DROP|ACCEPT,name>  Adds a rule
                                                to block/allow hosts to access
                                                specific COS service;'cport' can
						be specified like 'a:b'. For ex:
						0:65535 blocks all the ports;
						'host' can specified like 'a/b'. 
						For ex: 0.0.0.0/0 blocks all the
						hosts.
   --ipruleDel <host,cport,tcp|udp,REJECT|DROP|ACCEPT>    Deletes the host rule
				                previously added via --ipruleAdd
   --moduleAdd <module>                         Loads an iptables module, and
                                                adds it to the peristent
                                                firewall configuration.
   --moduleDel <module>                         Removes an iptables module, and
                                                removes it from the persistent
                                                firewall configuration.
   --blockIncoming                              Block all non-required incoming 
                                                ports  (default value).
   --blockOutgoing                              Block all non-required outgoing 
                                                ports (default value).
   --allowIncoming                              Allow all incoming ports.
   --allowOutgoing                              Allow all outgoing ports.
-h|--help                                       Show this message.
EOD
   exit 0;
}



########################################################################
#
# ConfigDB query wrappers. 
#
########################################################################

sub FWBlockIncoming()
{
   return $cfgDB->GetBoolean('/firewall/blockIncoming', 1);
}

sub FWBlockOutgoing()
{
   return $cfgDB->GetBoolean('/firewall/blockOutgoing', 1);
}


########################################################################
#
# Init --
#
#       Initialize infrastructure like config and logging.
#
# Results:
#       True on success, false on failure.
#
# Side effects:
#       Log and configuration setup.
#
########################################################################

sub Init
{
   
   unless ($> == 0) {
      LogError("Must be root to run this script\n");
      exit $errCodes{User_not_root};
   }

   unless (InitLog(StdFile('esxcfg_firewall_log', 'esx'), 1)) {
      LogWarn("Could not initialize script logfile, logging to stdout.");
   }
   my $log = LogPeek();
   $log->AddDestination(\*STDERR, "STDERR", VMware::Log::ID_ERROR);
   $log->AddDestination(\*STDOUT, "STDOUT", VMware::Log::ID_WARN);

   $cfgDB = new VMware::Config::VmacoreConfigObj();
   $cfgFile = StdFile('esx_config', 'esx');
   if (!$cfgDB->ReadFile($cfgFile)) {
      Panic("Failed to load cfg file $cfgFile\n", $errCodes{Failed_to_load_cfgfile});
   }
}

########################################################################
#
# LockConfigFile --
#
#       Check that the config file can be written and then lock
#       and read it.
#
# Results:
#       True on success, false on failure.
#
# Side effects:
#       Filesystem activity (create lock file, read config).
#
########################################################################
sub LockConfigFile
{
   #
   # We've been asked to perform a read-write operation.
   # Lock the configuration and read it in.
   #

   unless (-w $cfgFile) {
      Panic("Configuration file '$cfgFile' is not writable.", 
            $errCodes{Failed_to_lock_cfgfile});
   }
   if (!$cfgDB->LockAndReadFile($cfgFile)) {
      Panic("Unable to lock config file $cfgFile.  Try again later\n", 
            $errCodes{Failed_to_lock_cfgfile});
   }

   #
   # Try to ensure that the config file is unlocked in the event
   # a library function feel compelled to Panic().
   #

   my $panicUnlockSub = sub { $cfgDB->UnlockFile(); };
   RegisterPanicCallback($panicUnlockSub, [], "esxcfg-boot");
}


########################################################################
#
# ModifyService --
#
#       Enables / disables a service and reloads the firewall
#
# Results:
#
# Side Effects:
#
########################################################################

sub ModifyService
{
   my $optName = shift;
   my $service = shift;
   my $newValue = ($optName =~ /enable/)? 1 : 0;
   
   if (!$services{$service}) {
      LogError("Unknown service '$service'\n");
      exit $errCodes{Unknown_service};
   }
   LogInfo("Setting service $service to $newValue\n");

   LockConfigFile();
   $cfgDB->Set("/firewall/services/$service", $newValue);
   unless ($cfgDB->WriteAndUnlockFile()) {
      Panic("Could not commit changes back to esx.conf.",
            $errCodes{Failed_to_commit_cfgfile});
   }
   FWConditionalLoad();
}

########################################################################
#
# ModifyFWDefaults --
#
#       Modify default rules for the INPUT/OUTPUT chains
#
# Results:
#
# Side Effects:
#
########################################################################

sub ModifyFWDefaults
{
   my $optName = shift;
   my $newValue = ($optName =~ /block/)? 1 : 0;
   my $cfgKey = '/firewall/block' . (($optName =~ /(Incoming)/)? 'Incoming' : 'Outgoing');
   
   LogWarn("Setting firewall default $cfgKey to $newValue\n");

   LockConfigFile();
   $cfgDB->Set($cfgKey, $newValue);
   unless ($cfgDB->WriteAndUnlockFile()) {
      Panic("Could not commit changes back to esx.conf.", 
            $errCodes{Failed_to_commit_cfgfile});
   }
   FWConditionalLoad();
}


########################################################################
#
# LoadOpenPorts
#       
#       Loads the opened ports from the config file.  The config 
#       file looks something like this:
#  
#
# Results:
#
# Side Effects:
#
########################################################################

sub LoadOpenPorts
{
   my %portHash;
   my $portCfg = $cfgDB->GetTree(["firewall", "openedPorts[]"]);

   foreach my $portInfo (@$portCfg) {
      LogDebug("Info for port: $portInfo->{port}");
      foreach my $property (keys %{$portInfo}) {
         LogDebug("\t$property: $portInfo->{$property}");
      }
      $portHash{$portInfo->{port}} = $portInfo;
   }

   return \%portHash;
}


########################################################################
#
# SavePorts
#       
#       Writes back complete list of ports to esx.conf.  Skips
#       empty port specifications.
#
# Results:
#
# Side Effects:
#
########################################################################

sub SavePorts
{
   my $portHash = shift;
   my @portCfg;

   foreach my $portInfo (map { $portHash->{$_}; } (keys %{$portHash})) {
      if (!grep(/tcp|udp/, (keys %{$portInfo}))) {
         LogDebug("$portInfo->{port} has no open protocols/directiors.  nuking");
         next;
      }
      push @portCfg, $portInfo;
      LogDebug("SavePorts: I gots $portInfo");
      foreach my $property (keys %{$portInfo}) {
         LogDebug("SavePorts: $property: $portInfo->{$property}");
      }
   }

   $cfgDB->SetTree(["firewall", "openedPorts[]"], \@portCfg);
}


########################################################################
#
# PortSanityCheck
#       
#       Verifies the specified port info is sane, and populates
#       a hash with it.
#
# Results:
#
#       Hash ref containing the port info
#
# Side Effects:
#
########################################################################

sub PortSanityCheck
{
   my $port = shift;
   my $direction = shift;
   my $protocol = shift;
   my $name = shift;
   my %portInfo;

   LogDebug("Sanity checking port: $port, $direction, $protocol\n");
   if ($port =~ /^(\d+)?:?(\d+)?$/) {
      if (!defined($1) && !defined($2)) {
         LogError("PortSanityCheck: bad port specification, '$port'\n");
         Usage($errCodes{Port_out_of_range});
      }
      my $lower = defined($1) ? $1 : 0;
      my $upper = defined($2) ? $2 : 65535;
      if ($lower > $upper || $upper > 65535) {
         LogError("PortSanityCheck: bad port specification, '$port'\n");
         Usage($errCodes{Port_out_of_range});
      }
   } else {
      LogError("PortSanityCheck: bad port specification, '$port'\n");
      Usage($errCodes{Port_out_of_range});
   }

   if (!($protocol =~ m/^(tcp|udp)$/)) {
      LogError("PortSanityCheck: bad protocol specification, '$protocol'.  " .
               "Must be one of udp|tcp.\n");
      Usage($errCodes{Port_protocol_invalid});
   }

   if (!($direction =~ m/^(in|out)$/)) {
      LogError("PortSanityCheck: bad port direction specification, '$direction'.  " .
               "Must be one of in|out.\n");
      Usage($errCodes{Port_direction_invalid});
   }

   if (defined $name) {
      if (length $name >= 128) {
         my $shortName = substr $name, 0, 10;
         LogError("PortSanityCheck: name specified '$shortName...' is too long. " .
                  "Must be less than 128 characters.\n");
         Usage($errCodes{Port_protocol_invalid});
      }
      $portInfo{name} = $name;
   }
   $portInfo{port} = $port;
   $portInfo{"$protocol.$direction"} = 1;

   return \%portInfo;
}


########################################################################
#
# OpenPort --
#
#       Opens specified port in the firewall.
#
# Results:
#  
#   Doesn't fail if the port is already open.
#
# Side Effects:
#
########################################################################

sub OpenPort
{
   my $cmd = shift;
   my ($port,$protocol,$direction,$name) = split(',', shift);
   my $ports;

   if (!defined($name)) {
      LogError("OpenPort:  must specify port, direction, protocol and name.");
      Usage($errCodes{Invalid_argument});
   }

   my $portInfo = PortSanityCheck($port, $direction, $protocol, $name);

   LockConfigFile();
   $ports = LoadOpenPorts();

   foreach $property (keys %$portInfo) {
      LogDebug("OpenPort: I got $property:$portInfo->{$property}");
      $ports->{$port}->{$property} = $portInfo->{$property};
   }

   SavePorts($ports);

   unless ($cfgDB->WriteAndUnlockFile()) {
      Panic("Could not commit changes back to esx.conf.",
            $errCodes{Failed_to_commit_cfgfile});
   }
   FWConditionalLoad();
}


########################################################################
#
# ClosePort --
#
#       Closes an open port
#
# Results:
#       Exits with an error code on invalid or not open port.
#
# Side Effects:
#
########################################################################

sub ClosePort
{
   my $cmd = shift;
   my ($port,$protocol,$direction) = split(',', shift);

   if (!defined($protocol)) {
      LogError("ClosePort:  must specify port, direction, and protocol.");
      Usage($errCodes{Invalid_argument});
   }
   
   my $portInfo = PortSanityCheck($port,$direction,$protocol);

   LockConfigFile();
   my $ports = LoadOpenPorts();

   if (!$ports->{$port}) {
      LogError("$port isn't open.");
      exit $errCodes{Port_not_open};
   }

   
   my $deletedEntry = 0;
   foreach $property (keys %$portInfo) {
      next if ($property eq "port");
      LogDebug("\tClose ports, checking for $property\n");
      if (defined $ports->{$port}{$property}) {
         delete $ports->{$port}{$property};
         $deletedEntry = 1;
      }
   }
   if (!$deletedEntry) { 
      LogError("$port,$direction,$protocol isn't open");
      exit $errCodes{Port_not_open};
   }

   # save ports will reap dead ports
   SavePorts($ports);

   unless ($cfgDB->WriteAndUnlockFile()) {
      Panic("Could not commit changes back to esx.conf.",
            $errCodes{Failed_to_commit_cfgfile});
   }
   FWConditionalLoad();
}

########################################################################
#
# LoadIprules
#
#       Loads the Iprules from the config file.
#
#
# Results:
#
# Side Effects:
#
########################################################################

sub LoadIprules
{
   my %hostHash;
   my $hostCfg = $cfgDB->GetTree(["firewall", "Iprules[]"]);

   foreach my $hostInfo (@$hostCfg) {
      LogDebug("Info for host:
$hostInfo->{host},$hostInfo->{cport},$hostInfo->{target}");
      my $protdire;
      foreach my $property (keys %{$hostInfo}) {
         if (grep(/tcp|udp/,$property)){
            $protdire=$property;
         }
         LogDebug("\t$property: $hostInfo->{$property}");
      }
      $hostHash{"$hostInfo->{host}.$hostInfo->{cport}.$protdire.$hostInfo->{target}"} = $hostInfo;
   }

   return \%hostHash;
}

########################################################################
#
# SaveIprules
#
#       Writes back complete list of Iprules to esx.conf.  Skips
#       empty Iprule specifications.
#
# Results:
#
# Side Effects:
#
########################################################################

sub SaveIprules
{
   my $hostHash = shift;
   my @hostCfg;

   foreach my $hostInfo (map { $hostHash->{$_}; } (keys %{$hostHash})) {
      if (!grep(/tcp|udp/, (keys %{$hostInfo}))) {
         LogDebug("hostInfo: @(values %{$hostInfo}) has no open protocols \n");
         next;
      }
      push @hostCfg, $hostInfo;
      LogDebug("SaveIprules: I gots $hostInfo");
      foreach my $property (keys %{$hostInfo}) {
         LogDebug("SaveIprules: $property: $hostInfo->{$property}");
      }
   }

   $cfgDB->SetTree(["firewall", "Iprules[]"], \@hostCfg);
}


########################################################################
#
# IpruleSanityCheck
#
#       Verifies the specified host info is sane, and populates
#       a hash with it.
#
# Results:
#
#       Hash ref containing the port info
#
# Side Effects:
#
########################################################################

sub IpruleSanityCheck
{
   my $host = shift;
   my $cport = shift;
   my $protocol = shift;
   my $target = shift;
   my $name = shift;
   my %hostInfo;


   LogDebug("Sanity checking Iprule: $host, $cport, $protocol,$target\n");
   my ($ip,$mask) = split '/', $host;


   if(defined $mask) {
      if ($mask =~ /^(\d+)$/) {
	 if ($mask < 0 || $mask >32) {
            LogError("IpruleSanityCheck: bad mask,please set it between 0 and 32, '$host'\n");
            Usage($errCodes{Iprule_ip_out_of_range});
	 }
      } else {
         LogError("IpruleSanityCheck: bad mask,please set it between 0 and 32, '$host'\n");
         Usage($errCodes{Iprule_ip_out_of_range});
      }	
   } else { 

      if ($host =~ /^(\d+)\.(\d+)\.(\d+).(\d+)$/) {
         if ($1 < 0 || $2 < 0 || $3 < 0 || $4 < 0 || $1 > 255 || $2 > 255 || $3 >
255 || $4 > 255){
            LogError("IpruleSanityCheck: bad IP_ADDR specification, '$host'\n");
            Usage($errCodes{Iprule_ip_out_of_range});
         }
      } else {
         LogError("IpruleSanityCheck: bad IP_ADDR specification, '$host'\n");
         Usage($errCodes{Iprule_ip_out_of_range});
      }
   }

   if ($cport =~ /^(\d+)?:?(\d+)?$/) {
      if (!defined($1) && !defined($2)) {
         LogError("IpruleSanityCheck: bad port specification, '$cport'\n");
         Usage($errCodes{Iprule_port_out_of_range});
      }
      my $lower = defined($1) ? $1 : 0;
      my $upper = defined($2) ? $2 : 65535;
      if ($lower > $upper || $upper > 65535) {
         LogError("IpruleSanityCheck: bad port specification, '$cport'\n");
         Usage($errCodes{Iprule_port_out_of_range});
      }
   } else {
      LogError("IpruleSanityCheck: bad port specification, '$cport'\n");
      Usage($errCodes{Iprule_port_out_of_range});
   }

   if (!($protocol =~ m/^(tcp|udp)$/)) {
      LogError("IpruleSanityCheck: bad protocol specification, '$protocol'.  " .
               "Must be one of udp|tcp.\n");
      Usage($errCodes{Iprule_protocol_invalid});
   }


   if (!($target =~ m/^(REJECT|ACCEPT|DROP)$/)) {
      LogError("IpruleSanityCheck: bad Iprule target specification, '$target'. " .
               "Must be one of REJECT|ACCEPT|DROP.\n");
      Usage($errCodes{Iprule_target_invalid});

   }

   if (defined $name) {
      if (length $name >= 128) {
         my $shortName = substr $name, 0, 10;
         LogError("PortSanityCheck: name specified '$shortName...' is too long." .
                  "Must be less than 128 characters.\n");
         Usage($errCodes{Port_protocol_invalid});
      }
      $hostInfo{name} = $name;
   }

   $hostInfo{host} = $host;
   $hostInfo{cport} = $cport;
   $hostInfo{target} = $target;
   $hostInfo{name} = $name;
   $hostInfo{"$protocol"} = 1;

   return \%hostInfo;
}


########################################################################
#
# IpruleAdd --
#
#       Add an IP rule to block/allow hosts to access the services of COS.
#
# Results:
#
#   Doesn't fail if the Iprule is already added.
#
# Side Effects:
#
########################################################################
sub IpruleAdd
{

   my $cmd = shift;
   my ($host,$cport,$protocol,$target,$name) = split(',', shift);
   my $hosts;

   if (!defined($name)) {
      LogError("RestrictHost:  must specify host, cport, protocol, target and name.");
      Usage($errCodes{Invalid_argument});
   }

   my $hostInfo = IpruleSanityCheck($host,$cport,$protocol,$target,$name);

   LockConfigFile();
   $hosts = LoadIprules();

   foreach $property (keys %$hostInfo) {
      LogDebug("IpruleAdd: I got $property:$hostInfo->{$property}");
      $hosts->{"$host.$cport.$protocol.$target"}->{$property} = $hostInfo->{$property};
   }

   SaveIprules($hosts);

   unless ($cfgDB->WriteAndUnlockFile()) {
      Panic("Could not commit changes back to esx.conf.",$errCodes{Failed_to_commit_cfgfile});
   }
   FWConditionalLoad();
}


########################################################################
#
# IpruleDel --
#
#       Delete an IP rule.
#
# Results:
#
#   Doesn't fail if the Iprule doesn't exist.
#
# Side Effects:
#
########################################################################
sub IpruleDel
{

   my $cmd = shift;
   my ($host,$cport,$protocol,$target) = split(',', shift);
   my $hosts;

   my $hostInfo = IpruleSanityCheck($host,$cport,$protocol,$target);

   LockConfigFile();
   $hosts = LoadIprules();

   if (!$hosts->{"$host.$cport.$protocol.$target"}) {
      LogError("Iprule $host,$cport,$protocol is not added.");
      exit $errCodes{Iprule_not_added};
   }


   my $deletedEntry = 0;
   foreach $property (keys %$hostInfo) {
      next if ($property eq "$host.$cport.$protocol.$target");
      LogDebug("\tDelete Iprules, checking for $property\n");
      if (defined $hosts->{"$host.$cport.$protocol.$target"}{$property}) {
         delete $hosts->{"$host.$cport.$protocol.$target"}{$property};
         $deletedEntry = 1;
      }
   }
   if (!$deletedEntry) {
      LogError("$host,$cport,$protocol isn't added");
      exit $errCodes{Iprule_not_added};
   }

   SaveIprules($hosts);

   unless ($cfgDB->WriteAndUnlockFile()) {
      Panic("Could not commit changes back to esx.conf.",
            $errCodes{Failed_to_commit_cfgfile});
   }
   FWConditionalLoad();
}


########################################################################
#
# ModuleAdd --
#
#       Add a module to be loaded at firewall startup.
#
# Results:
#
#   Doesn't fail if the module is already added.
#
# Side Effects:
#
########################################################################
sub ModuleAdd
{
   my $cmd = shift;
   my $module = shift;

   if (!defined($module)) {
      LogError("ModuleAdd:  must specify module name.");
      Usage($errCodes{Invalid_argument});
   }

   if (!grep($module, GetNetfilterModules())) {
      LogWarn("ModuleAdd: $module is not an iptables module.");
      Usage($errCodes{Invalid_argument});
   }

   LockConfigFile();
   my @cfgModules = $cfgDB->GetArray('/firewall/modules', []);
   if (grep($_ eq $module, @cfgModules)) {
      $cfgDB->UnlockFile();
      return;
   }

   push @cfgModules, $module;

   $cfgDB->Set('/firewall/modules', join(',', @cfgModules));

   unless ($cfgDB->WriteAndUnlockFile()) {
      Panic("Could not commit changes back to esx.conf.",
            $errCodes{Failed_to_commit_cfgfile});
   }
   FWConditionalLoad();
}


########################################################################
#
# ModuleDel --
#
#       Remove a module from the list loaded at firewall startup.
#
# Results:
#
#   Doesn't fail if the module is already removed.
#
# Side Effects:
#
########################################################################
sub ModuleDel
{
   my $cmd = shift;
   my $module = shift;

   if (!defined($module)) {
      LogError("ModuleDel:  must specify module name.");
      Usage($errCodes{Invalid_argument});
   }

   LockConfigFile();
   my @cfgModules = $cfgDB->GetArray('/firewall/modules', []);
   my @newModules;
   foreach $index (0 .. $#cfgModules) {
      if ($cfgModules[$index] ne $module) {
         push @newModules, $cfgModules[$index];
      }
   }

   if ($#cfgModules == $#newModules) {
      $cfgDB->UnlockFile();
      return;
   }

   $cfgDB->Set('/firewall/modules', join(',', @newModules));

   unless ($cfgDB->WriteAndUnlockFile()) {
      Panic("Could not commit changes back to esx.conf.",
            $errCodes{Failed_to_commit_cfgfile});
   }
   FWConditionalLoad();
}


########################################################################
#
# ResetDefafaults --
#
#       Sets all firewall options to default values.
#
# Results:
#       None
#
# Side Effects:
#
########################################################################

sub ResetDefafaults
{
   LockConfigFile();
   $cfgDB->SetTree(["firewall"], []);

   LogInfo("Reseting all firewall options to the factory defaults\n");
   unless ($cfgDB->WriteAndUnlockFile()) {
      Panic("Could not commit changes back to esx.conf.",
            $errCodes{Failed_to_commit_cfgfile});
   }
   FWConditionalLoad();
}

Usage() if (!$ARGV[0]);

Init();

GetOptions(
  'query:s'          => \&FWQuery,
  'load'             => \&FWLoad,
  'unload'           => \&FWUnload,
  'services'         => \&FWListServices,
  'verbose'          => \$logDrops,
  'help'             => \&Usage,
  'enableService=s'  => \&ModifyService,
  'disableService=s' => \&ModifyService,
  'openPort=s'       => \&OpenPort,
  'closePort=s'      => \&ClosePort,
  'ipruleAdd=s'      => \&IpruleAdd,
  'ipruleDel=s'      => \&IpruleDel,
  'moduleAdd=s'      => \&ModuleAdd,
  'moduleDel=s'      => \&ModuleDel,
  'blockIncoming'    => \&ModifyFWDefaults,
  'blockOutgoing'    => \&ModifyFWDefaults,
  'allowIncoming'    => \&ModifyFWDefaults,
  'allowOutgoing'    => \&ModifyFWDefaults,
  'resetDefaults'    => \&ResetDefafaults,
) or Usage();

### Error out if unparsed options
if ($ARGV[0]) {
   print("\nUnknown option $ARGV[0]\n");
   Usage() 
}

exit 0;
