Modified: vcl/trunk/managementnode/lib/VCL/Module/OS/Linux/firewall/iptables.pm URL: http://svn.apache.org/viewvc/vcl/trunk/managementnode/lib/VCL/Module/OS/Linux/firewall/iptables.pm?rev=1789589&r1=1789588&r2=1789589&view=diff ============================================================================== --- vcl/trunk/managementnode/lib/VCL/Module/OS/Linux/firewall/iptables.pm (original) +++ vcl/trunk/managementnode/lib/VCL/Module/OS/Linux/firewall/iptables.pm Thu Mar 30 21:46:02 2017 @@ -50,6 +50,8 @@ use strict; use warnings; use diagnostics; +use English '-no_match_vars'; + use VCL::utils; ############################################################################## @@ -93,14 +95,435 @@ sub initialize { #///////////////////////////////////////////////////////////////////////////// +=head2 process_post_load + + Parameters : none + Returns : boolean + Description : Performs the initial iptables firewall configuration after an + image is loaded: + * A vcl-post_load chain is created in the filter table with a + rule is added to this chain to allow traffic on any port from + the management node's IP address. + * All existing rules explicitly allowing traffic to TCP/22 are + deleted. + * All other chains in the filter table named vcl-* are deleted to + clean up any possible remnants. + +=cut + +sub process_post_load { + my $self = shift; + if (ref($self) !~ /VCL::Module::OS::Linux::firewall/i) { + notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); + return 0; + } + + my $computer_name = $self->data->get_computer_short_name(); + + notify($ERRORS{'DEBUG'}, 0, "beginning firewall post-load configuration on $computer_name"); + + my $timestamp = makedatestring(); + my $post_load_chain_name = $self->get_post_load_chain_name(); + + # Try to determine the IP address the management node uses to connect to remote hosts + # managementnode.IPaddress is not necessarily the private IP used to connect to computers being loaded + my @mn_ip_addresses = $self->os->get_management_node_connected_ip_address(); + + # If unable to determine the connecting IP, open up access to all MN IP's + if (!@mn_ip_addresses) { + # Get all of the IP addresses in use on the management node + @mn_ip_addresses = $self->mn_os->get_ip_addresses(); + if (!@mn_ip_addresses) { + notify($ERRORS{'WARNING'}, 0, "failed to complete firewall post-load configuration on $computer_name, management node IP addresses could not be determined"); + return; + } + } + + # Create a chain and add a jump rule to INPUT + $self->create_chain('filter', $post_load_chain_name); + if (!$self->insert_rule('filter', 'INPUT', + { + 'parameters' => { + 'jump' => $post_load_chain_name, + }, + 'match_extensions' => { + 'comment' => { + 'comment' => "VCL: jump to rules added during the post-load stage ($timestamp)", + }, + }, + } + )) { + notify($ERRORS{'WARNING'}, 0, "failed to complete firewall post-load configuration on $computer_name, failed to create rule in INPUT chain to jump to '$post_load_chain_name' chain"); + return; + } + + # Allow traffic from any of the management node IP addresses + if (!$self->insert_rule('filter', $post_load_chain_name, + { + 'parameters' => { + 'source' => join(',', @mn_ip_addresses), + 'jump' => 'ACCEPT', + }, + 'match_extensions' => { + 'comment' => { + 'comment' => "VCL: Allow traffic from management node ($timestamp)", + }, + }, + } + )) { + notify($ERRORS{'WARNING'}, 0, "failed to complete firewall post-load configuration on $computer_name, failed to add rule allowing traffic from management node IP addresses to $post_load_chain_name chain"); + return; + } + + # Delete other vcl-* chains added by vcld + my $table_info = $self->get_table_info(); + for my $chain_name (keys %$table_info) { + if ($chain_name ne $post_load_chain_name && $chain_name =~ /^vcl-/) { + $self->delete_chain('filter', $chain_name); + } + } + + # Legacy code may have been used previously for a reservation, before an upgrade + # Clean up old connect method rules from the INPUT chain + # Delete all rules from INPUT chain matching connect method protocols and ports + $self->delete_connect_method_rules(); + + # Delete all TCP/22 rules + # Images captured prior to VCL 2.5 are saved with an expicit TCP/22 allow rule from any address + $self->delete_rules('filter', 'INPUT', + { + "match_extensions" => { + "tcp" => { + "dport" => 22, + }, + }, + "parameters" => { + "jump" => "ACCEPT", + }, + } + ); + + $self->save_configuration(); + + notify($ERRORS{'DEBUG'}, 0, "completed firewall post-load configuration on $computer_name"); + return 1; +} + +#///////////////////////////////////////////////////////////////////////////// + +=head2 process_reserved + + Parameters : none + Returns : boolean + Description : Configures the iptables firewall for the reserved state: + * A vcl-reserved chain is created with rules allowing traffic to + the connect method ports from any IP address. + +=cut + +sub process_reserved { + my $self = shift; + if (ref($self) !~ /VCL::Module::OS::Linux::firewall/i) { + notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); + return 0; + } + + # Make sure the post-load steps were done + if (!$self->chain_exists('filter', $self->get_post_load_chain_name())) { + $self->process_post_load(); + } + + my $timestamp = makedatestring(); + my $computer_name = $self->data->get_computer_short_name(); + notify($ERRORS{'DEBUG'}, 0, "beginning firewall configuration on $computer_name for reserved state"); + + my $reserved_chain_name = $self->get_reserved_chain_name(); + + # Delete existing chain if one exists to prevent inconsistent results + # Create a chain and add a jump rule to INPUT + $self->create_chain('filter', $reserved_chain_name); + if (!$self->insert_rule('filter', 'INPUT', + { + 'parameters' => { + 'jump' => $reserved_chain_name, + }, + 'match_extensions' => { + 'comment' => { + 'comment' => "VCL: jump to rules added during the reserved stage ($timestamp)", + }, + }, + } + )) { + notify($ERRORS{'WARNING'}, 0, "failed to complete firewall reserved configuration on $computer_name, failed to create rule in INPUT chain to jump to '$reserved_chain_name' chain"); + return; + } + + my @protocol_ports = $self->data->get_connect_method_protocol_port_array(); + for my $protocol_port (@protocol_ports) { + my ($protocol, $port) = @$protocol_port; + if (!$self->insert_rule('filter', $reserved_chain_name, + { + 'parameters' => { + 'protocol' => $protocol, + 'jump' => 'ACCEPT', + }, + 'match_extensions' => { + $protocol => { + 'dport' => $port, + }, + 'comment' => { + 'comment' => "VCL: Allow traffic from any IP address to connect method ports during reserved stage ($timestamp)", + }, + }, + } + )) { + notify($ERRORS{'WARNING'}, 0, "failed to complete firewall reserved configuration on $computer_name, failed to add rule to allow traffic to '$reserved_chain_name' chain, protocol: $protocol, port: $port"); + return; + } + } + + $self->save_configuration(); + + notify($ERRORS{'DEBUG'}, 0, "completed firewall reserved configuration on $computer_name"); + return 1; +} + +#///////////////////////////////////////////////////////////////////////////// + +=head2 process_inuse + + Parameters : $remote_ip_address (optional) + Returns : boolean + Description : Configures the iptables firewall for the inuse state: + * A vcl-inuse chain is created if it does not already exist. + * Rules are added to the vcl-inuse chain allowing to allow + traffic to the connect method ports from the end user's + specific IP address. + * The vcl-reserved chain is deleted if it exists. + + This subroutine can be called over and over again. It will not + remove rules previously added to the vcl-inuse chain. If a user's + remote IP address changes, this subroutine will add a new rule to + the vcl-inuse chain. + +=cut + +sub process_inuse { + my $self = shift; + if (ref($self) !~ /VCL::Module::OS::Linux::firewall/i) { + notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); + return 0; + } + + # Make sure the post-load steps were done + if (!$self->chain_exists('filter', $self->get_post_load_chain_name())) { + $self->process_post_load(); + } + + my $timestamp = makedatestring(); + my $computer_name = $self->data->get_computer_short_name(); + + my $remote_ip_address = shift || $self->data->get_reservation_remote_ip(); + if (!$remote_ip_address) { + notify($ERRORS{'WARNING'}, 0, "failed to complete firewall inuse configuration on $computer_name, remote IP could not be retrieved for reservation"); + return; + } + + notify($ERRORS{'DEBUG'}, 0, "beginning firewall configuration on $computer_name for inuse state"); + + my $inuse_chain_name = $self->get_inuse_chain_name(); + my $reserved_chain_name = $self->get_reserved_chain_name(); + + # Delete existing chain if one exists to prevent inconsistent results + # Create a chain and add a jump rule to INPUT + $self->create_chain('filter', $inuse_chain_name); + if (!$self->insert_rule('filter', 'INPUT', + { + 'parameters' => { + 'jump' => $inuse_chain_name, + }, + 'match_extensions' => { + 'comment' => { + 'comment' => "VCL: jump to rules added during the inuse stage ($timestamp)", + }, + }, + } + )) { + notify($ERRORS{'WARNING'}, 0, "failed to complete firewall inuse configuration on $computer_name, failed to create rule in INPUT chain to jump to '$inuse_chain_name' chain"); + return; + } + + my @protocol_ports = $self->data->get_connect_method_protocol_port_array(); + for my $protocol_port (@protocol_ports) { + my ($protocol, $port) = @$protocol_port; + if (!$self->insert_rule('filter', $inuse_chain_name, + { + 'parameters' => { + 'protocol' => $protocol, + 'source' => "$remote_ip_address", + 'jump' => 'ACCEPT', + }, + 'match_extensions' => { + $protocol => { + 'dport' => $port, + }, + 'comment' => { + 'comment' => "VCL: Allow traffic from $remote_ip_address to $protocol/$port ($timestamp)", + }, + }, + } + )) { + notify($ERRORS{'WARNING'}, 0, "failed to complete firewall inuse configuration on $computer_name, failed to add rule to allow traffic to '$inuse_chain_name' chain, protocol: $protocol, port: $port"); + return; + } + } + + # Delete the reserved chain which allows traffic from any address + $self->delete_chain('filter', $reserved_chain_name); + + $self->save_configuration(); + + notify($ERRORS{'DEBUG'}, 0, "completed firewall inuse configuration on $computer_name"); + return 1; +} + +#///////////////////////////////////////////////////////////////////////////// + +=head2 process_sanitize + + Parameters : none + Returns : boolean + Description : Performs the same iptables firewall configuration steps as + process_post_load. + +=cut + +sub process_sanitize { + my $self = shift; + if (ref($self) !~ /VCL::Module::OS::Linux::firewall/i) { + notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); + return 0; + } + + return $self->process_post_load(); +} + +#///////////////////////////////////////////////////////////////////////////// + +=head2 process_pre_capture + + Parameters : none + Returns : boolean + Description : Performs the iptables firewall configuration prior to capturing + an image: + * A vcl-pre_capture chain is added to the filter table + with a rule allowing TCP/22 traffic from any IP address. + * Rules matching any of the management node's IP addresses are + deleted. + * Any other chains named vcl-* are flushed and deleted. + +=cut + +sub process_pre_capture { + my $self = shift; + if (ref($self) !~ /VCL::Module::OS::Linux::firewall/i) { + notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); + return 0; + } + + my $timestamp = makedatestring(); + my $computer_name = $self->data->get_computer_short_name(); + notify($ERRORS{'DEBUG'}, 0, "beginning firewall pre-capture configuration on $computer_name"); + + my $pre_capture_chain_name = $self->get_pre_capture_chain_name(); + + # Create a chain and add a jump rule to INPUT + if (!$self->create_chain('filter', $pre_capture_chain_name)) { + notify($ERRORS{'WARNING'}, 0, "failed to complete firewall pre-capture configuration on $computer_name, failed to create '$pre_capture_chain_name' chain"); + return; + } + if (!$self->insert_rule('filter', 'INPUT', + { + 'parameters' => { + 'jump' => $pre_capture_chain_name, + }, + 'match_extensions' => { + 'comment' => { + 'comment' => "VCL: jump to rules added during the pre-capture stage ($timestamp)", + }, + }, + } + )) { + notify($ERRORS{'WARNING'}, 0, "failed to complete firewall pre-capture configuration on $computer_name, failed to create rule in INPUT chain to jump to '$pre_capture_chain_name' chain"); + return; + } + + # Allow unrestricted SSH traffic + if (!$self->insert_rule('filter', $pre_capture_chain_name, + { + 'parameters' => { + 'jump' => 'ACCEPT', + 'protocol' => 'tcp', + }, + 'match_extensions' => { + 'tcp' => { + 'destination-port' => 22, + }, + 'comment' => { + 'comment' => "VCL: Allow traffic to SSH port 22 from any IP address ($timestamp)", + }, + }, + } + )) { + notify($ERRORS{'WARNING'}, 0, "failed to complete firewall pre-capture configuration on $computer_name, failed to add rule to allow traffic on port 22 to $pre_capture_chain_name chain"); + return; + } + + # Delete all rules explicitly defined for any of the management node IP addresses + # Legacy firewall code would add rules directly to the filter/INPUT table for each management node address + my @mn_ip_addresses = $self->mn_os->get_ip_addresses(); + for my $mn_ip_address (@mn_ip_addresses) { + $self->delete_rules('filter', 'INPUT', + { + 'parameters' => { + 'source' => $mn_ip_address, + }, + } + ); + } + + # Legacy code may have been used previously for a reservation, before an upgrade + # Clean up old connect method rules from the INPUT chain + # Delete all rules from INPUT chain matching connect method protocols and ports + $self->delete_connect_method_rules(); + + # Delete other vcl-* chains added by vcld + my $table_info = $self->get_table_info(); + for my $chain_name (keys %$table_info) { + if ($chain_name ne $pre_capture_chain_name && $chain_name =~ /^vcl-/) { + $self->delete_chain('filter', $chain_name); + } + } + + $self->save_configuration(); + + notify($ERRORS{'DEBUG'}, 0, "completed firewall pre-capture configuration on $computer_name"); + return 1; +} + +#///////////////////////////////////////////////////////////////////////////// + =head2 get_iptables_semaphore Parameters : none - Returns : VCL::Semaphore object reference - Description : Obtains and returns a VCL::Semaphore object. This should be - called prior to executing iptables commands which must be run - individually. Otherwise, the following error is generated: - iptables: Resource temporarily unavailable. + Returns : true or VCL::Semaphore object reference + Description : Obtains and returns a VCL::Semaphore object if called from a + subroutine containing 'nat' in the name. This should always be + called prior to executing iptables commands on a host this could + potentially be controlled by multiple vcld processes at the same + time. If multiple iptables commands are attempted at the same + time, the following error is generated: + iptables: Resource temporarily unavailable. =cut @@ -111,6 +534,12 @@ sub get_iptables_semaphore { return 0; } + # Check if the calling subroutine contains 'nat' + my $calling_subroutine = get_calling_subroutine(); + if ($calling_subroutine !~ /(nat)/) { + return 1; + } + my $computer_id = $self->data->get_computer_id(); return $self->get_semaphore("iptables-$computer_id", 120, 1); @@ -120,16 +549,10 @@ sub get_iptables_semaphore { =head2 insert_rule - Parameters : hash reference + Parameters : $table_name, $chain_name, $rule_specification_hashref Returns : boolean Description : Inserts an iptables rule. The argument must be a properly constructed hash reference. Supported top-level hash keys are: - * {table} => '<string>' (optional) - Specifies the name of the table the rule will be added to. - If ommitted, the rule will be added to the filter table by - default. - * {chain} => '<string>' (mandatory) - Specifies the name of the chain the rule will be added to. * {parameters} => {<hash reference>} (optional) Allows any of the options under the iptables man page "PARAMETERS" section to be specified. Full parameter names @@ -150,28 +573,30 @@ sub get_iptables_semaphore { should be a hash reference whose key names should be the names of the supported options for that target extension module. + Example: - { - 'table' => 'nat', - 'chain' => 'PREROUTING', - 'parameters' => { - 'protocol' => 'tcp', - 'in-interface' => 'eth1', - }, - 'match_extensions' => { - 'comment' => { - 'comment' => "forward: eth1:50443 --> 10.1.2.3.4:443 (tcp)", + $self->os->firewall->create_chain('nat', 'test'); + $self->os->firewall->insert_rule('nat', 'test', + { + 'parameters' => { + 'protocol' => 'tcp', + 'in-interface' => 'eth1', }, - $protocol => { - 'destination-port' => 50443, + 'match_extensions' => { + 'comment' => { + 'comment' => "forward: eth1:50443 --> 10.1.2.3:443 (tcp)", + }, + 'tcp' => { + 'destination-port' => 50443, + }, }, - }, - 'target_extensions' => { - 'DNAT' => { - 'to-destination' => "10.1.2.3.4:443", + 'target_extensions' => { + 'DNAT' => { + 'to-destination' => "10.1.2.3:443", + }, }, - }, - } + } + ); =cut @@ -182,35 +607,43 @@ sub insert_rule { return 0; } - my $arguments = shift; - if (!$arguments) { - notify($ERRORS{'WARNING'}, 0, "argument was not supplied"); + my ($table_name, $chain_name, $rule_specification_hashref, $check_already_exists) = @_; + if (!$table_name) { + notify($ERRORS{'WARNING'}, 0, "table name argument was not specified"); return; } - elsif (!ref($arguments) || ref($arguments) ne 'HASH') { - notify($ERRORS{'WARNING'}, 0, "argument is not a hash reference"); + elsif (!$chain_name) { + notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified"); + return; + } + elsif (!$rule_specification_hashref) { + notify($ERRORS{'WARNING'}, 0, "rule specification hash reference argument was not specified"); + return; + } + elsif (!ref($rule_specification_hashref) || ref($rule_specification_hashref) ne 'HASH') { + notify($ERRORS{'WARNING'}, 0, "rule specification argument is not a hash reference:\n" . format_data($rule_specification_hashref)); + return; + } + elsif (!scalar(keys(%$rule_specification_hashref))) { + notify($ERRORS{'WARNING'}, 0, "rule specification argument does not contain any keys"); return; } - my $computer_name = $self->data->get_computer_hostname(); - my $command = '/sbin/iptables'; + my $computer_name = $self->data->get_computer_hostname(); - # Add the table argument if specified - if ($arguments->{table}) { - $command .= " -t $arguments->{table}"; + # Avoid duplicate/redundant rules + my @matching_rules = $self->get_matching_rules($table_name, $chain_name, $rule_specification_hashref); + if (@matching_rules) { + my @specification_strings = map { $_->{"rule_specification"} } @matching_rules; + notify($ERRORS{'OK'}, 0, "$chain_name chain rule in $table_name table already exists on $computer_name:\n" . join("\n", @specification_strings)); + return 1; } - # Get the chain argument - my $chain = $arguments->{chain}; - if (!defined($chain)) { - notify($ERRORS{'WARNING'}, 0, "chain argument was not specified:\n" . format_data($arguments)); - return; - } - $command .= " -I $chain"; + my $command = "/sbin/iptables -t $table_name -I $chain_name"; # Add the parameters to the command - for my $parameter (sort keys %{$arguments->{parameters}}) { - my $value = $arguments->{parameters}{$parameter}; + for my $parameter (sort keys %{$rule_specification_hashref->{parameters}}) { + my $value = $rule_specification_hashref->{parameters}{$parameter}; if ($parameter =~ /^\!/) { $command .= " !"; @@ -220,10 +653,10 @@ sub insert_rule { } # Add the match extension to the command - for my $match_extension (sort keys %{$arguments->{match_extensions}}) { + for my $match_extension (sort keys %{$rule_specification_hashref->{match_extensions}}) { $command .= " --match $match_extension"; - for my $option (sort keys %{$arguments->{match_extensions}{$match_extension}}) { - my $value = $arguments->{match_extensions}{$match_extension}{$option}; + for my $option (sort keys %{$rule_specification_hashref->{match_extensions}{$match_extension}}) { + my $value = $rule_specification_hashref->{match_extensions}{$match_extension}{$option}; if ($option =~ /(comment)/) { $value = "\"$value\""; @@ -241,10 +674,10 @@ sub insert_rule { } # Add the target extensions to the command - for my $target_extension (sort keys %{$arguments->{target_extensions}}) { + for my $target_extension (sort keys %{$rule_specification_hashref->{target_extensions}}) { $command .= " --jump $target_extension"; - for my $option (sort keys %{$arguments->{target_extensions}{$target_extension}}) { - my $value = $arguments->{target_extensions}{$target_extension}{$option}; + for my $option (sort keys %{$rule_specification_hashref->{target_extensions}{$target_extension}}) { + my $value = $rule_specification_hashref->{target_extensions}{$target_extension}{$option}; $command .= " --$option " if $option; $command .= $value; } @@ -253,120 +686,293 @@ sub insert_rule { my $semaphore = $self->get_iptables_semaphore(); my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { - notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command"); + notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name: $command"); return; } elsif ($exit_status ne '0') { - notify($ERRORS{'WARNING'}, 0, "failed to add iptables rule on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); + notify($ERRORS{'WARNING'}, 0, "failed to add iptables rule to $chain_name chain in $table_name table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return 0; } else { - notify($ERRORS{'OK'}, 0, "added iptables rule on $computer_name, command: $command"); + notify($ERRORS{'OK'}, 0, "added iptables rule to $chain_name chain in $table_name table on $computer_name, command: $command"); return 1; } } #///////////////////////////////////////////////////////////////////////////// -=head2 delete_rule +=head2 get_matching_rules - Parameters : hash reference - -or- - $table_name, $chain_name, $rule_specification - Returns : boolean - Description : Deletes a rule. + Parameters : $table_name, $chain_name, $rule_specification_hashref + Returns : array + Description : Checks the chain for any rules that match all parameters + specified in the $rule_specification_hashref argument. For + example, to find all TCP/22 rules: + $self->os->firewall->get_matching_rules('filter', 'INPUT', + { + 'parameters' => { + 'protocol' => 'tcp', + }, + 'match_extensions' => { + 'tcp' => { + 'dport' => 22, + }, + }, + } + ); =cut -sub delete_rule { +sub get_matching_rules { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } - my $argument = shift; - if (!$argument) { - notify($ERRORS{'WARNING'}, 0, "argument was not supplied"); + my ($table_name, $chain_name, $rule_specification_hashref) = @_; + if (!$table_name) { + notify($ERRORS{'WARNING'}, 0, "table name argument was not specified"); + return; + } + elsif (!$chain_name) { + notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified"); + return; + } + elsif (!$rule_specification_hashref) { + notify($ERRORS{'WARNING'}, 0, "rule specification hash reference argument was not specified"); + return; + } + elsif (!ref($rule_specification_hashref) || ref($rule_specification_hashref) ne 'HASH') { + notify($ERRORS{'WARNING'}, 0, "rule specification argument is not a hash reference:\n" . format_data($rule_specification_hashref)); + return; + } + elsif (!scalar(keys(%$rule_specification_hashref))) { + notify($ERRORS{'WARNING'}, 0, "rule specification argument does not contain any keys"); return; } my $computer_name = $self->data->get_computer_hostname(); - my $command = '/sbin/iptables'; + my @matching_rules; - - if (ref($argument) && ref($argument) eq 'HASH') { - # Add the table argument if specified - if ($argument->{table}) { - $command .= " -t $argument->{table}"; - } - - # Get the chain argument - my $chain = $argument->{chain}; - if (!defined($chain)) { - notify($ERRORS{'WARNING'}, 0, "chain argument was not specified:\n" . format_data($argument)); - return; - } - $command .= " -D $chain"; - - # Add the parameters to the command - for my $parameter (sort keys %{$argument->{parameters}}) { - my $value = $argument->{parameters}{$parameter}; - $command .= " --$parameter $value"; - } - - # Add the match extension to the command - for my $match_extension (sort keys %{$argument->{match_extensions}}) { - $command .= " --match $match_extension"; - for my $option (sort keys %{$argument->{match_extensions}{$match_extension}}) { - my $value = $argument->{match_extensions}{$match_extension}{$option}; - - if ($option =~ /(comment)/) { - $value = "\"$value\""; + my $table_info = $self->get_table_info($table_name) || return; + if (!defined($table_info->{$chain_name})) { + notify($ERRORS{'DEBUG'}, 0, "no rules match on $computer_name, $table_name table does not contain a '$chain_name' chain"); + return @matching_rules; + } + elsif (!defined($table_info->{$chain_name}{rules})) { + notify($ERRORS{'DEBUG'}, 0, "no rules match on $computer_name, $chain_name chain in $table_name table contains no rules"); + return @matching_rules; + } + + # This sub was designed to accept a hash reference argument to match other + # parts of this module. However, we need to compare the hash reference + # argument to the hash reference which contains current rule info. Comparing + # the two as-is is extremely difficult and would require complex recursion. + # Instead, get_collapsed_hash_reference takes the input multi-level hash + # reference, finds all of the keys which contain a scalar value, and + # constucts concatenated key names containing the values. The key names can + # used in an eval statement to compare another hash reference. + + my $collapsed_specification = get_collapsed_hash_reference($rule_specification_hashref); + if (!$collapsed_specification) { + notify($ERRORS{'WARNING'}, 0, "failed to determine if any rules match on $computer_name, failed to parse rule specification hash reference argument:\n" . format_data($rule_specification_hashref)); + return; + } + elsif (!scalar keys(%$collapsed_specification)) { + notify($ERRORS{'WARNING'}, 0, "failed to determine if any rules match on $computer_name, attempt to collapse the rule specification hash reference argument produced a result with no keys:\n" . format_data($rule_specification_hashref)); + return; + } + notify($ERRORS{'DEBUG'}, 0, "checking if $chain_name chain in $table_name table on $computer_name has any rules matching specifications:\n" . format_data($collapsed_specification)); + + # Some iptables options may take multiple forms + # Attempt to try all forms + my $alternate_option_names = { + 'destination-port' => 'dport', + 'source-port' => 'sport', + }; + + RULE: for my $rule (@{$table_info->{$chain_name}{rules}}) { + for my $specification_key (keys %$collapsed_specification) { + # Ignore comments when comparing + if ($specification_key =~ /(comment)/i) { + next; + } + + my $specification_value = $collapsed_specification->{$specification_key}; + + # Check if matches known alternate ('source-port' <--> 'sport') + my $alternate_specification_key; + for my $original_name (keys %$alternate_option_names) { + if ($specification_key =~ /$original_name/i) { + my $alternate_name = $alternate_option_names->{$original_name}; + $alternate_specification_key = $specification_key; + $alternate_specification_key =~ s/$original_name/$alternate_name/i; } - - $command .= " --$option $value"; } - } - - # Add the target extensions to the command - for my $target_extension (sort keys %{$argument->{target_extensions}}) { - $command .= " --jump $target_extension"; - for my $option (sort keys %{$argument->{target_extensions}{$target_extension}}) { - my $value = $argument->{target_extensions}{$target_extension}{$option}; - $command .= " --$option $value"; + + # $specification_key will contain a string such as: + # "{'match_extensions'}{'tcp'}{'dport'}" + # Use this in an eval block to check if the current rule has a matching key and the same value + my $rule_value; + my $eval_string; + if ($alternate_specification_key) { + $eval_string = "\$rule_value = (\$rule->$specification_key || \$rule->$alternate_specification_key)"; + } + else { + $eval_string = "\$rule_value = \$rule->$specification_key"; + } + eval($eval_string); + if ($EVAL_ERROR) { + notify($ERRORS{'WARNING'}, 0, "failed to determine value of $specification_key key from rule on $computer_name, code evaluated: '$eval_string', error: $EVAL_ERROR, rule:\n" . format_data($rule)); + return; + } + elsif (!defined($rule_value)) { + #notify($ERRORS{'DEBUG'}, 0, "ignoring rule on $computer_name, it does not contain a $specification_key value"); + next RULE; + } + + if ($rule_value ne $specification_value && $rule_value !~ /^$specification_value(\/32)?$/i) { + #notify($ERRORS{'DEBUG'}, 0, "ignoring rule on $computer_name, $specification_key value does not match, rule: '$rule_value', argument:'$specification_value'"); + next RULE; } } + + notify($ERRORS{'DEBUG'}, 0, "rule matches: " . $rule->{rule_specification}); + push @matching_rules, $rule; } - elsif (my $type = ref($argument)) { - notify($ERRORS{'WARNING'}, 0, "argument $type reference not supported, argument must only be a HASH reference or scalar"); + + my $matching_rule_count = scalar(@matching_rules); + notify($ERRORS{'DEBUG'}, 0, "found $matching_rule_count matching rule" . ($matching_rule_count == 1 ? '' : 's')); + return @matching_rules; +} + +#///////////////////////////////////////////////////////////////////////////// + +=head2 delete_rules + + Parameters : $table_name, $chain_name, $rule_specification_hashref + Returns : boolean + Description : Deletes all rules matching the table, chain, and specification + hash reference. The hash must be in the same format that is + returned by get_table_info, such as: + { + "match_extensions" => { + "tcp" => { + "dport" => 22, + }, + }, + "parameters" => { + "jump" => { + "target" => "ACCEPT", + }, + "protocol" => "tcp", + }, + } + + An existing rule will be deleted if and only if it contains + exactly all of the keys defined in the argument, case sensitive. + The actual value must match but is checked case insensitive. + +=cut + +sub delete_rules { + my $self = shift; + if (ref($self) !~ /VCL::Module/i) { + notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); + return 0; + } + + my ($table_name, $chain_name, $rule_specification_hashref) = @_; + if (!$table_name) { + notify($ERRORS{'WARNING'}, 0, "table name argument was not specified"); return; } - else { - my $table_name = $argument; - my ($chain_name, $specification) = @_; - if (!defined($chain_name) || !defined($specification)) { - notify($ERRORS{'WARNING'}, 0, "1st argument is a scalar, 2nd chain name and 3rd rule specification arguments not provided"); - return; - } - $command .= " -D $chain_name -t $table_name $specification"; + elsif (!$chain_name) { + notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified"); + return; } - - my $semaphore = $self->get_iptables_semaphore(); - my ($exit_status, $output) = $self->os->execute($command, 0); - if (!defined($output)) { - notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command"); + elsif (!$rule_specification_hashref) { + notify($ERRORS{'WARNING'}, 0, "rule specification hash reference argument was not specified"); return; } - elsif ($exit_status ne '0') { - notify($ERRORS{'WARNING'}, 0, "failed to delete iptables rule on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); + elsif (!ref($rule_specification_hashref) || ref($rule_specification_hashref) ne 'HASH') { + notify($ERRORS{'WARNING'}, 0, "rule specification argument is not a hash reference:\n" . format_data($rule_specification_hashref)); + return; + } + elsif (!scalar(keys(%$rule_specification_hashref))) { + notify($ERRORS{'WARNING'}, 0, "rule specification argument does not contain any keys"); + return; + } + + my $computer_name = $self->data->get_computer_hostname(); + + my @matching_rules = $self->get_matching_rules($table_name, $chain_name, $rule_specification_hashref); + for my $rule (@matching_rules) { + # Make sure rule has a 'rule_specification' value or else it can't be deleted + my $rule_specification_string = $rule->{rule_specification}; + if (!$rule_specification_string) { + notify($ERRORS{'DEBUG'}, 0, "ignoring rule on $computer_name because it does not contain a 'rule_specification' key:\n" . format_data($rule)); + next RULE; + } + + notify($ERRORS{'DEBUG'}, 0, "attempting to delete rule on $computer_name: $rule_specification_string"); + my $semaphore = $self->get_iptables_semaphore(); + my $command = "/sbin/iptables --delete $chain_name -t $table_name $rule_specification_string"; + my ($exit_status, $output) = $self->os->execute($command, 0); + if (!defined($output)) { + notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name: $command"); + return; + } + elsif ($exit_status ne '0') { + notify($ERRORS{'WARNING'}, 0, "failed to delete rule on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); + return; + } + else { + notify($ERRORS{'OK'}, 0, "deleted rule on $computer_name with specification: '$rule_specification_string'"); + } + } + return 1; +} + +#///////////////////////////////////////////////////////////////////////////// + +=head2 delete_connect_method_rules + + Parameters : none + Returns : boolean + Description : Deletes all rules from the INPUT chain in the filter table + matching any connect method ports. + +=cut + +sub delete_connect_method_rules { + my $self = shift; + if (ref($self) !~ /VCL::Module/i) { + notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } - else { - notify($ERRORS{'OK'}, 0, "deleted iptables rule on $computer_name, command: $command"); - return 1; + + my @protocol_ports = $self->data->get_connect_method_protocol_port_array(); + for my $protocol_port (@protocol_ports) { + my ($protocol, $port) = @$protocol_port; + $self->delete_rules('filter', 'INPUT', + { + 'parameters' => { + 'protocol' => $protocol, + }, + 'match_extensions' => { + $protocol => { + 'dport' => $port, + }, + }, + } + ); } + + notify($ERRORS{'DEBUG'}, 0, "deleted explicit rules from INPUT chain in filter table for all connect method ports"); + return 1; } #///////////////////////////////////////////////////////////////////////////// @@ -399,9 +1005,9 @@ sub create_chain { my $computer_name = $self->data->get_computer_hostname(); - my $command = "/sbin/iptables --new-chain $chain_name --table $table_name"; - my $semaphore = $self->get_iptables_semaphore(); + + my $command = "/sbin/iptables --new-chain $chain_name --table $table_name"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command"); @@ -427,7 +1033,7 @@ sub create_chain { Parameters : $table_name, $chain_name Returns : boolean - Description : Deletes the specified chain from the specified table. All rules + Description : Deletes the specified chain from the table. All rules which exist in the chain or reference the chain are deleted prior to deletion of the chain. @@ -494,78 +1100,118 @@ sub delete_chain { #///////////////////////////////////////////////////////////////////////////// -=head2 sanitize_reservation +=head2 delete_chain_references - Parameters : $reservation_id (optional) + Parameters : $table_name, $chain_name Returns : boolean - Description : Deletes the chains created for the reservation. Saves the - iptables configuration. + Description : Checks all chains in the specified table for references to the + $chain_name argument. If found, the referencing rules are + deleted. =cut -sub sanitize_reservation { +sub delete_chain_references { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } - my $reservation_id = shift || $self->data->get_reservation_id(); - my $reservation_chain_name = $self->get_reservation_chain_name($reservation_id); - - if (!$self->delete_chain('nat', $reservation_chain_name)) { + my ($table_name, $chain_name) = @_; + if (!defined($table_name)) { + notify($ERRORS{'WARNING'}, 0, "table name argument was not specified"); + return; + } + elsif (!defined($chain_name)) { + notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified"); return; } - $self->save_configuration(); + my $computer_name = $self->data->get_computer_hostname(); + + my $table_info = $self->get_table_info($table_name); + for my $referencing_chain_name (keys %$table_info) { + for my $rule (@{$table_info->{$referencing_chain_name}{rules}}) { + my $rule_specification_string = $rule->{rule_specification}; + if ($rule_specification_string =~ /-j $chain_name(\s|$)/) { + notify($ERRORS{'DEBUG'}, 0, "rule in '$table_name' table references '$chain_name' chain, referencing chain: $referencing_chain_name, rule specification: $rule_specification_string"); + if (!$self->delete_rules($table_name, $referencing_chain_name, {'rule_specification' => $rule_specification_string})) { + return; + } + } + } + } + + notify($ERRORS{'DEBUG'}, 0, "deleted all rules in '$table_name' table referencing '$chain_name' chain on $computer_name"); return 1; } #///////////////////////////////////////////////////////////////////////////// -=head2 delete_chain_references +=head2 chain_exists - Parameters : $table_name, $referenced_chain_name + Parameters : $table_name, $chain_name Returns : boolean - Description : Checks all chains in the specified table for references to the - $referenced_chain_name argument. If found, the referencing rules - are deleted. + Description : Determines if an iptables chain exists in the table specified. =cut -sub delete_chain_references { +sub chain_exists { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } - my ($table_name, $referenced_chain_name) = @_; + my ($table_name, $chain_name) = @_; if (!defined($table_name)) { notify($ERRORS{'WARNING'}, 0, "table name argument was not specified"); return; } - elsif (!defined($referenced_chain_name)) { - notify($ERRORS{'WARNING'}, 0, "referenced chain name argument was not specified"); + elsif (!defined($chain_name)) { + notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified"); return; } my $computer_name = $self->data->get_computer_hostname(); - my $table_info = $self->get_table_info($table_name); - for my $referencing_chain_name (keys %$table_info) { - for my $rule (@{$table_info->{$referencing_chain_name}{rules}}) { - my $rule_specification = $rule->{rule_specification}; - if ($rule_specification =~ /-j $referenced_chain_name(\s|$)/) { - notify($ERRORS{'DEBUG'}, 0, "rule in '$table_name' table references '$referenced_chain_name' chain, referencing chain: $referencing_chain_name, rule specification: $rule_specification"); - if (!$self->delete_rule($table_name, $referencing_chain_name, $rule_specification)) { - return; - } - } - } + my $table_info = $self->get_table_info($table_name) || return; + if (defined($table_info->{$chain_name})) { + notify($ERRORS{'DEBUG'}, 0, "$chain_name chain exists in $table_name table on $computer_name"); + return 1; + } + else { + notify($ERRORS{'DEBUG'}, 0, "'$chain_name' chain does NOT exist in '$table_name' table on $computer_name"); + return 0; + } +} + +#///////////////////////////////////////////////////////////////////////////// + +=head2 sanitize_nat_reservation + + Parameters : $reservation_id (optional) + Returns : boolean + Description : Deletes the chains created for a reservation on a NAT host. Saves + the iptables configuration. + +=cut + +sub sanitize_nat_reservation { + my $self = shift; + if (ref($self) !~ /VCL::Module/i) { + notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); + return 0; } - notify($ERRORS{'DEBUG'}, 0, "deleted all rules in '$table_name' table referencing '$referenced_chain_name' chain on $computer_name"); + my $reservation_id = shift || $self->data->get_reservation_id(); + my $reservation_chain_name = $self->get_reservation_chain_name($reservation_id); + + if (!$self->delete_chain('nat', $reservation_chain_name)) { + return; + } + + $self->save_configuration(); return 1; } @@ -626,11 +1272,12 @@ sub flush_chain { =head2 get_table_info - Parameters : $table_name, $chain_name (optional) + Parameters : $table_name (optional) Returns : boolean Description : Retrieves the configuration of an iptables table and constructs a - hash reference. Example: - { + hash reference. Information from the 'filter' table is returned + if the $table_name argument is not specified. Example: + { "OUTPUT" => { "policy" => "ACCEPT" }, @@ -681,23 +1328,13 @@ sub get_table_info { return 0; } - my ($table_name, $chain_name) = @_; - if (!defined($table_name)) { - notify($ERRORS{'WARNING'}, 0, "table name argument was not specified"); - return; - } + my $table_name = shift || 'filter'; $ENV{iptables_get_table_info_count}{$table_name}++; my $computer_name = $self->data->get_computer_hostname(); - my $command = "/sbin/iptables --list-rules"; - my $chain_text = ''; - if (defined($chain_name)) { - $command .= " $chain_name"; - $chain_text = "of '$chain_name' chain "; - } - $command .= " --table $table_name"; + my $command = "/sbin/iptables --list-rules --table $table_name"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { @@ -705,7 +1342,7 @@ sub get_table_info { return; } elsif ($exit_status ne '0') { - notify($ERRORS{'WARNING'}, 0, "failed to list rules " . $chain_text . "from '$table_name' table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); + notify($ERRORS{'WARNING'}, 0, "failed to list rules from '$table_name' table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return 0; } @@ -795,6 +1432,9 @@ sub get_table_info { 'goto' => '\s*(-g|--goto)\s+([^\s]+)\s*(.*)', }; + # -j ACCEPT + # -j REJECT --reject-with icmp-host-prohibited + # Parse the parameters which specify targets TARGET_PARAMETER: for my $target_parameter (keys %$target_parameters) { my $pattern = $target_parameters->{$target_parameter}; @@ -804,7 +1444,7 @@ sub get_table_info { # Assemble a regex to remove the target specification from the overall specification my $target_parameter_regex = "\\s*$target_parameter_match\\s+$target\\s*"; - $rule->{parameters}{$target_parameter}{target} = $target; + $rule->{parameters}{$target_parameter} = $target; my $target_extension_option_name; my @target_extension_option_sections = split(/\s+/, $target_extension_option_string); @@ -817,8 +1457,8 @@ sub get_table_info { # Check if this is the beginning of a target extension option if ($target_extension_option_section =~ /^[-]+(\w[\w-]+)/) { $target_extension_option_name = $1; - #notify($ERRORS{'DEBUG'}, 0, "located $target_parameter target extension option: $target_extension_option_name"); - $rule->{parameters}{$target_parameter}{$target_extension_option_name} = undef; + #notify($ERRORS{'DEBUG'}, 0, "located $target_parameter/$target target extension option: $target_extension_option_name"); + $rule->{target_extensions}{$target}{$target_extension_option_name} = undef; } elsif (!$target_extension_option_name) { # If here, the section should be a target extension option value @@ -830,7 +1470,7 @@ sub get_table_info { } else { # Found target extension option value - $rule->{parameters}{$target_parameter}{$target_extension_option_name} = $target_extension_option_section; + $rule->{target_extensions}{$target}{$target_extension_option_name} = $target_extension_option_section; $target_extension_option_name = undef; } @@ -939,14 +1579,14 @@ sub get_table_info { } } - #notify($ERRORS{'DEBUG'}, 0, "retrieved rules " . $chain_text . "from iptables $table_name table from $computer_name:\n" . format_data($table_info)); + #notify($ERRORS{'DEBUG'}, 0, "retrieved rules from iptables $table_name table from $computer_name:\n" . format_data($table_info)); return $table_info; } #///////////////////////////////////////////////////////////////////////////// -=head2 configure_nat +=head2 nat_configure_host Parameters : $public_ip_address, $internal_ip_address Returns : boolean @@ -954,7 +1594,7 @@ sub get_table_info { =cut -sub configure_nat { +sub nat_configure_host { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); @@ -995,9 +1635,9 @@ sub configure_nat { # Check if NAT has previously been configured for my $rule (@{$nat_table_info->{POSTROUTING}{rules}}) { - my $rule_specification = $rule->{rule_specification}; - if ($rule_specification =~ /MASQUERADE/) { - notify($ERRORS{'DEBUG'}, 0, "POSTROUTING chain in nat table contains a MASQUERADE rule, assuming NAT has already been configured: $rule_specification"); + my $rule_specification_string = $rule->{rule_specification}; + if ($rule_specification_string =~ /MASQUERADE/) { + notify($ERRORS{'DEBUG'}, 0, "POSTROUTING chain in nat table contains a MASQUERADE rule, assuming NAT has already been configured: $rule_specification_string"); return 1; } } @@ -1051,98 +1691,102 @@ sub configure_nat { $destination_ports .= "$start_port:$end_port"; } - if (!$self->insert_rule({ - 'table' => 'nat', - 'chain' => 'POSTROUTING', - 'parameters' => { - 'out-interface' => $public_interface_name, - '!destination' => "$internal_network_address/$internal_network_bits", - 'jump' => 'MASQUERADE', - }, - 'match_extensions' => { - 'comment' => { - 'comment' => "change IP of outbound $public_interface_name packets to NAT host IP address $public_ip_address", - }, - }, - })) { - return; - } - - if (!$self->insert_rule({ - 'chain' => 'INPUT', - 'parameters' => { - 'in-interface' => $public_interface_name, - 'destination' => $public_ip_address, - 'jump' => 'ACCEPT', - 'protocol' => 'tcp', - }, - 'match_extensions' => { - 'state' => { - 'state' => 'NEW,RELATED,ESTABLISHED', - }, - 'multiport' => { - 'destination-ports' => $destination_ports, - }, - }, - })) { - return; - } - - if (!$self->insert_rule({ - 'chain' => 'INPUT', - 'parameters' => { - 'in-interface' => $public_interface_name, - 'destination' => $public_ip_address, - 'jump' => 'ACCEPT', - 'protocol' => 'udp', - }, - 'match_extensions' => { - 'state' => { - 'state' => 'NEW,RELATED,ESTABLISHED', - }, - 'multiport' => { - 'destination-ports' => $destination_ports, - }, - }, - })) { - return; - } - - if (!$self->insert_rule({ - 'chain' => 'FORWARD', - 'parameters' => { - 'in-interface' => $public_interface_name, - 'out-interface' => $internal_interface_name, - 'jump' => 'ACCEPT', - }, - 'match_extensions' => { - 'state' => { - 'state' => 'NEW,RELATED,ESTABLISHED', - }, - 'comment' => { - 'comment' => "forward inbound packets from public $public_interface_name to internal $internal_interface_name", - }, - }, - })) { - return; - } - - if (!$self->insert_rule({ - 'chain' => 'FORWARD', - 'parameters' => { - 'in-interface' => $internal_interface_name, - 'out-interface' => $public_interface_name, - 'jump' => 'ACCEPT', - }, - 'match_extensions' => { - 'state' => { - 'state' => 'NEW,RELATED,ESTABLISHED', + if (!$self->insert_rule('nat', 'POSTROUTING', + { + 'parameters' => { + 'out-interface' => $public_interface_name, + '!destination' => "$internal_network_address/$internal_network_bits", + 'jump' => 'MASQUERADE', + }, + 'match_extensions' => { + 'comment' => { + 'comment' => "change IP of outbound $public_interface_name packets to NAT host IP address $public_ip_address", + }, + }, + } + )) { + return; + } + + if (!$self->insert_rule('filter', 'INPUT', + { + 'parameters' => { + 'in-interface' => $public_interface_name, + 'destination' => $public_ip_address, + 'jump' => 'ACCEPT', + 'protocol' => 'tcp', + }, + 'match_extensions' => { + 'state' => { + 'state' => 'NEW,RELATED,ESTABLISHED', + }, + 'multiport' => { + 'destination-ports' => $destination_ports, + }, + }, + } + )) { + return; + } + + if (!$self->insert_rule('filter', 'INPUT', + { + 'parameters' => { + 'in-interface' => $public_interface_name, + 'destination' => $public_ip_address, + 'jump' => 'ACCEPT', + 'protocol' => 'udp', }, - 'comment' => { - 'comment' => "forward outbound packets from internal $internal_interface_name to public $public_interface_name", + 'match_extensions' => { + 'state' => { + 'state' => 'NEW,RELATED,ESTABLISHED', + }, + 'multiport' => { + 'destination-ports' => $destination_ports, + }, + }, + } + )) { + return; + } + + if (!$self->insert_rule('filter', 'FORWARD', + { + 'parameters' => { + 'in-interface' => $public_interface_name, + 'out-interface' => $internal_interface_name, + 'jump' => 'ACCEPT', }, - }, - })) { + 'match_extensions' => { + 'state' => { + 'state' => 'NEW,RELATED,ESTABLISHED', + }, + 'comment' => { + 'comment' => "forward inbound packets from public $public_interface_name to internal $internal_interface_name", + }, + }, + } + )) { + return; + } + + if (!$self->insert_rule('filter', 'FORWARD', + { + 'parameters' => { + 'in-interface' => $internal_interface_name, + 'out-interface' => $public_interface_name, + 'jump' => 'ACCEPT', + }, + 'match_extensions' => { + 'state' => { + 'state' => 'NEW,RELATED,ESTABLISHED', + }, + 'comment' => { + 'comment' => "forward outbound packets from internal $internal_interface_name to public $public_interface_name", + }, + }, + } + )) { return; } @@ -1152,7 +1796,7 @@ sub configure_nat { #///////////////////////////////////////////////////////////////////////////// -=head2 configure_nat_reservation +=head2 nat_configure_reservation Parameters : none Returns : boolean @@ -1162,7 +1806,7 @@ sub configure_nat { =cut -sub configure_nat_reservation { +sub nat_configure_reservation { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); @@ -1191,21 +1835,21 @@ sub configure_nat_reservation { # Check if rule to jump to reservation's chain already exists in the PREROUTING table for my $rule (@{$nat_table_info->{PREROUTING}{rules}}) { - my $rule_specification = $rule->{rule_specification}; - if ($rule_specification =~ /-j $chain_name(\s|$)/) { - notify($ERRORS{'DEBUG'}, 0, "PREROUTING chain in nat table on $computer_name already contains a rule to jump to '$chain_name' chain: $rule_specification"); + my $rule_specification_string = $rule->{rule_specification}; + if ($rule_specification_string =~ /-j $chain_name(\s|$)/) { + notify($ERRORS{'DEBUG'}, 0, "PREROUTING chain in nat table on $computer_name already contains a rule to jump to '$chain_name' chain: $rule_specification_string"); return 1;; } } # Add a rule to the nat PREROUTING chain - if (!$self->insert_rule({ - 'table' => 'nat', - 'chain' => 'PREROUTING', - 'parameters' => { - 'jump' => $chain_name, - }, - })) { + if (!$self->insert_rule('nat', 'PREROUTING', + { + 'parameters' => { + 'jump' => $chain_name, + }, + } + )) { notify($ERRORS{'WARNING'}, 0, "failed to configure NAT host $computer_name for reservation, failed to create rule in PREROUTING chain in nat table to jump to '$chain_name' chain"); return; } @@ -1216,15 +1860,15 @@ sub configure_nat_reservation { #///////////////////////////////////////////////////////////////////////////// -=head2 add_nat_port_forward +=head2 nat_add_port_forward - Parameters : $protocol, $source_port, $destination_ip_address, $destination_port, $chain_name (optional) + Parameters : $protocol, $source_port, $destination_ip_address, $destination_port Returns : boolean Description : Forwards a port via DNAT. =cut -sub add_nat_port_forward { +sub nat_add_port_forward { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); @@ -1233,7 +1877,7 @@ sub add_nat_port_forward { my $computer_name = $self->data->get_computer_hostname(); - my ($protocol, $source_port, $destination_ip_address, $destination_port, $chain_name) = @_; + my ($protocol, $source_port, $destination_ip_address, $destination_port) = @_; if (!defined($protocol)) { notify($ERRORS{'WARNING'}, 0, "protocol argument was not provided"); return; @@ -1250,7 +1894,8 @@ sub add_nat_port_forward { notify($ERRORS{'WARNING'}, 0, "destination port argument was not provided"); return; } - $chain_name = $self->get_reservation_chain_name() unless defined $chain_name; + + my $chain_name = $self->get_reservation_chain_name(); $protocol = lc($protocol); @@ -1264,56 +1909,56 @@ sub add_nat_port_forward { # Check if rule has previously been added for my $rule (@{$nat_table_info->{$chain_name}{rules}}) { - my $rule_target = $rule->{parameters}{jump}{target} || '<not set>'; + my $rule_target = $rule->{parameters}{jump} || '<not set>'; if ($rule_target ne 'DNAT') { - #notify($ERRORS{'DEBUG'}, 0, "ignoring rule, target is not DNAT: $rule_target"); + notify($ERRORS{'DEBUG'}, 0, "ignoring rule, target is not DNAT: $rule_target"); next; } my $rule_protocol = $rule->{parameters}{protocol} || '<not set>'; if (lc($rule_protocol) ne $protocol) { - #notify($ERRORS{'DEBUG'}, 0, "ignoring rule, protocol '$rule_protocol' does not match protocol argument: '$protocol'"); + notify($ERRORS{'DEBUG'}, 0, "ignoring rule, protocol '$rule_protocol' does not match protocol argument: '$protocol'"); next; } my $rule_source_port = $rule->{match_extensions}{$protocol}{dport} || '<not set>'; if ($rule_source_port ne $source_port) { - #notify($ERRORS{'DEBUG'}, 0, "ignoring rule, source port $rule_source_port does not match argument: $source_port"); + notify($ERRORS{'DEBUG'}, 0, "ignoring rule, source port $rule_source_port does not match argument: $source_port"); next; } - my $rule_destination = $rule->{parameters}{jump}{'to-destination'} || '<not set>'; + my $rule_destination = $rule->{target_extensions}{DNAT}{'to-destination'} || '<not set>'; if ($rule_destination ne "$destination_ip_address:$destination_port") { - #notify($ERRORS{'DEBUG'}, 0, "ignoring rule, destination $rule_destination does not match argument: $destination_ip_address:$destination_port"); + notify($ERRORS{'DEBUG'}, 0, "ignoring rule, destination $rule_destination does not match argument: $destination_ip_address:$destination_port"); next; } - my $rule_specification = $rule->{'rule_specification'}; - notify($ERRORS{'DEBUG'}, 0, "NAT port forwared rule already exists, chain: $chain_name, protocol: $protocol, source port: $source_port, destination: $destination_ip_address:$destination_port\nrule specification:\n$rule_specification"); + my $rule_specification_string = $rule->{'rule_specification'}; + notify($ERRORS{'DEBUG'}, 0, "NAT port forwared rule already exists, chain: $chain_name, protocol: $protocol, source port: $source_port, destination: $destination_ip_address:$destination_port\nrule specification:\n$rule_specification_string"); return 1; } - if ($self->insert_rule({ - 'table' => 'nat', - 'chain' => $chain_name, - 'parameters' => { - 'protocol' => $protocol, - 'in-interface' => $public_interface_name, - }, - 'match_extensions' => { - 'comment' => { - 'comment' => "forward: $public_interface_name:$source_port --> $destination_ip_address:$destination_port ($protocol)", - }, - $protocol => { - 'destination-port' => $source_port, - }, - }, - 'target_extensions' => { - 'DNAT' => { - 'to-destination' => "$destination_ip_address:$destination_port", + if ($self->insert_rule('nat', $chain_name, + { + 'parameters' => { + 'protocol' => $protocol, + 'in-interface' => $public_interface_name, + }, + 'match_extensions' => { + 'comment' => { + 'comment' => "forward: $public_interface_name:$source_port --> $destination_ip_address:$destination_port ($protocol)", + }, + $protocol => { + 'destination-port' => $source_port, + }, }, - }, - })) { + 'target_extensions' => { + 'DNAT' => { + 'to-destination' => "$destination_ip_address:$destination_port", + }, + }, + } + )) { notify($ERRORS{'OK'}, 0, "added NAT port forward on $computer_name: $public_interface_name:$source_port --> $destination_ip_address:$destination_port"); return 1; } @@ -1325,28 +1970,6 @@ sub add_nat_port_forward { #///////////////////////////////////////////////////////////////////////////// -=head2 get_reservation_chain_name - - Parameters : $reservation_id (optional) - Returns : string - Description : Returns the name of the iptables chain containing rules for a - single VCL reservation. - -=cut - -sub get_reservation_chain_name { - my $self = shift; - if (ref($self) !~ /VCL::Module/i) { - notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); - return 0; - } - - my $reservation_id = shift || $self->data->get_reservation_id(); - return "$PROCESSNAME-$reservation_id"; -} - -#///////////////////////////////////////////////////////////////////////////// - =head2 save_configuration Parameters : $file_path (optional) @@ -1383,8 +2006,6 @@ sub save_configuration { return 0; } - my $file_exists = $self->os->file_exists($file_path); - # Make sure output contains at least 1 line beginning with "-A" # If the iptables service is stopped the output will be blank # If the iptables service is stopped but "iptables -L" is executed the output may contain something like: @@ -1399,31 +2020,115 @@ sub save_configuration { notify($ERRORS{'WARNING'}, 0, "failed to save iptables configuration on $computer_name, iptables service may not be running, no output was returned from $command"); return 0; } - elsif (!grep(/^-A/, @$output) && ($file_exists || $file_path eq '/etc/sysconfig/iptables')) { + elsif (!grep(/^-A/, @$output)) { notify($ERRORS{'WARNING'}, 0, "iptables configuration not saved to $file_path on $computer_name for safety, iptables service may not be running, output of $command does not contain any lines beginning with '-A':\n" . join("\n", @$output)); return 0; } - # Attempt to get a semaphore if the file already exists - my $semaphore; - if ($file_exists) { - $semaphore = $self->get_semaphore("iptables-save_configuration-$computer_id", (30 * 1)); - if (!$semaphore) { - notify($ERRORS{'WARNING'}, 0, "failed to save iptables configuration on $computer_name, $file_path already exists and semaphore could not be obtained to avoid multiple processes writing to the file at the same time"); - return; - } + my $semaphore = $self->get_iptables_semaphore(); + if (!$semaphore) { + notify($ERRORS{'WARNING'}, 0, "failed to save iptables configuration on $computer_name, $file_path already exists and semaphore could not be obtained to avoid multiple processes writing to the file at the same time"); + return; } return $self->os->create_text_file($file_path, join("\n", @$output)); } + + + + +#///////////////////////////////////////////////////////////////////////////// + +=head2 get_pre_capture_chain_name + + Parameters : none + Returns : string + Description : Returns 'vcl-pre_capture'. + +=cut + +sub get_pre_capture_chain_name { + return 'vcl-pre_capture'; +} + +#///////////////////////////////////////////////////////////////////////////// + +=head2 get_post_load_chain_name + + Parameters : none + Returns : string + Description : Returns 'vcl-post_load'. + +=cut + +sub get_post_load_chain_name { + return 'vcl-post_load'; +} + +#///////////////////////////////////////////////////////////////////////////// + +=head2 get_reserved_chain_name + + Parameters : none + Returns : string + Description : Returns 'vcl-reserved'. + +=cut + +sub get_reserved_chain_name { + return 'vcl-reserved'; +} + +#///////////////////////////////////////////////////////////////////////////// + +=head2 get_reservation_chain_name + + Parameters : $reservation_id (optional) + Returns : string + Description : Returns the name of the iptables chain containing rules for a + VCL reservation: '<vcld process name>-<reservation ID>' + +=cut + +sub get_reservation_chain_name { + my $self = shift; + if (ref($self) !~ /VCL::Module/i) { + notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); + return 0; + } + + my $reservation_id = shift || $self->data->get_reservation_id(); + return "$PROCESSNAME-$reservation_id"; +} + +#///////////////////////////////////////////////////////////////////////////// + +=head2 get_inuse_chain_name + + Parameters : $reservation_id (optional) + Returns : string + Description : Returns the name of the iptables chain containing rules added + during the inuse state for a VCL reservation. + +=cut + +sub get_inuse_chain_name { + my $self = shift; + if (ref($self) !~ /VCL::Module/i) { + notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); + return 0; + } + return 'vcl-inuse'; +} + #///////////////////////////////////////////////////////////////////////////// =head2 DESTROY Parameters : none Returns : true - Description : + Description : Prints the number of calls to get_table_info. =cut
Modified: vcl/trunk/managementnode/lib/VCL/Module/Provisioning.pm URL: http://svn.apache.org/viewvc/vcl/trunk/managementnode/lib/VCL/Module/Provisioning.pm?rev=1789589&r1=1789588&r2=1789589&view=diff ============================================================================== --- vcl/trunk/managementnode/lib/VCL/Module/Provisioning.pm (original) +++ vcl/trunk/managementnode/lib/VCL/Module/Provisioning.pm Thu Mar 30 21:46:02 2017 @@ -421,12 +421,16 @@ sub retrieve_image { } # Copy each file path to the image repository directory + my $total_file_count = scalar(keys %{$partner_info{$retrieval_partner}{file_paths}}); + my $file_number = 0; for my $partner_file_path (sort {lc($a) cmp lc($b)} keys %{$partner_info{$retrieval_partner}{file_paths}}) { + $file_number++; my $file_name = $partner_info{$retrieval_partner}{file_paths}{$partner_file_path}{file_name}; my $local_file_path = "$image_repository_path_local/$file_name"; + notify($ERRORS{'DEBUG'}, 0, "file $file_number/$total_file_count: retrieving image from $retrieval_partner_hostname: $partner_file_path -->"); if (run_scp_command("$partner_info{$retrieval_partner}{user}\@$retrieval_partner:$partner_file_path", $local_file_path, $partner_info{$retrieval_partner}{key}, $partner_info{$retrieval_partner}{port})) { - notify($ERRORS{'OK'}, 0, "image $image_name was copied from $retrieval_partner_hostname"); + notify($ERRORS{'OK'}, 0, "file $file_number/$total_file_count: retrieved image from $retrieval_partner_hostname: --> $local_file_path"); } else { notify($ERRORS{'WARNING'}, 0, "failed to copy image $image_name from $retrieval_partner_hostname"); Modified: vcl/trunk/managementnode/lib/VCL/reclaim.pm URL: http://svn.apache.org/viewvc/vcl/trunk/managementnode/lib/VCL/reclaim.pm?rev=1789589&r1=1789588&r2=1789589&view=diff ============================================================================== --- vcl/trunk/managementnode/lib/VCL/reclaim.pm (original) +++ vcl/trunk/managementnode/lib/VCL/reclaim.pm Thu Mar 30 21:46:02 2017 @@ -131,13 +131,13 @@ sub process { if ($self->nathost_os(0)) { my $nathost_hostname = $self->data->get_nathost_hostname(); if ($self->nathost_os->firewall()) { - if ($self->nathost_os->firewall->can('sanitize_reservation')) { - if (!$self->nathost_os->firewall->sanitize_reservation()) { + if ($self->nathost_os->firewall->can('sanitize_nat_reservation')) { + if (!$self->nathost_os->firewall->sanitize_nat_reservation()) { notify($ERRORS{'CRITICAL'}, 0, "failed to sanitize firewall for reservation on NAT host $nathost_hostname"); } } else { - notify($ERRORS{'WARNING'}, 0, "unable to sanitize firewall for reservation on NAT host $nathost_hostname, " . ref($self->nathost_os->firewall) . " does not implement a 'sanitize_reservation' subroutine"); + notify($ERRORS{'WARNING'}, 0, "unable to sanitize firewall for reservation on NAT host $nathost_hostname, " . ref($self->nathost_os->firewall) . " does not implement a 'sanitize_nat_reservation' subroutine"); } } @@ -277,7 +277,7 @@ sub insert_reload_and_exit { notify($ERRORS{'WARNING'}, 0, "predictor module did not return required information, calling get_next_image_default from utils"); ($next_image_name, $next_image_id, $next_imagerevision_id) = get_next_image_default($computer_id); } - + # Update the DataStructure object with the next image values # These will be used by insert_reload_request() $self->data->set_image_name($next_image_name); Modified: vcl/trunk/managementnode/lib/VCL/reserved.pm URL: http://svn.apache.org/viewvc/vcl/trunk/managementnode/lib/VCL/reserved.pm?rev=1789589&r1=1789588&r2=1789589&view=diff ============================================================================== --- vcl/trunk/managementnode/lib/VCL/reserved.pm (original) +++ vcl/trunk/managementnode/lib/VCL/reserved.pm Thu Mar 30 21:46:02 2017 @@ -270,8 +270,13 @@ sub process { # Tighten up the firewall # Process the connect methods again, lock the firewall down to the address the user connected from my $remote_ip = $self->data->get_reservation_remote_ip(); - if (!$self->os->process_connect_methods($remote_ip, 1)) { - notify($ERRORS{'CRITICAL'}, 0, "failed to process connect methods after user connected to computer"); + if ($self->os->can('firewall') && $self->os->firewall->can('process_inuse')) { + $self->os->firewall->process_inuse($remote_ip); + } + else { + if (!$self->os->process_connect_methods($remote_ip, 1)) { + notify($ERRORS{'CRITICAL'}, 0, "failed to process connect methods after user connected to computer"); + } } # Run custom post_initial_connection scripts residing on the management node Modified: vcl/trunk/managementnode/lib/VCL/utils.pm URL: http://svn.apache.org/viewvc/vcl/trunk/managementnode/lib/VCL/utils.pm?rev=1789589&r1=1789588&r2=1789589&view=diff ============================================================================== --- vcl/trunk/managementnode/lib/VCL/utils.pm (original) +++ vcl/trunk/managementnode/lib/VCL/utils.pm Thu Mar 30 21:46:02 2017 @@ -129,6 +129,7 @@ our @EXPORT = qw( get_database_table_names get_code_ref_package_name get_code_ref_subroutine_name + get_collapsed_hash_reference get_computer_current_private_ip_address get_computer_current_state_name get_computer_grp_members @@ -14937,6 +14938,79 @@ EOF } #///////////////////////////////////////////////////////////////////////////// + +=head2 get_collapsed_hash_reference + + Parameters : $hash_reference + Returns : array + Description : Takes a potentially multi-level hash reference and generates a + new single-level hash with key names constructed by concatenating + the levels of key names. Example: + Argument: + { + "match_extensions" => { + "tcp" => { + "dport" => 22, + }, + }, + "parameters" => { + "jump" => { + "target" => "ACCEPT", + }, + "protocol" => "tcp", + }, + } + + Result: + { + "{'match_extensions'}{'tcp'}{'dport'}" => 22, + "{'parameters'}{'jump'}{'target'}" => "ACCEPT", + "{'parameters'}{'protocol'}" => "tcp" + } + + This is potentially useful when 2 multi-level hashes need to be + compared. The hash keys in the resultant hash can be used in an + eval block to check if another hash has a key with the same name + and/or value. + +=cut + +sub get_collapsed_hash_reference { + my ($hash_reference, @parent_keys) = @_; + if (!defined($hash_reference)) { + notify($ERRORS{'WARNING'}, 0, "hash reference argument was not specified"); + return; + } + elsif (!ref($hash_reference) || ref($hash_reference) ne 'HASH') { + notify($ERRORS{'WARNING'}, 0, "argument is not a hash reference:\n" . format_data($hash_reference)); + return; + } + + my %collapsed_hash; + + for my $key (keys %$hash_reference) { + if (ref($hash_reference->{$key})) { + if (ref($hash_reference->{$key}) eq 'HASH') { + my $child_collapsed_hash_ref = get_collapsed_hash_reference($hash_reference->{$key}, (@parent_keys, $key)) || {}; + %collapsed_hash = (%collapsed_hash, %$child_collapsed_hash_ref); + } + } + else { + my $value = $hash_reference->{$key}; + + my $key_path; + for my $parent_key (@parent_keys) { + $key_path .= "{'$parent_key'}"; + } + $key_path .= "{'$key'}"; + + $collapsed_hash{$key_path} = $value; + } + } + return \%collapsed_hash; +} + +#///////////////////////////////////////////////////////////////////////////// 1;
