]> git.proxmox.com Git - pve-firewall.git/blobdiff - src/PVE/Firewall.pm
remove a level of indirection on FW config parsing
[pve-firewall.git] / src / PVE / Firewall.pm
index ba1cb5f5924af094fe647f98d40e2703eca96ff4..72d5a69ae50d2e19bd3fe037da44a502000c194b 100644 (file)
@@ -2,6 +2,7 @@ package PVE::Firewall;
 
 use warnings;
 use strict;
+
 use POSIX;
 use Data::Dumper;
 use Digest::SHA;
@@ -41,7 +42,6 @@ eval {
     $have_lxc = 1;
 };
 
-
 my $pve_fw_status_dir = "/var/lib/pve-firewall";
 
 mkdir $pve_fw_status_dir; # make sure this exists
@@ -131,6 +131,7 @@ my $nodename = PVE::INotify::nodename();
 my $pve_fw_lock_filename = "/var/lock/pvefw.lck";
 
 my $default_log_level = 'nolog'; # avoid logs by default
+my $global_log_ratelimit = '--limit 1/sec';
 
 my $log_level_hash = {
     debug => 7,
@@ -143,6 +144,11 @@ my $log_level_hash = {
     emerg => 0,
 };
 
+my $verbose = 0;
+sub set_verbose {
+    $verbose = shift;
+}
+
 # %rule
 #
 # name => optional
@@ -1199,6 +1205,33 @@ our $cluster_option_properties = {
        optional => 1,
        enum => ['ACCEPT', 'REJECT', 'DROP'],
     },
+    log_ratelimit => {
+       description => "Log ratelimiting settings",
+       type => 'string', format => {
+           enable => {
+               default_key => 1,
+               description => 'Enable or disable log rate limiting',
+               type => 'boolean',
+               default => '1',
+           },
+           rate => {
+               type => 'string',
+               description => 'Frequency with which the burst bucket gets refilled',
+               optional => 1,
+               pattern => '[1-9][0-9]*\/(second|minute|hour|day)',
+               format_description => 'rate',
+               default => '1/second',
+           },
+           burst => {
+               type => 'integer',
+               minimum => 0,
+               optional => 1,
+               description => 'Inital burst of packages which will get logged',
+               default => 5,
+           },
+       },
+       optional => 1,
+    },
 };
 
 our $host_option_properties = {
@@ -2103,10 +2136,14 @@ sub get_log_rule_base {
     $vmid = 0 if !defined($vmid);
     $msg = "" if !defined($msg);
 
+    my $rlimit = '';
+    if (defined($global_log_ratelimit)) {
+       $rlimit = "-m limit $global_log_ratelimit ";
+    }
+
     # Note: we use special format for prefix to pass further
     # info to log daemon (VMID, LOGLEVEL and CHAIN)
-
-    return "-m limit --limit 1/sec -j NFLOG --nflog-prefix \":$vmid:$loglevel:$chain: $msg\"";
+    return "${rlimit}-j NFLOG --nflog-prefix \":$vmid:$loglevel:$chain: $msg\"";
 }
 
 sub ruleset_add_chain_policy {
@@ -2536,7 +2573,7 @@ sub get_mark_values {
 }
 
 sub parse_fw_rule {
-    my ($prefix, $line, $cluster_conf, $fw_conf, $rule_env, $verbose) = @_;
+    my ($prefix, $line, $cluster_conf, $fw_conf, $rule_env) = @_;
 
     my $orig_line = $line;
 
@@ -2697,6 +2734,9 @@ sub parse_clusterfw_option {
     } elsif ($line =~ m/^(policy_(in|out)):\s*(ACCEPT|DROP|REJECT)\s*$/i) {
        $opt = lc($1);
        $value = uc($3);
+    } elsif ($line =~ m/^(log_ratelimit):\s*(\S+)\s*$/) {
+       $opt = lc($1);
+       $value = $2;
     } else {
        die "can't parse option '$line'\n"
     }
@@ -2759,7 +2799,10 @@ sub parse_alias {
 }
 
 sub generic_fw_config_parser {
-    my ($filename, $fh, $verbose, $cluster_conf, $empty_conf, $rule_env) = @_;
+    my ($filename, $cluster_conf, $empty_conf, $rule_env) = @_;
+
+    my $fh = IO::File->new($filename, O_RDONLY);
+    return {} if !$fh;
 
     my $section;
     my $group;
@@ -2856,7 +2899,7 @@ sub generic_fw_config_parser {
            warn "$prefix: $@" if $@;
        } elsif ($section eq 'rules') {
            my $rule;
-           eval { $rule = parse_fw_rule($prefix, $line, $cluster_conf, $res, $rule_env, $verbose); };
+           eval { $rule = parse_fw_rule($prefix, $line, $cluster_conf, $res, $rule_env); };
            if (my $err = $@) {
                warn "$prefix: $err";
                next;
@@ -2864,7 +2907,7 @@ sub generic_fw_config_parser {
            push @{$res->{$section}}, $rule;
        } elsif ($section eq 'groups') {
            my $rule;
-           eval { $rule = parse_fw_rule($prefix, $line, $cluster_conf, undef, 'group', $verbose); };
+           eval { $rule = parse_fw_rule($prefix, $line, $cluster_conf, undef, 'group'); };
            if (my $err = $@) {
                warn "$prefix: $err";
                next;
@@ -2921,47 +2964,6 @@ sub generic_fw_config_parser {
     return $res;
 }
 
-sub parse_hostfw_config {
-    my ($filename, $fh, $cluster_conf, $verbose) = @_;
-
-    my $empty_conf = { rules => [], options => {}};
-
-    return generic_fw_config_parser($filename, $fh, $verbose, $cluster_conf, $empty_conf, 'host');
-}
-
-sub parse_vmfw_config {
-    my ($filename, $fh, $cluster_conf, $rule_env, $verbose) = @_;
-
-    my $empty_conf = {
-       rules => [],
-       options => {},
-       aliases => {},
-       ipset => {} ,
-       ipset_comments => {},
-    };
-
-    return generic_fw_config_parser($filename, $fh, $verbose, $cluster_conf, $empty_conf, $rule_env);
-}
-
-sub parse_clusterfw_config {
-    my ($filename, $fh, $verbose) = @_;
-
-    my $section;
-    my $group;
-
-    my $empty_conf = {
-       rules => [],
-       options => {},
-       aliases => {},
-       groups => {},
-       group_comments => {},
-       ipset => {} ,
-       ipset_comments => {},
-    };
-
-    return generic_fw_config_parser($filename, $fh, $verbose, $empty_conf, $empty_conf, 'cluster');
-}
-
 sub run_locked {
     my ($code, @param) = @_;
 
@@ -3011,17 +3013,21 @@ sub read_local_vm_config {
 };
 
 sub load_vmfw_conf {
-    my ($cluster_conf, $rule_env, $vmid, $dir, $verbose) = @_;
-
-    my $vmfw_conf = {};
+    my ($cluster_conf, $rule_env, $vmid, $dir) = @_;
 
     $dir = $pvefw_conf_dir if !defined($dir);
-
     my $filename = "$dir/$vmid.fw";
-    if (my $fh = IO::File->new($filename, O_RDONLY)) {
-       $vmfw_conf = parse_vmfw_config($filename, $fh, $cluster_conf, $rule_env, $verbose);
-       $vmfw_conf->{vmid} = $vmid;
-    }
+
+    my $empty_conf = {
+       rules => [],
+       options => {},
+       aliases => {},
+       ipset => {} ,
+       ipset_comments => {},
+    };
+
+    my $vmfw_conf = generic_fw_config_parser($filename, $cluster_conf, $empty_conf, $rule_env);
+    $vmfw_conf->{vmid} = $vmid;
 
     return $vmfw_conf;
 }
@@ -3183,17 +3189,17 @@ sub clone_vmfw_conf {
 }
 
 sub read_vm_firewall_configs {
-    my ($cluster_conf, $vmdata, $dir, $verbose) = @_;
+    my ($cluster_conf, $vmdata, $dir) = @_;
 
     my $vmfw_configs = {};
 
     foreach my $vmid (keys %{$vmdata->{qemu}}) {
-       my $vmfw_conf = load_vmfw_conf($cluster_conf, 'vm', $vmid, $dir, $verbose);
+       my $vmfw_conf = load_vmfw_conf($cluster_conf, 'vm', $vmid, $dir);
        next if !$vmfw_conf->{options}; # skip if file does not exists
        $vmfw_configs->{$vmid} = $vmfw_conf;
     }
     foreach my $vmid (keys %{$vmdata->{lxc}}) {
-        my $vmfw_conf = load_vmfw_conf($cluster_conf, 'ct', $vmid, $dir, $verbose);
+        my $vmfw_conf = load_vmfw_conf($cluster_conf, 'ct', $vmid, $dir);
         next if !$vmfw_conf->{options}; # skip if file does not exists
         $vmfw_configs->{$vmid} = $vmfw_conf;
     }
@@ -3332,15 +3338,43 @@ sub round_powerof2 {
     return ++$int;
 }
 
+my $set_global_log_ratelimit = sub {
+    my $cluster_opts = shift;
+
+    $global_log_ratelimit = '--limit 1/sec';
+    if (defined(my $log_rlimit = $cluster_opts->{log_ratelimit})) {
+       my $ll_format = $cluster_option_properties->{log_ratelimit}->{format};
+       my $limit = PVE::JSONSchema::parse_property_string($ll_format, $log_rlimit);
+
+       if ($limit->{enable}) {
+           if (my $rate = $limit->{rate}) {
+               $global_log_ratelimit = "--limit $rate";
+           }
+           if (my $burst = $limit->{burst}) {
+               $global_log_ratelimit .= " --limit-burst $burst";
+           }
+       } else {
+           $global_log_ratelimit = undef;
+       }
+    }
+};
+
 sub load_clusterfw_conf {
-    my ($filename, $verbose) = @_;
+    my ($filename) = @_;
 
     $filename = $clusterfw_conf_filename if !defined($filename);
+    my $empty_conf = {
+       rules => [],
+       options => {},
+       aliases => {},
+       groups => {},
+       group_comments => {},
+       ipset => {} ,
+       ipset_comments => {},
+    };
 
-    my $cluster_conf = {};
-    if (my $fh = IO::File->new($filename, O_RDONLY)) {
-       $cluster_conf = parse_clusterfw_config($filename, $fh, $verbose);
-    }
+    my $cluster_conf = generic_fw_config_parser($filename, $empty_conf, $empty_conf, 'cluster');
+    $set_global_log_ratelimit->($cluster_conf->{options});
 
     return $cluster_conf;
 }
@@ -3389,15 +3423,12 @@ sub save_clusterfw_conf {
 }
 
 sub load_hostfw_conf {
-    my ($cluster_conf, $filename, $verbose) = @_;
+    my ($cluster_conf, $filename) = @_;
 
     $filename = $hostfw_conf_filename if !defined($filename);
 
-    my $hostfw_conf = {};
-    if (my $fh = IO::File->new($filename, O_RDONLY)) {
-       $hostfw_conf = parse_hostfw_config($filename, $fh, $cluster_conf, $verbose);
-    }
-    return $hostfw_conf;
+    my $empty_conf = { rules => [], options => {}};
+    return generic_fw_config_parser($filename, $cluster_conf, $empty_conf, 'host');
 }
 
 sub save_hostfw_conf {
@@ -3423,7 +3454,7 @@ sub save_hostfw_conf {
 }
 
 sub compile {
-    my ($cluster_conf, $hostfw_conf, $vmdata, $verbose) = @_;
+    my ($cluster_conf, $hostfw_conf, $vmdata) = @_;
 
     my $vmfw_configs;
 
@@ -3433,19 +3464,19 @@ sub compile {
     if ($vmdata) { # test mode
        my $testdir = $vmdata->{testdir} || die "no test directory specified";
        my $filename = "$testdir/cluster.fw";
-       $cluster_conf = load_clusterfw_conf($filename, $verbose);
+       $cluster_conf = load_clusterfw_conf($filename);
 
        $filename = "$testdir/host.fw";
-       $hostfw_conf = load_hostfw_conf($cluster_conf, $filename, $verbose);
+       $hostfw_conf = load_hostfw_conf($cluster_conf, $filename);
 
-       $vmfw_configs = read_vm_firewall_configs($cluster_conf, $vmdata, $testdir, $verbose);
+       $vmfw_configs = read_vm_firewall_configs($cluster_conf, $vmdata, $testdir);
     } else { # normal operation
-       $cluster_conf = load_clusterfw_conf(undef, $verbose) if !$cluster_conf;
+       $cluster_conf = load_clusterfw_conf(undef) if !$cluster_conf;
 
-       $hostfw_conf = load_hostfw_conf($cluster_conf, undef, $verbose) if !$hostfw_conf;
+       $hostfw_conf = load_hostfw_conf($cluster_conf, undef) if !$hostfw_conf;
 
        $vmdata = read_local_vm_config();
-       $vmfw_configs = read_vm_firewall_configs($cluster_conf, $vmdata, undef, $verbose);
+       $vmfw_configs = read_vm_firewall_configs($cluster_conf, $vmdata, undef);
     }
 
     return ({},{},{},{}) if !$cluster_conf->{options}->{enable};
@@ -3463,16 +3494,16 @@ sub compile {
 
     push @{$cluster_conf->{ipset}->{management}}, { cidr => $localnet };
 
-    my $ruleset = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 4, $verbose);
-    my $rulesetv6 = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 6, $verbose);
-    my $ebtables_ruleset = compile_ebtables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $verbose);
+    my $ruleset = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 4);
+    my $rulesetv6 = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 6);
+    my $ebtables_ruleset = compile_ebtables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata);
     my $ipset_ruleset = compile_ipsets($cluster_conf, $vmfw_configs, $vmdata);
 
     return ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset);
 }
 
 sub compile_iptables_filter {
-    my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $ipversion, $verbose) = @_;
+    my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $ipversion) = @_;
 
     my $ruleset = {};
 
@@ -3682,7 +3713,7 @@ sub compile_ipsets {
 }
 
 sub compile_ebtables_filter {
-    my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $verbose) = @_;
+    my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata) = @_;
 
     if (!($cluster_conf->{options}->{ebtables} // 1)) {
        return {};
@@ -3807,7 +3838,7 @@ sub generate_tap_layer2filter {
 # * both the $active_chains hash and the returned status_hash have different
 #   structure (they contain a key named 'rules').
 sub get_ruleset_status {
-    my ($ruleset, $active_chains, $digest_fn, $verbose, $change_only_regex) = @_;
+    my ($ruleset, $active_chains, $digest_fn, $change_only_regex) = @_;
 
     my $statushash = {};
 
@@ -3865,12 +3896,12 @@ sub print_sig_rule {
 }
 
 sub get_ruleset_cmdlist {
-    my ($ruleset, $verbose, $iptablescmd) = @_;
+    my ($ruleset, $iptablescmd) = @_;
 
     my $cmdlist = "*filter\n"; # we pass this to iptables-restore;
 
     my ($active_chains, $hooks) = iptables_get_chains($iptablescmd);
-    my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest, $verbose);
+    my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest);
 
     # create missing chains first
     foreach my $chain (sort keys %$ruleset) {
@@ -3929,14 +3960,14 @@ sub get_ruleset_cmdlist {
 my $pve_ebtables_chainname_regex = qr/PVEFW-\S+|(?:tap|veth)\d+i\d+-(?:IN|OUT)/;
 
 sub get_ebtables_cmdlist {
-    my ($ruleset, $verbose) = @_;
+    my ($ruleset) = @_;
 
     my $changes = 0;
     my $cmdlist = "*filter\n";
 
     my $active_chains = ebtables_get_chains();
     my $statushash = get_ruleset_status($ruleset, $active_chains,
-                                       \&iptables_chain_digest, $verbose,
+                                       \&iptables_chain_digest,
                                        $pve_ebtables_chainname_regex);
 
     # create chains first and make sure PVE rules are evaluated if active
@@ -3967,14 +3998,14 @@ sub get_ebtables_cmdlist {
 }
 
 sub get_ipset_cmdlist {
-    my ($ruleset, $verbose) = @_;
+    my ($ruleset) = @_;
 
     my $cmdlist = "";
 
     my $delete_cmdlist = "";
 
     my $active_chains = ipset_get_chains();
-    my $statushash = get_ruleset_status($ruleset, $active_chains, \&ipset_chain_digest, $verbose);
+    my $statushash = get_ruleset_status($ruleset, $active_chains, \&ipset_chain_digest);
 
     # remove stale _swap chains
     foreach my $chain (keys %$active_chains) {
@@ -4025,16 +4056,16 @@ sub get_ipset_cmdlist {
 }
 
 sub apply_ruleset {
-    my ($ruleset, $hostfw_conf, $ipset_ruleset, $rulesetv6, $ebtables_ruleset, $verbose) = @_;
+    my ($ruleset, $hostfw_conf, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = @_;
 
     enable_bridge_firewall();
 
     my ($ipset_create_cmdlist, $ipset_delete_cmdlist, $ipset_changes) =
-       get_ipset_cmdlist($ipset_ruleset, $verbose);
+       get_ipset_cmdlist($ipset_ruleset);
 
-    my ($cmdlist, $changes) = get_ruleset_cmdlist($ruleset, $verbose);
-    my ($cmdlistv6, $changesv6) = get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables");
-    my ($ebtables_cmdlist, $ebtables_changes) = get_ebtables_cmdlist($ebtables_ruleset, $verbose);
+    my ($cmdlist, $changes) = get_ruleset_cmdlist($ruleset);
+    my ($cmdlistv6, $changesv6) = get_ruleset_cmdlist($rulesetv6, "ip6tables");
+    my ($ebtables_cmdlist, $ebtables_changes) = get_ebtables_cmdlist($ebtables_ruleset);
 
     if ($verbose) {
        if ($ipset_changes) {
@@ -4086,7 +4117,7 @@ sub apply_ruleset {
 
     # test: re-read status and check if everything is up to date
     my $active_chains = iptables_get_chains();
-    my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest, 0);
+    my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest);
 
     my $errors;
     foreach my $chain (sort keys %$ruleset) {
@@ -4098,7 +4129,7 @@ sub apply_ruleset {
     }
 
     my $active_chainsv6 = iptables_get_chains("ip6tables");
-    my $statushashv6 = get_ruleset_status($rulesetv6, $active_chainsv6, \&iptables_chain_digest, 0);
+    my $statushashv6 = get_ruleset_status($rulesetv6, $active_chainsv6, \&iptables_chain_digest);
 
     foreach my $chain (sort keys %$rulesetv6) {
        my $stat = $statushashv6->{$chain};
@@ -4111,7 +4142,7 @@ sub apply_ruleset {
     my $active_ebtables_chains = ebtables_get_chains();
     my $ebtables_statushash = get_ruleset_status($ebtables_ruleset,
                                $active_ebtables_chains, \&iptables_chain_digest,
-                               0, $pve_ebtables_chainname_regex);
+                               $pve_ebtables_chainname_regex);
 
     foreach my $chain (sort keys %$ebtables_ruleset) {
        my $stat = $ebtables_statushash->{$chain};