sub get_table_info()

in managementnode/lib/VCL/Module/OS/Linux/firewall/iptables.pm [1539:1905]


sub get_table_info {
	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) = @_;
	$table_name = 'filter' unless $table_name;
	
	$ENV->{iptables_get_table_info_count}->{$table_name}++;
	
	my $computer_name = $self->data->get_computer_hostname();
	
	my @lines;
	
	my $command = "/sbin/iptables --list-rules --table $table_name";
	my ($exit_status, $output) = $self->_execute_iptables($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command");
		return;
	}
	elsif (grep(/Unknown arg/i, @$output)) {
		# Older versions of iptables don't support --list-rules
		# Error output:
		#    iptables v1.3.5: Unknown arg `--list-rules'
		# Try iptables-save
		notify($ERRORS{'DEBUG'}, 0, "version of iptables installed on $computer_name does NOT support the --list-rules option, trying iptables-save");
		
		my $iptables_save_command = "/sbin/iptables-save";
		my ($iptables_save_exit_status, $iptables_save_output) = $self->os->execute($iptables_save_command, 0);
		if (!defined($iptables_save_output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $iptables_save_command");
			return;
		}
		elsif ($iptables_save_exit_status ne '0') {
			notify($ERRORS{'WARNING'}, 0, "failed to list rules from '$table_name' table on $computer_name, iptables does not support the --list-rules option and iptables-save returned exit status: $iptables_save_exit_status, command:\n$iptables_save_command\noutput:\n" . join("\n", @$iptables_save_output));
			return 0;
		}
		else {
			# Extract lines like:
			# -A INPUT -p tcp...
			@lines = grep(/^-[A-Z]\s/, @$iptables_save_output);
			#notify($ERRORS{'DEBUG'}, 0, "parsed iptables-save output for command lines, output:\n" . join("\n", @$iptables_save_output) . "\ncommand lines:\n" . join("\n", @lines));
		}
	}
	elsif ($exit_status ne '0') {
		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;
	}
	else {
		@lines = @$output;
	}
	
	if ($self->can('get_all_direct_rules')) {
		# Convert:
		#    ipv4 filter vcl-pre_capture 0 --jump ACCEPT --protocol tcp --match comment --comment 'VCL: ...' --match tcp --destination-port 22
		#    ipv4 nat POSTROUTING 0 '!' --destination 10.0.0.0/20 --jump MASQUERADE --out-interface eth1 --match comment --comment 'blah... blah'
		# To:
		#    -A vcl-pre_capture -p tcp -m comment --comment "VCL: ..." -m tcp --dport 22 -j ACCEPT
		DIRECT_RULE: for my $direct_rule ($self->get_all_direct_rules()) {
			my ($rule_protocol, $rule_table, $rule_chain, $rule_priority, $rule_specification) = $direct_rule =~
				/^
				(\S+)\s+
				(\S+)\s+
				(\S+)\s+
				(\d+)\s+
				(\S.*)
				$/x
			;
			if (!defined($rule_specification)) {
				notify($ERRORS{'WARNING'}, 0, "failed to parse firewalld direct rule: $direct_rule");
				next DIRECT_RULE;
			}
			elsif ($rule_table ne $table_name) {
				#notify($ERRORS{'DEBUG'}, 0, "ignoring rule, table does not match '$table_name': $direct_rule");
				next DIRECT_RULE;
			}
			
			my $converted_rule = "-A $rule_chain $rule_specification";
			#notify($ERRORS{'DEBUG'}, 0, "converted iptables direct rule to iptables format:\n" .
			#	"direct rule     : $direct_rule\n" .
			#	"iptables format : $converted_rule"
			#);
			push @lines, $converted_rule;
		}
	}
	
	my $table_info = {};
	LINE: for my $line (@lines) {
		# Split the rule, samples:
		#    -P OUTPUT ACCEPT
		#    -N vcld-3115
		#    -A PREROUTING -j vclark-3115
		#    -A POSTROUTING ! -d 192.168.96.0/20 -o eth1
		#    -A INPUT -d 192.168.96.0/32 -i eth1 -p udp -m multiport --dports 5700:6500,9696:9701,49152:65535 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
		my ($iptables_command, $chain_name, $rule_specification_string) = $line =~
		/
			^
			(--?[a-z\-]+)	# command: -A, -N, etc
			\s+				# space after command
			([^ ]+)			# chain name
			\s*				# space after chain name
			(.*)				# remainder of rule
			\s*				# trailing spaces
			$
		/ixg;
		
		if (!defined($iptables_command)) {
			notify($ERRORS{'WARNING'}, 0, "failed to parse iptables rule, iptables command type (ex. '-A') could not be parsed from beginning of line:\n$line");
			next LINE;
		}
		elsif (!defined($chain_name)) {
			notify($ERRORS{'WARNING'}, 0, "failed to parse iptables rule, iptables chain name could not be parsed from line:\n$line");
			next LINE;
		}
		
		# Make sure the rule specification isn't null to avoid warnings
		$rule_specification_string = '' unless defined($rule_specification_string);
		
		# Remove spaces from end of rule specification
		$rule_specification_string =~ s/\s+$//;
		
		#notify($ERRORS{'DEBUG'}, 0, "split iptables line:\n" .
		#	"line          : '$line'\n" .
		#	"command       : '$iptables_command'\n" .
		#	"chain         : '$chain_name'\n" .
		#	"specification : '$rule_specification_string'"
		#);
		
		if ($iptables_command =~ /^(-P|--policy)/) {
			# -P, --policy chain target (Set  the policy for the chain to the given target)
			$table_info->{$chain_name}{policy} = $rule_specification_string;
		}
		elsif ($iptables_command =~ /^(-N|--new-chain)/) {
			# -N, --new-chain chain
			$table_info->{$chain_name} = {} unless defined($table_info->{$chain_name});
		}
		elsif ($iptables_command =~ /^(-A|--append chain)/) {
			# -A, --append chain rule-specification
			#notify($ERRORS{'DEBUG'}, 0, "parsing iptables append rule command:\n" .
			#	"iptables command: $line\n" .
			#	"iptables rule specification: $rule_specification_string"
			#);
			
			my $rule = {};
			$rule->{rule_specification} = $rule_specification_string;
			
			# Parse the rule parameters
			# Be sure to check for ! enclosed in quotes:
			#    -A POSTROUTING '!' --destination 10.10.0.0/20 --jump MASQUERADE
			my $parameters = {
				'protocol'      => '(?:\'?(\!?)\'?\s)?(-p|--protocol)\s+([^\s]+)',
				'source'        => '(?:\'?(\!?)\'?\s)?(-s|--source)\s+([\d\.\/]+)',
				'destination'   => '(?:\'?(\!?)\'?\s)?(-d|--destination)\s+([\d\.\/]+)',
				'in-interface'  => '(?:\'?(\!?)\'?\s)?(-i|--in-interface)\s+([^\s]+)',
				'out-interface' => '(?:\'?(\!?)\'?\s)?(-o|--out-interface)\s+([^\s]+)',
				'fragment'      => '(?:\'?(\!?)\'?\s)?(-f|--fragment)',
			};
			
			PARAMETER: for my $parameter (keys %$parameters) {
				my $pattern = $parameters->{$parameter};
				my ($inverted, $parameter_match, $value) = $rule_specification_string =~ /$pattern/ig;
				next PARAMETER unless $parameter_match;
				
				if ($inverted) {
					$rule->{parameters}{"!$parameter"} = $value;
				}
				else {
					$rule->{parameters}{$parameter} = $value;
				}
				
				# Remove the matching pattern from the rule specification string
				# This is done to make it easier to parse the match extension parts of the specification later on
				my $rule_specification_string_before = $rule_specification_string;
				$rule_specification_string =~ s/(^\s+|$pattern|\s+$)//igx;
				#notify($ERRORS{'DEBUG'}, 0, "trimmed $parameter parameter:\n" .
				#	"before : '$rule_specification_string_before'\n" .
				#	"after  : '$rule_specification_string'"
				#);
			}

			# -j ACCEPT
			# -j REJECT --reject-with icmp-host-prohibited
			# -j LOG --log-prefix "[UFW BLOCK] "
			
			my $target_section_regex = <<'EOF';
(
	(-[jg]|--(?:jump|goto))
	\s+
	([^\s]+)
	(
		(?:
			(?!\s+(?:-m|--match)\s+)
			.
		)*
	)
)
EOF
			my ($target_section_match, $target_parameter_match, $target, $target_extension_option_string) = $rule_specification_string =~ /$target_section_regex/ix;
			if ($target_parameter_match) {
				my $target_parameter_type = ($target_parameter_match =~ /j/ ? 'jump' : 'goto');
				$rule->{parameters}{$target_parameter_type} = $target;
				
				my $target_extension_option_name;
				
				# Need to split line not just by spaces, but also find sections enclosed in quotes:
				#    -j REJECT --reject-with icmp-host-prohibited
				#    -j LOG --log-prefix "IN_public_DROP: "
				my @target_extension_option_sections = $target_extension_option_string =~
				/
					(
						['"][^'"]*['"]
						|
						[^\s]+
					)
				/gx;
				
				TARGET_OPTION_SECTION: for my $target_extension_option_section (@target_extension_option_sections) {
					# 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 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
						notify($ERRORS{'WARNING'}, 0, "failed to parse iptables rule on $computer_name, target extension option name was not detected before this section: '$target_extension_option_section'\n" .
							"output line: $line\n" .
							"target section: $target_section_match"
						);
						next LINE;
					}
					else {
						# Found target extension option value
						$rule->{target_extensions}{$target}{$target_extension_option_name} = $target_extension_option_section;
						$target_extension_option_name = undef;
					}
				}  # TARGET_OPTION_SECTION
				
				my $rule_specification_string_before = $rule_specification_string;
				$rule_specification_string =~ s/(^\s+|$target_section_regex|\s+$)//igx;
				if ($rule_specification_string_before ne $rule_specification_string) {
					#notify($ERRORS{'DEBUG'}, 0, "trimmed $target_parameter_type target section:\n" .
					#	"before : '$rule_specification_string_before'\n" .
					#	"after  : '$rule_specification_string'"
					#);
				}
				else {
					notify($ERRORS{'WARNING'}, 0, "regex failed to remove target section from rule specification:\n" .
						"line                                : $line\n" .
						"remaining rule specification before : $rule_specification_string_before\n" .
						"remaining rule specification after  : $rule_specification_string\n" .
						"target section regex:\n$target_section_regex"
					);
				}
			}
			else {
				notify($ERRORS{'WARNING'}, 0, "target section was not found in rule specification: '$rule_specification_string', line: '$line'");
			}
			
			# The only text remaining in $rule_specification_string should be match extension information
			
			# Make sure space exists between match extension module name (comment) and the option
			# --match comment--comment 'my comment'
			# --match tcp--destination-port
			$rule_specification_string =~ s/(--match [^\s-]+)--/$1 --/g;

			# Split the remaining string by spaces or sections enclosed in quotes
			my @match_extension_sections = $rule_specification_string =~
				/
					(
						['"][^'"]*['"]
						|
						[^\s]+
					)
				/gx;
			
			# Match extensions will be in the form:
			# -m,--match <module> [!] -<x>,--<option> <value> [[!] -<x>,--<option> <value>...]
			my $match_extension_module_name;
			my $match_extension_option;
			my $match_extension_option_inverted = 0;
			my $comment;
			
			MATCH_EXTENSION_SECTION: for my $match_extension_section (@match_extension_sections) {
				next MATCH_EXTENSION_SECTION if !$match_extension_section;
				
				# Check if the section is the beginning of a match extension specification
				if ($match_extension_section =~ /^(-m|--match)$/) {
					$match_extension_module_name = undef;
					$match_extension_option = undef;
					$match_extension_option_inverted = 0;
					next MATCH_EXTENSION_SECTION;
				}
				
				# Parse match extension module name
				if (!$match_extension_module_name) {
					# Haven't found module name for this match extension specification
					# If section begins with a letter it should be the match extension module name
					if ($match_extension_section =~ /^[a-z]/i) {
						$match_extension_module_name = $match_extension_section;
						#notify($ERRORS{'DEBUG'}, 0, "located match extension module name: $match_extension_module_name");
						next MATCH_EXTENSION_SECTION;
					}
					else {
						notify($ERRORS{'WARNING'}, 0, "failed to parse iptables rule in $table_name table on $computer_name\n" .
							"match extension module name was not detected before this section: '$match_extension_section'\n" .
							"iptables rule specification: '$rule_specification_string'\n" .
							"iptables command: '$line'"
						);
						next LINE;
					}
				}
				
				# Check if this is the beginning of a match extension option
				if ($match_extension_section =~ /^[-]+(\w[\w-]+)/) {
					$match_extension_option = $1;
					if ($match_extension_option_inverted) {
						$match_extension_option = "!$match_extension_option";
						$match_extension_option_inverted = 0;
					}
					#notify($ERRORS{'DEBUG'}, 0, "match extension module name: $match_extension_module_name, located match extension option: $match_extension_option");
					next MATCH_EXTENSION_SECTION;
				}
				elsif ($match_extension_section =~ /^!/) {
					$match_extension_option_inverted = 1;
					next MATCH_EXTENSION_SECTION;
				}
				
				# If here, the section should be (part of) a match extension option value
				if (!$match_extension_option) {
					notify($ERRORS{'WARNING'}, 0, "failed to parse iptables rule, match extension option name was not detected before this section: '$match_extension_section'\n" .
						"iptables command: $line\n" .
						"iptables rule specification: $rule_specification_string\n" .
						"preceeding match extension module name: $match_extension_module_name"
					);
					next LINE;
				}
				
				# Check if this is part of a comment
				if ($match_extension_module_name =~ /(comment)/) {
					$comment .= "$match_extension_section ";
					next MATCH_EXTENSION_SECTION;
				}
				
				$rule->{match_extensions}{$match_extension_module_name}{$match_extension_option} = $match_extension_section;
			}
			
			if ($comment) {
				# Remove quotes from beginning and end of comment
				$comment =~ s/(^[\\\"]+|[\s\\\"]+$)//g;
				$rule->{match_extensions}{comment}{comment} = $comment;
				$comment = undef;
			}
			
			push @{$table_info->{$chain_name}{rules}}, $rule;
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "iptables '$iptables_command' command is not supported: $line");
			next LINE;
		}
	}
	
	#notify($ERRORS{'DEBUG'}, 0, "retrieved rules from iptables $table_name table from $computer_name:\n" . format_data($table_info));
	return $table_info;
}