From d1e4c5d5777d15a1b82c2368b1cfba1a862bda76 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Mon, 20 Jan 2025 01:16:05 +0200 Subject: [PATCH] Add API to handle direct rules in FirewallD --- firewalld/firewalld-lib.pl | 191 +++++++++++++++++++++++++++++++++---- firewalld/lang/en | 7 ++ 2 files changed, 177 insertions(+), 21 deletions(-) diff --git a/firewalld/firewalld-lib.pl b/firewalld/firewalld-lib.pl index be59feaa0..420ccc21c 100644 --- a/firewalld/firewalld-lib.pl +++ b/firewalld/firewalld-lib.pl @@ -20,6 +20,14 @@ sub check_firewalld return undef; } +# get_config_files() +# Returns a list of all firewalld config files +sub get_config_files +{ +my $conf_dir = $config{'config_dir'} || '/etc/firewalld'; +return (glob("$conf_dir/*.xml"), glob("$conf_dir/*/*.xml")); +} + # check_ip_family() # Determines which IP families are enabled and functional on the system # @@ -479,7 +487,7 @@ foreach my $ip_key ('source address', 'destination address') { if (defined($cidr)) { # Make sure CIDR is numeric and within range $cidr =~ /^\d+$/ && $cidr <= ($family eq 'ipv6' ? 128 : 32) || - &error("$text{'list_rule_cidrerr'} : /$cidr"); + &error("$text{'save_rule_cidrerr'} : /$cidr"); } } } @@ -590,33 +598,174 @@ return &rich_rule('remove', { 'zone' => $zone->{'name'}, 'permanent' => 1, 'rule' => $rule }); } -# remove_direct_rule(rule) -# Remove given direct rule -sub remove_direct_rule +# construct_direct_rule(&opts) +# Constructs a direct Firewalld rule string +# +# Opts can include: +# 'family' => 'ipv4' | 'ipv6' | 'eb' (default = 'ipv4') +# 'table' => 'filter' | 'nat' | 'mangle' | 'raw' | +# 'security' (default = 'filter') +# 'chain' => 'INPUT' | 'OUTPUT' | 'FORWARD' | +# 'PREROUTING' | 'POSTROUTING' (default = 'INPUT') +# 'priority' => integer priority (default = 0) +# 'rule' => string containing iptables-like match/target +# +# Returns: +# A string representing the direct rule is returned +# +sub construct_direct_rule +{ +my ($opts) = @_; + +# Defaults +my $family = $opts->{'family'} || 'ipv4'; +my $table = $opts->{'table'} || 'filter'; +my $chain = $opts->{'chain'} || 'INPUT'; +my $priority = $opts->{'priority'} // 0; +my $rule = $opts->{'rule'} || ''; + +# Basic validation +$family =~ /^(ipv4|ipv6|eb)$/ || + &error(&text('save_rule_efamily', $family)); + +$table =~ /^(filter|nat|mangle|raw|security)$/ || + &error(&text('save_rule_etable', $table)); + +$chain =~ /^(INPUT|OUTPUT|FORWARD|PREROUTING|POSTROUTING)$/ || + &error(&text('save_rule_echain', $chain)); + +# Priority must be integer +$priority =~ /^\d+$/ || &error(&text('save_rule_epriority', $priority)); + +# If still empty after parsing, throw an error +$rule !~ /^\s*$/ || &error(&text('save_rule_erule')); + +# Sanitize rule string by splitting into components and validating each +my @parts = split(/\s+/, $rule); +my $sanitized_rule = ''; +for (my $i = 0; $i < @parts; $i++) { + my $part = $parts[$i]; + next if (!defined($part) || $part eq ''); + + if ($family =~ /^ipv[46]$/ && + $part =~ /^(?:--source|--destination)$/) { + # Get the IP value (next part) + my $ip = $parts[++$i]; + if (defined($ip)) { + # Split IP and CIDR if present + my ($ip_only, $cidr) = split(/\//, $ip); + + # Validate the IP portion + &check_ipaddress($ip_only) || + &check_ip6address($ip_only) || + &error("$text{'list_rule_iperr'} : $ip_only"); + + # Verify IP family matches the rule family + my $ip_family = $ip_only =~ /:/ ? 'ipv6' : 'ipv4'; + $ip_family eq $family || + &error(&text('save_rule_eruleipmismatch')); + + # Validate CIDR if present + if (defined($cidr)) { + # Make sure CIDR is numeric and within range + my $cidr_valid = $family eq 'ipv6' ? 128 : 32; + $cidr =~ /^\d+$/ && $cidr <= ($cidr_valid) || + &error("$text{'save_rule_cidrerr'} : /$cidr"); + } + $sanitized_rule .= ' ' . $part . ' ' . $ip; + } + } + elsif ($family eq 'eb' && $part =~ /^(?:--src-mac|--dst-mac)$/) { + # Get the MAC value (next part) + my $mac = $parts[++$i]; + if (defined($mac)) { + # MAC validation could be added here + $sanitized_rule .= ' ' . $part . ' ' . $mac; + } + } + else { + if ($part =~ /^-/) { + # Options/flags can only contain certain characters + $part =~ tr/A-Za-z0-9\-\_//cd; + } + else { + # Values can contain more characters + $part =~ tr/A-Za-z0-9\-\_\=\.\:\,\/\"\'//cd; + } + $sanitized_rule .= ' ' . $part; + } + } + +# Remove extra possible spaces +$sanitized_rule =~ s/\s+/ /g; + +# Return the constructed rule +return "$family $table $chain $priority $sanitized_rule"; +} + +# direct_rule(action, &opts) +# Add or remove a direct rule +# +# Returns: +# undef on success, or (error_message, error_code) on failure in list context +sub direct_rule +{ +my ($action, $opts) = @_; + +# Validate action +$action eq 'add' || $action eq 'remove' || &error($text{'list_rule_actionerr'}); + +# Extract permanent flag and construct rule +my $permanent = delete($opts->{'permanent'}); + +# Get rule +my $rule = $opts->{'rule'}; +$rule =~ s/\s+/ /g; + +# Add/remove direct rule +my $get_cmd = sub { + my ($perm) = @_; + my $type = $perm ? " --permanent" : ""; + return "$config{'firewall_cmd'}$type --direct --$action-rule $rule"; + }; + +for my $type (0..1) { + next if ($type == 1 && !$permanent); + my $cmd = &$get_cmd($type); + my $out = &backquote_logged($cmd." 2>&1 &1 &1 &1 $rule, 'permanent' => 1 }); +return wantarray ? ($out, $?) : $out if ($?); } 1; diff --git a/firewalld/lang/en b/firewalld/lang/en index fff0ffbb1..944e05354 100644 --- a/firewalld/lang/en +++ b/firewalld/lang/en @@ -127,6 +127,13 @@ list_rules_origin=Origin list_rules_action=Action list_rules_rule=Rule list_rules_plus_more=+ $1 more +save_rule_efamily=Invalid family $1 +save_rule_etable=Invalid table $1 +save_rule_echain=Invalid chain $1 +save_rule_epriority=Invalid priority $1 +save_rule_erule=Empty rule +save_rule_eruleipmismatch=IP version doesn't match rule family +save_rule_cidrerr=Invalid CIDR range log_save_rules=Deleted $1 rules restart_err=Failed to apply configuration