X-Git-Url: https://git.proxmox.com/?p=pve-firewall.git;a=blobdiff_plain;f=src%2FPVE%2FFirewall.pm;h=be5354c780799f02dfe184937481e921f1d7f6fb;hp=31b9ad123cf1e47b89a1f8ff07bf52b7ae758464;hb=e2beb7aa9900c650ec69594a2f26cc2889908134;hpb=097820b037b12789bc4080d221284147303f5ca2 diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm index 31b9ad1..be5354c 100644 --- a/src/PVE/Firewall.pm +++ b/src/PVE/Firewall.pm @@ -40,7 +40,7 @@ sub pve_verify_ipv4_or_cidr { my ($cidr, $noerr) = @_; if ($cidr =~ m!^(?:$IPV4RE)(/(\d+))?$!) { - return $cidr if Net::IP->new($cidr); + return $cidr if Net::IP->new($cidr); return undef if $noerr; die Net::IP::Error() . "\n"; } @@ -53,7 +53,7 @@ PVE::JSONSchema::register_standard_option('ipset-name', { type => 'string', pattern => '[A-Za-z][A-Za-z0-9\-\_]+', minLength => 2, - maxLength => 20, + maxLength => 20, }); PVE::JSONSchema::register_standard_option('pve-fw-alias', { @@ -61,12 +61,12 @@ PVE::JSONSchema::register_standard_option('pve-fw-alias', { type => 'string', pattern => '[A-Za-z][A-Za-z0-9\-\_]+', minLength => 2, - maxLength => 20, + maxLength => 20, }); PVE::JSONSchema::register_standard_option('pve-fw-loglevel' => { description => "Log level.", - type => 'string', + type => 'string', enum => ['emerg', 'alert', 'crit', 'err', 'warning', 'notice', 'info', 'debug', 'nolog'], optional => 1, }); @@ -79,7 +79,7 @@ PVE::JSONSchema::register_standard_option('pve-security-group-name', { type => 'string', pattern => $security_group_name_pattern, minLength => 2, - maxLength => 20, + maxLength => 20, }); my $feature_ipset_nomatch = 0; @@ -98,7 +98,6 @@ use Data::Dumper; my $nodename = PVE::INotify::nodename(); my $pve_fw_lock_filename = "/var/lock/pvefw.lck"; -my $pve_fw_status_filename = "/var/lib/pve-firewall/pvefw.status"; my $default_log_level = 'info'; @@ -733,7 +732,7 @@ sub parse_address_list { } $iprange = 1 if $elem =~ m/-/; } - + die "you can use a range in a list\n" if $iprange && $count > 1; } @@ -757,7 +756,7 @@ sub parse_port_name_number_or_range { if ($icmp_type_names->{$item}) { $icmp_port = 1; } else { - die "invalid port '$item'\n" if !$services->{byname}->{$item}; + die "invalid port '$item'\n" if !$services->{byname}->{$item}; } } } @@ -918,7 +917,7 @@ sub add_rule_properties { sub delete_rule_properties { my ($rule, $delete_str) = @_; - + foreach my $opt (PVE::Tools::split_list($delete_str)) { raise_param_exc({ 'delete' => "no such property ('$opt')"}) if !defined($rule_properties->{$opt}); @@ -956,7 +955,7 @@ my $apply_macro = sub { if (!defined($v)) { my $msg = "missing parameter '$k' in macro '$macro_name'"; - raise_param_exc({ macro => $msg }) if $verify; + raise_param_exc({ macro => $msg }) if $verify; die "$msg\n"; } $rule->{$k} = $v; @@ -968,7 +967,7 @@ my $apply_macro = sub { if (defined($rule->{$k})) { if ($rule->{$k} ne $param->{$k}) { my $msg = "parameter '$k' already define in macro (value = '$rule->{$k}')"; - raise_param_exc({ $k => $msg }) if $verify; + raise_param_exc({ $k => $msg }) if $verify; die "$msg\n"; } } else { @@ -988,19 +987,19 @@ sub verify_rule { raise_param_exc({ type => "missing property"}) if !$type; raise_param_exc({ action => "missing property"}) if !$rule->{action}; - + if ($type eq 'in' || $type eq 'out') { raise_param_exc({ action => "unknown action '$rule->{action}'"}) if $rule->{action} !~ m/^(ACCEPT|DROP|REJECT)$/; } elsif ($type eq 'group') { - raise_param_exc({ type => "security groups not allowed"}) + raise_param_exc({ type => "security groups not allowed"}) if !$allow_groups; - raise_param_exc({ action => "invalid characters in security group name"}) + raise_param_exc({ action => "invalid characters in security group name"}) if $rule->{action} !~ m/^${security_group_name_pattern}$/; } else { raise_param_exc({ type => "unknown rule type '$type'"}); } - + # fixme: verify $rule->{iface}? if ($rule->{macro}) { @@ -1243,7 +1242,7 @@ sub ruleset_generate_cmdstr { die "invalid security group name '$dest'\n"; } } elsif ($dest =~ m/^${ip_alias_pattern}$/){ - my $alias = lc($source); + my $alias = lc($dest); my $e = $cluster_conf->{aliases}->{$alias}; die "no such alias $dest" if !$e; push @cmd, "-d $e->{cidr}"; @@ -1436,7 +1435,12 @@ sub ruleset_chain_add_conn_filters { } sub ruleset_chain_add_input_filters { - my ($ruleset, $chain, $options) = @_; + my ($ruleset, $chain, $options, $cluster_conf, $loglevel) = @_; + + if ($cluster_conf->{ipset}->{blacklist}){ + ruleset_addlog($ruleset, $chain, 0, "DROP: ", $loglevel, "-m set --match-set PVEFW-blacklist src"); + ruleset_addrule($ruleset, $chain, "-m set --match-set PVEFW-blacklist src -j DROP"); + } if (!(defined($options->{nosmurfs}) && $options->{nosmurfs} == 0)) { ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID,NEW -j PVEFW-smurfs"); @@ -1471,6 +1475,26 @@ sub ruleset_create_vm_chain { } } +sub ruleset_add_group_rule { + my ($ruleset, $cluster_conf, $chain, $rule, $direction, $action) = @_; + + my $group = $rule->{action}; + my $group_chain = "GROUP-$group-$direction"; + if(!ruleset_chain_exist($ruleset, $group_chain)){ + generate_group_rules($ruleset, $cluster_conf, $group); + } + + if ($direction eq 'OUT' && $rule->{iface_out}) { + ruleset_addrule($ruleset, $chain, "-o $rule->{iface_out} -j $group_chain"); + } elsif ($direction eq 'IN' && $rule->{iface_in}) { + ruleset_addrule($ruleset, $chain, "-i $rule->{iface_in} -j $group_chain"); + } else { + ruleset_addrule($ruleset, $chain, "-j $group_chain"); + } + + ruleset_addrule($ruleset, $chain, "-m mark --mark 1 -j $action"); +} + sub ruleset_generate_vm_rules { my ($ruleset, $rules, $cluster_conf, $chain, $netid, $direction, $options) = @_; @@ -1482,28 +1506,18 @@ sub ruleset_generate_vm_rules { next if $rule->{iface} && $rule->{iface} ne $netid; next if !$rule->{enable}; if ($rule->{type} eq 'group') { - my $group_chain = "GROUP-$rule->{action}-$direction"; - if(!ruleset_chain_exist($ruleset, $group_chain)){ - generate_group_rules($ruleset, $cluster_conf, $rule->{action}); - } - ruleset_addrule($ruleset, $chain, "-j $group_chain"); - if ($direction eq 'OUT'){ - ruleset_addrule($ruleset, $chain, "-m mark --mark 1 -j RETURN"); - }else{ - my $accept = generate_nfqueue($options); - ruleset_addrule($ruleset, $chain, "-m mark --mark 1 -j $accept"); - } - + ruleset_add_group_rule($ruleset, $cluster_conf, $chain, $rule, $direction, + $direction eq 'OUT' ? 'RETURN' : $in_accept); } else { next if $rule->{type} ne $lc_direction; eval { if ($direction eq 'OUT') { ruleset_generate_rule($ruleset, $chain, $rule, - { ACCEPT => "PVEFW-SET-ACCEPT-MARK", REJECT => "PVEFW-reject" }, + { ACCEPT => "PVEFW-SET-ACCEPT-MARK", REJECT => "PVEFW-reject" }, undef, $cluster_conf); } else { - ruleset_generate_rule($ruleset, $chain, $rule, - { ACCEPT => $in_accept , REJECT => "PVEFW-reject" }, + ruleset_generate_rule($ruleset, $chain, $rule, + { ACCEPT => $in_accept , REJECT => "PVEFW-reject" }, undef, $cluster_conf); } }; @@ -1548,8 +1562,6 @@ sub ruleset_generate_vm_ipsrules { sub generate_venet_rules_direction { my ($ruleset, $cluster_conf, $hostfw_conf, $vmfw_conf, $vmid, $ip, $direction) = @_; - parse_address_list($ip); # make sure we have a valid $ip list - my $lc_direction = lc($direction); my $rules = $vmfw_conf->{rules}; @@ -1649,7 +1661,7 @@ sub enable_host_firewall { ruleset_addrule($ruleset, $chain, "-i lo -j ACCEPT"); ruleset_chain_add_conn_filters($ruleset, $chain, 'ACCEPT'); - ruleset_chain_add_input_filters($ruleset, $chain, $options); + ruleset_chain_add_input_filters($ruleset, $chain, $options, $cluster_conf, $loglevel); ruleset_addrule($ruleset, $chain, "-m addrtype --dst-type MULTICAST -j ACCEPT"); ruleset_addrule($ruleset, $chain, "-p udp -m conntrack --ctstate NEW --dport 5404:5405 -j ACCEPT"); @@ -1660,8 +1672,14 @@ sub enable_host_firewall { # add host rules first, so that cluster wide rules can be overwritten foreach my $rule (@$rules, @$cluster_rules) { - next if $rule->{type} ne 'in'; - ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" }, undef, $cluster_conf); + $rule->{iface_in} = $rule->{iface} if $rule->{iface}; + if ($rule->{type} eq 'group') { + ruleset_add_group_rule($ruleset, $cluster_conf, $chain, $rule, 'IN', $accept_action); + } elsif ($rule->{type} eq 'in') { + ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" }, + undef, $cluster_conf); + } + delete $rule->{iface_in}; } # implement input policy @@ -1687,8 +1705,14 @@ sub enable_host_firewall { # add host rules first, so that cluster wide rules can be overwritten foreach my $rule (@$rules, @$cluster_rules) { - next if $rule->{type} ne 'out'; - ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" }, undef, $cluster_conf); + $rule->{iface_out} = $rule->{iface} if $rule->{iface}; + if ($rule->{type} eq 'group') { + ruleset_add_group_rule($ruleset, $cluster_conf, $chain, $rule, 'OUT', $accept_action); + } elsif ($rule->{type} eq 'out') { + ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" }, + undef, $cluster_conf); + } + delete $rule->{iface_out}; } # implement output policy @@ -1701,10 +1725,14 @@ sub enable_host_firewall { sub generate_group_rules { my ($ruleset, $cluster_conf, $group) = @_; - die "no such security group '$group'\n" if !$cluster_conf->{groups}->{$group}; my $rules = $cluster_conf->{groups}->{$group}; + if (!$rules) { + warn "no such security group '$group'\n"; + $rules = []; # create empty chain + } + my $chain = "GROUP-${group}-IN"; ruleset_create_chain($ruleset, $chain); @@ -1777,7 +1805,7 @@ sub parse_fw_rule { die "unknown action '$action'\n"; } } elsif ($type eq 'group') { - die "wrong number of rule elements\n" if scalar(@data) != 3; + die "wrong number of rule elements\n" if scalar(@data) > 3; die "groups disabled\n" if !$allow_groups; die "invalid characters in group name\n" if $action !~ m/^${security_group_name_pattern}$/; @@ -1853,7 +1881,7 @@ sub parse_hostfw_option { my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog"; - if ($line =~ m/^(enable|nosmurfs|tcpflags|optimize):\s*(0|1)\s*$/i) { + if ($line =~ m/^(enable|nosmurfs|tcpflags):\s*(0|1)\s*$/i) { $opt = lc($1); $value = int($2); } elsif ($line =~ m/^(log_level_in|log_level_out|tcp_flags_log_level|smurf_log_level):\s*(($loglevels)\s*)?$/i) { @@ -2012,14 +2040,14 @@ sub parse_cluster_fw_rules { my $section; my $group; - my $res = { - rules => [], - options => {}, - aliases => {}, - groups => {}, - group_comments => {}, + my $res = { + rules => [], + options => {}, + aliases => {}, + groups => {}, + group_comments => {}, ipset => {} , - ipset_comments => {}, + ipset_comments => {}, }; while (defined(my $line = <$fh>)) { @@ -2059,7 +2087,7 @@ sub parse_cluster_fw_rules { $group = lc($1); my $comment = $2; $res->{$section}->{$group} = []; - $res->{ipset_comments}->{$group} = decode('utf8', $comment) + $res->{ipset_comments}->{$group} = decode('utf8', $comment) if $comment; next; } @@ -2107,7 +2135,7 @@ sub parse_cluster_fw_rules { if($cidr !~ m/^${ip_alias_pattern}$/) { $cidr =~ s|/32$||; - + eval { pve_verify_ipv4_or_cidr($cidr); }; if (my $err = $@) { warn "$prefix: $cidr - $err"; @@ -2115,10 +2143,10 @@ sub parse_cluster_fw_rules { } } - my $entry = { cidr => $cidr }; + my $entry = { cidr => $cidr }; $entry->{nomatch} = 1 if $nomatch; $entry->{comment} = $comment if $comment; - + push @{$res->{$section}->{$group}}, $entry; } } @@ -2274,7 +2302,7 @@ my $format_ipset = sub { if $entry->{comment} && $entry->{comment} !~ m/^\s*$/; $raw .= "$line\n"; } - + return $raw; }; @@ -2422,30 +2450,6 @@ sub round_powerof2 { return ++$int; } -sub save_pvefw_status { - my ($status) = @_; - - die "unknown status '$status' - internal error" - if $status !~ m/^(stopped|active)$/; - - mkdir dirname($pve_fw_status_filename); - PVE::Tools::file_set_contents($pve_fw_status_filename, $status); -} - -sub read_pvefw_status { - - my $status = 'unknown'; - - return 'stopped' if ! -f $pve_fw_status_filename; - - eval { - $status = PVE::Tools::file_get_contents($pve_fw_status_filename); - }; - warn $@ if $@; - - return $status; -} - sub load_clusterfw_conf { my ($filename) = @_; @@ -2469,7 +2473,7 @@ sub save_clusterfw_conf { my $aliases = $cluster_conf->{aliases}; $raw .= &$format_aliases($aliases) if scalar(keys %$aliases); - + foreach my $ipset (sort keys %{$cluster_conf->{ipset}}) { if (my $comment = $cluster_conf->{ipset_comments}->{$ipset}) { my $utf8comment = encode('utf8', $comment); @@ -2561,8 +2565,7 @@ sub compile { } - my $ipset_ruleset = {}; - generate_ipset_chains($ipset_ruleset, $cluster_conf); + $cluster_conf->{ipset}->{venet0} = []; my $ruleset = {}; @@ -2570,7 +2573,7 @@ sub compile { ruleset_create_chain($ruleset, "PVEFW-OUTPUT"); ruleset_create_chain($ruleset, "PVEFW-FORWARD"); - + my $hostfw_options = $hostfw_conf->{options} || {}; # fixme: what log level should we use here? @@ -2578,27 +2581,22 @@ sub compile { ruleset_chain_add_conn_filters($ruleset, "PVEFW-FORWARD", "ACCEPT"); - if ($cluster_conf->{ipset}->{blacklist}){ - ruleset_addlog($ruleset, "PVEFW-FORWARD", 0, "DROP: ", $loglevel, "-m set --match-set PVEFW-blacklist src"); - ruleset_addrule($ruleset, "PVEFW-FORWARD", "-m set --match-set PVEFW-blacklist src -j DROP"); - } - ruleset_create_chain($ruleset, "PVEFW-VENET-OUT"); - ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i venet0 -j PVEFW-VENET-OUT"); - ruleset_addrule($ruleset, "PVEFW-INPUT", "-i venet0 -j PVEFW-VENET-OUT"); + ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i venet0 -m set --match-set PVEFW-venet0 src -j PVEFW-VENET-OUT"); + ruleset_addrule($ruleset, "PVEFW-INPUT", "-i venet0 -m set --match-set PVEFW-venet0 src -j PVEFW-VENET-OUT"); ruleset_create_chain($ruleset, "PVEFW-FWBR-IN"); - ruleset_chain_add_input_filters($ruleset, "PVEFW-FWBR-IN", $hostfw_options); + ruleset_chain_add_input_filters($ruleset, "PVEFW-FWBR-IN", $hostfw_options, $cluster_conf, $loglevel); - ruleset_addrule($ruleset, "PVEFW-FORWARD", "-m physdev --physdev-is-bridged --physdev-in link+ -j PVEFW-FWBR-IN"); + ruleset_addrule($ruleset, "PVEFW-FORWARD", "-m physdev --physdev-is-bridged --physdev-in fwln+ -j PVEFW-FWBR-IN"); ruleset_create_chain($ruleset, "PVEFW-FWBR-OUT"); - ruleset_addrule($ruleset, "PVEFW-FORWARD", "-m physdev --physdev-is-bridged --physdev-out link+ -j PVEFW-FWBR-OUT"); + ruleset_addrule($ruleset, "PVEFW-FORWARD", "-m physdev --physdev-is-bridged --physdev-out fwln+ -j PVEFW-FWBR-OUT"); ruleset_create_chain($ruleset, "PVEFW-VENET-IN"); - ruleset_chain_add_input_filters($ruleset, "PVEFW-VENET-IN", $hostfw_options); + ruleset_chain_add_input_filters($ruleset, "PVEFW-VENET-IN", $hostfw_options, $cluster_conf, $loglevel); - ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o venet0 -j PVEFW-VENET-IN"); + ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o venet0 -m set --match-set PVEFW-venet0 dst -j PVEFW-VENET-IN"); generate_std_chains($ruleset, $hostfw_options); @@ -2606,7 +2604,7 @@ sub compile { enable_host_firewall($ruleset, $hostfw_conf, $cluster_conf) if $hostfw_enable; - ruleset_addrule($ruleset, "PVEFW-OUTPUT", "-o venet0 -j PVEFW-VENET-IN"); + ruleset_addrule($ruleset, "PVEFW-OUTPUT", "-o venet0 -m set --match-set PVEFW-venet0 dst -j PVEFW-VENET-IN"); # generate firewall rules for QEMU VMs foreach my $vmid (keys %{$vmdata->{qemu}}) { @@ -2618,7 +2616,7 @@ sub compile { foreach my $netid (keys %$conf) { next if $netid !~ m/^net(\d+)$/; my $net = PVE::QemuServer::parse_net($conf->{$netid}); - next if !$net; + next if !$net->{firewall}; my $iface = "tap${vmid}i$1"; my $macaddr = $net->{macaddr}; @@ -2639,6 +2637,17 @@ sub compile { 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); + + foreach my $singleip (@ips) { + my $venet0ipset = {}; + $venet0ipset->{cidr} = $singleip; + push @{$cluster_conf->{ipset}->{venet0}}, $venet0ipset; + } + generate_venet_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $vmfw_conf, $vmid, $ip, 'IN'); generate_venet_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $vmfw_conf, $vmid, $ip, 'OUT'); } @@ -2662,6 +2671,9 @@ sub compile { ruleset_insertrule($ruleset, "PVEFW-FORWARD", "-m conntrack --ctstate RELATED,ESTABLISHED -j PVEFW-IPS"); } + my $ipset_ruleset = {}; + generate_ipset_chains($ipset_ruleset, $cluster_conf); + return ($ruleset, $ipset_ruleset); } @@ -2781,7 +2793,7 @@ sub get_ipset_cmdlist { my $active_chains = ipset_get_chains(); my $statushash = get_ruleset_status($ruleset, $active_chains, \&ipset_chain_digest, $verbose); - # remove stale _swap chains + # remove stale _swap chains foreach my $chain (keys %$active_chains) { if ($chain =~ m/^PVEFW-\S+_swap$/) { $cmdlist .= "destroy $chain\n"; @@ -2932,7 +2944,7 @@ sub remove_pvefw_chains { } sub update { - my ($start, $verbose) = @_; + my ($verbose) = @_; my $code = sub { @@ -2941,9 +2953,7 @@ sub update { my $enable = $cluster_options->{enable}; - my $status = read_pvefw_status(); - - die "Firewall is disabled - cannot start\n" if !$enable && $start; + die "Firewall is disabled - cannot start\n" if !$enable; if (!$enable) { PVE::Firewall::remove_pvefw_chains(); @@ -2955,14 +2965,7 @@ sub update { my ($ruleset, $ipset_ruleset) = compile($cluster_conf, $hostfw_conf); - if ($start || $status eq 'active') { - - save_pvefw_status('active') if ($status ne 'active'); - - apply_ruleset($ruleset, $hostfw_conf, $ipset_ruleset, $verbose); - } else { - print "Firewall not active (status = $status)\n" if $verbose; - } + apply_ruleset($ruleset, $hostfw_conf, $ipset_ruleset, $verbose); }; run_locked($code);