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;
}