X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=src%2FPVE%2FFirewall.pm;h=dd251bf2edda73d22b34d5de754b8ea97d185862;hb=db8a955f4d4cc32cc25734f5a340bdc6b6d229ab;hp=22cae5a8c7ee50eafe1073f063339837353d1199;hpb=55fad3b7889f943599038c3a13e070cd1fcab051;p=pve-firewall.git diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm index 22cae5a..dd251bf 100644 --- a/src/PVE/Firewall.pm +++ b/src/PVE/Firewall.pm @@ -19,7 +19,8 @@ use PVE::Tools qw(run_command lock_file dir_glob_foreach); use Encode; my $hostfw_conf_filename = "/etc/pve/local/host.fw"; -my $clusterfw_conf_filename = "/etc/pve/firewall/cluster.fw"; +my $pvefw_conf_dir = "/etc/pve/firewall"; +my $clusterfw_conf_filename = "$pvefw_conf_dir/cluster.fw"; # dynamically include PVE::QemuServer and PVE::OpenVZ # to avoid dependency problems @@ -488,7 +489,8 @@ my $pve_fw_parsed_macros; my $pve_fw_macro_descr; my $pve_fw_preferred_macro_names = {}; -my $pve_std_chains = { +my $pve_std_chains = {}; +$pve_std_chains->{4} = { 'PVEFW-SET-ACCEPT-MARK' => [ "-j MARK --set-mark 1", ], @@ -759,7 +761,10 @@ sub local_network { return $__local_network; } -my $max_iptables_ipset_name_length = 27; +# ipset names are limited to 31 characters, and we use '_swap' +# suffix for atomic update, for example PVEFW-${VMID}-${ipset_name}_swap + +my $max_iptables_ipset_name_length = 31 - length("_swap"); sub compute_ipset_chain_name { my ($vmid, $ipset_name) = @_; @@ -776,6 +781,12 @@ sub compute_ipset_chain_name { return "PVEFW-$id"; } +sub compute_ipfilter_ipset_name { + my ($iface) = @_; + + return "ipfilter-$iface"; +} + sub parse_address_list { my ($str) = @_; @@ -1204,6 +1215,46 @@ sub copy_rule_data { return $rule; } +sub rules_modify_permissions { + my ($rule_env) = @_; + + if ($rule_env eq 'host') { + return { + check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]], + }; + } elsif ($rule_env eq 'cluster' || $rule_env eq 'group') { + return { + check => ['perm', '/', [ 'Sys.Modify' ]], + }; + } elsif ($rule_env eq 'vm' || $rule_env eq 'ct') { + return { + check => ['perm', '/vms/{vmid}', [ 'VM.Config.Network' ]], + } + } + + return undef; +} + +sub rules_audit_permissions { + my ($rule_env) = @_; + + if ($rule_env eq 'host') { + return { + check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]], + }; + } elsif ($rule_env eq 'cluster' || $rule_env eq 'group') { + return { + check => ['perm', '/', [ 'Sys.Audit' ]], + }; + } elsif ($rule_env eq 'vm' || $rule_env eq 'ct') { + return { + check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]], + } + } + + return undef; +} + # core functions my $bridge_firewall_enabled = 0; @@ -1623,7 +1674,7 @@ sub ruleset_chain_add_input_filters { } sub ruleset_create_vm_chain { - my ($ruleset, $chain, $options, $macaddr, $direction) = @_; + my ($ruleset, $chain, $options, $macaddr, $ipfilter_ipset, $direction) = @_; ruleset_create_chain($ruleset, $chain); my $accept = generate_nfqueue($options); @@ -1642,6 +1693,9 @@ sub ruleset_create_vm_chain { if (defined($macaddr) && !(defined($options->{macfilter}) && $options->{macfilter} == 0)) { ruleset_addrule($ruleset, $chain, "-m mac ! --mac-source $macaddr -j DROP"); } + if ($ipfilter_ipset) { + ruleset_addrule($ruleset, $chain, "-m set ! --match-set $ipfilter_ipset src -j DROP"); + } ruleset_addrule($ruleset, $chain, "-j MARK --set-mark 0"); # clear mark } } @@ -1742,7 +1796,7 @@ sub generate_venet_rules_direction { my $chain = "venet0-$vmid-$direction"; - ruleset_create_vm_chain($ruleset, $chain, $options, undef, $direction); + ruleset_create_vm_chain($ruleset, $chain, $options, undef, undef, $direction); ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $vmfw_conf, $chain, 'venet', $direction); @@ -1784,24 +1838,34 @@ sub generate_tap_rules_direction { my $tapchain = "$iface-$direction"; - ruleset_create_vm_chain($ruleset, $tapchain, $options, $macaddr, $direction); + my $ipfilter_name = compute_ipfilter_ipset_name($netid); + my $ipfilter_ipset = compute_ipset_chain_name($vmid, $ipfilter_name) + if $vmfw_conf->{ipset}->{$ipfilter_name}; - ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $vmfw_conf, $tapchain, $netid, $direction, $options); + # create chain with mac and ip filter + ruleset_create_vm_chain($ruleset, $tapchain, $options, $macaddr, $ipfilter_ipset, $direction); - ruleset_generate_vm_ipsrules($ruleset, $options, $direction, $iface); + if ($options->{enable}) { + ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $vmfw_conf, $tapchain, $netid, $direction, $options); - # implement policy - my $policy; + ruleset_generate_vm_ipsrules($ruleset, $options, $direction, $iface); - if ($direction eq 'OUT') { - $policy = $options->{policy_out} || 'ACCEPT'; # allow everything by default - } else { + # implement policy + my $policy; + + if ($direction eq 'OUT') { + $policy = $options->{policy_out} || 'ACCEPT'; # allow everything by default + } else { $policy = $options->{policy_in} || 'DROP'; # allow nothing by default - } + } - my $accept = generate_nfqueue($options); - my $accept_action = $direction eq 'OUT' ? "PVEFW-SET-ACCEPT-MARK" : $accept; - ruleset_add_chain_policy($ruleset, $tapchain, $vmid, $policy, $loglevel, $accept_action); + my $accept = generate_nfqueue($options); + my $accept_action = $direction eq 'OUT' ? "PVEFW-SET-ACCEPT-MARK" : $accept; + ruleset_add_chain_policy($ruleset, $tapchain, $vmid, $policy, $loglevel, $accept_action); + } else { + my $accept_action = $direction eq 'OUT' ? "PVEFW-SET-ACCEPT-MARK" : 'ACCEPT'; + ruleset_add_chain_policy($ruleset, $tapchain, $vmid, 'ACCEPT', $loglevel, $accept_action); + } # plug the tap chain to bridge chain if ($direction eq 'IN') { @@ -2401,7 +2465,7 @@ sub load_vmfw_conf { my $vmfw_conf = {}; - $dir = "/etc/pve/firewall" if !defined($dir); + $dir = $pvefw_conf_dir if !defined($dir); my $filename = "$dir/$vmid.fw"; if (my $fh = IO::File->new($filename, O_RDONLY)) { @@ -2521,21 +2585,23 @@ sub save_vmfw_conf { my $raw = ''; my $options = $vmfw_conf->{options}; - $raw .= &$format_options($options) if scalar(keys %$options); + $raw .= &$format_options($options) if $options && scalar(keys %$options); my $aliases = $vmfw_conf->{aliases}; - $raw .= &$format_aliases($aliases) if scalar(keys %$aliases); + $raw .= &$format_aliases($aliases) if $aliases && scalar(keys %$aliases); - $raw .= &$format_ipsets($vmfw_conf); + $raw .= &$format_ipsets($vmfw_conf) if $vmfw_conf->{ipset}; my $rules = $vmfw_conf->{rules} || []; - if (scalar(@$rules)) { + if ($rules && scalar(@$rules)) { $raw .= "[RULES]\n\n"; $raw .= &$format_rules($rules, 1); $raw .= "\n"; } - my $filename = "/etc/pve/firewall/$vmid.fw"; + mkdir $pvefw_conf_dir; + + my $filename = "$pvefw_conf_dir/$vmid.fw"; PVE::Tools::file_set_contents($filename, $raw); } @@ -2576,29 +2642,35 @@ sub get_option_log_level { } sub generate_std_chains { - my ($ruleset, $options) = @_; + my ($ruleset, $options, $ipversion) = @_; + + my $std_chains = $pve_std_chains->{$ipversion} || die "internal error"; my $loglevel = get_option_log_level($options, 'smurf_log_level'); - # same as shorewall smurflog. - my $chain = 'PVEFW-smurflog'; - $pve_std_chains->{$chain} = []; + my $chain; - push @{$pve_std_chains->{$chain}}, get_log_rule_base($chain, 0, "DROP: ", $loglevel) if $loglevel; - push @{$pve_std_chains->{$chain}}, "-j DROP"; + if ($ipversion == 4) { + # same as shorewall smurflog. + $chain = 'PVEFW-smurflog'; + $std_chains->{$chain} = []; + + push @{$std_chains->{$chain}}, get_log_rule_base($chain, 0, "DROP: ", $loglevel) if $loglevel; + push @{$std_chains->{$chain}}, "-j DROP"; + } # same as shorewall logflags action. $loglevel = get_option_log_level($options, 'tcp_flags_log_level'); $chain = 'PVEFW-logflags'; - $pve_std_chains->{$chain} = []; + $std_chains->{$chain} = []; # fixme: is this correctly logged by pvewf-logger? (ther is no --log-ip-options for NFLOG) - push @{$pve_std_chains->{$chain}}, get_log_rule_base($chain, 0, "DROP: ", $loglevel) if $loglevel; - push @{$pve_std_chains->{$chain}}, "-j DROP"; + push @{$std_chains->{$chain}}, get_log_rule_base($chain, 0, "DROP: ", $loglevel) if $loglevel; + push @{$std_chains->{$chain}}, "-j DROP"; - foreach my $chain (keys %$pve_std_chains) { + foreach my $chain (keys %$std_chains) { ruleset_create_chain($ruleset, $chain); - foreach my $rule (@{$pve_std_chains->{$chain}}) { + foreach my $rule (@{$std_chains->{$chain}}) { if (ref($rule)) { ruleset_generate_rule($ruleset, $chain, $rule); } else { @@ -2685,33 +2757,36 @@ sub save_clusterfw_conf { my $raw = ''; my $options = $cluster_conf->{options}; - $raw .= &$format_options($options) if scalar(keys %$options); + $raw .= &$format_options($options) if $options && scalar(keys %$options); my $aliases = $cluster_conf->{aliases}; - $raw .= &$format_aliases($aliases) if scalar(keys %$aliases); + $raw .= &$format_aliases($aliases) if $aliases && scalar(keys %$aliases); - $raw .= &$format_ipsets($cluster_conf); + $raw .= &$format_ipsets($cluster_conf) if $cluster_conf->{ipset}; my $rules = $cluster_conf->{rules}; - if (scalar(@$rules)) { + if ($rules && scalar(@$rules)) { $raw .= "[RULES]\n\n"; $raw .= &$format_rules($rules, 1); $raw .= "\n"; } - foreach my $group (sort keys %{$cluster_conf->{groups}}) { - my $rules = $cluster_conf->{groups}->{$group}; - if (my $comment = $cluster_conf->{group_comments}->{$group}) { - my $utf8comment = encode('utf8', $comment); - $raw .= "[group $group] # $utf8comment\n\n"; - } else { - $raw .= "[group $group]\n\n"; - } + if ($cluster_conf->{groups}) { + foreach my $group (sort keys %{$cluster_conf->{groups}}) { + my $rules = $cluster_conf->{groups}->{$group}; + if (my $comment = $cluster_conf->{group_comments}->{$group}) { + my $utf8comment = encode('utf8', $comment); + $raw .= "[group $group] # $utf8comment\n\n"; + } else { + $raw .= "[group $group]\n\n"; + } - $raw .= &$format_rules($rules, 0); - $raw .= "\n"; + $raw .= &$format_rules($rules, 0); + $raw .= "\n"; + } } + mkdir $pvefw_conf_dir; PVE::Tools::file_set_contents($clusterfw_conf_filename, $raw); } @@ -2733,10 +2808,10 @@ sub save_hostfw_conf { my $raw = ''; my $options = $hostfw_conf->{options}; - $raw .= &$format_options($options) if scalar(keys %$options); + $raw .= &$format_options($options) if $options && scalar(keys %$options); my $rules = $hostfw_conf->{rules}; - if (scalar(@$rules)) { + if ($rules && scalar(@$rules)) { $raw .= "[RULES]\n\n"; $raw .= &$format_rules($rules, 1); $raw .= "\n"; @@ -2768,6 +2843,13 @@ sub compile { $vmfw_configs = read_vm_firewall_configs($cluster_conf, $vmdata, undef, $verbose); } + my ($ruleset, $ipset_ruleset) = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 4, $verbose); + return ($ruleset, $ipset_ruleset); +} + +sub compile_iptables_filter { + my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $ipversion, $verbose) = @_; + $cluster_conf->{ipset}->{venet0} = []; my $venet0_ipset_chain = compute_ipset_chain_name(0, 'venet0'); @@ -2815,7 +2897,7 @@ sub compile { ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o venet0 -m set --match-set ${venet0_ipset_chain} dst -j PVEFW-VENET-IN"); - generate_std_chains($ruleset, $hostfw_options); + generate_std_chains($ruleset, $hostfw_options, $ipversion); my $hostfw_enable = !(defined($hostfw_options->{enable}) && ($hostfw_options->{enable} == 0)); @@ -2834,7 +2916,6 @@ sub compile { my $conf = $vmdata->{qemu}->{$vmid}; my $vmfw_conf = $vmfw_configs->{$vmid}; return if !$vmfw_conf; - return if !$vmfw_conf->{options}->{enable}; generate_ipset_chains($ipset_ruleset, $cluster_conf, $vmfw_conf); @@ -2861,32 +2942,34 @@ sub compile { my $vmfw_conf = $vmfw_configs->{$vmid}; return if !$vmfw_conf; - return if !$vmfw_conf->{options}->{enable}; generate_ipset_chains($ipset_ruleset, $cluster_conf, $vmfw_conf); - if ($conf->{ip_address} && $conf->{ip_address}->{value}) { - my $ip = $conf->{ip_address}->{value}; - $ip =~ s/\s+/,/g; - parse_address_list($ip); # make sure we have a valid $ip list + if ($vmfw_conf->{options}->{enable}) { + if ($conf->{ip_address} && $conf->{ip_address}->{value}) { + my $ip = $conf->{ip_address}->{value}; + $ip =~ s/\s+/,/g; + parse_address_list($ip); # make sure we have a valid $ip list - my @ips = split(',', $ip); + my @ips = split(',', $ip); - foreach my $singleip (@ips) { - my $venet0ipset = {}; - $venet0ipset->{cidr} = $singleip; - push @{$cluster_conf->{ipset}->{venet0}}, $venet0ipset; - } + foreach my $singleip (@ips) { + my $venet0ipset = {}; + $venet0ipset->{cidr} = $singleip; + push @{$cluster_conf->{ipset}->{venet0}}, $venet0ipset; + } - generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, 'IN'); - generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, 'OUT'); + generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, 'IN'); + generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, 'OUT'); + } } if ($conf->{netif} && $conf->{netif}->{value}) { my $netif = PVE::OpenVZ::parse_netif($conf->{netif}->{value}); foreach my $netid (keys %$netif) { my $d = $netif->{$netid}; - + my $bridge = $d->{bridge}; + next if !$bridge || $bridge !~ m/^vmbr\d+(v(\d+))?f$/; # firewall enabled ? my $macaddr = $d->{mac}; my $iface = $d->{host_ifname}; generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,