use File::Path;
use IO::File;
use Net::IP;
-use PVE::Tools qw(run_command lock_file);
+use PVE::Tools qw(run_command lock_file dir_glob_foreach);
use Encode;
my $hostfw_conf_filename = "/etc/pve/local/host.fw";
maxLength => 20,
});
-my $security_group_pattern = '[A-Za-z][A-Za-z0-9\-\_]+';
+PVE::JSONSchema::register_standard_option('pve-fw-loglevel' => {
+ description => "Log level.",
+ type => 'string',
+ enum => ['emerg', 'alert', 'crit', 'err', 'warning', 'notice', 'info', 'debug', 'nolog'],
+ optional => 1,
+});
+
+my $security_group_name_pattern = '[A-Za-z][A-Za-z0-9\-\_]+';
+my $ip_alias_pattern = '[A-Za-z][A-Za-z0-9\-\_]+';
PVE::JSONSchema::register_standard_option('pve-security-group-name', {
description => "Security Group name.",
type => 'string',
- pattern => $security_group_pattern,
+ pattern => $security_group_name_pattern,
minLength => 2,
maxLength => 20,
});
my ($str) = @_;
return if $str =~ m/^(\+)(\S+)$/; # ipset ref
+ return if $str =~ m/^${ip_alias_pattern}$/;
my $count = 0;
my $iprange = 0;
description => "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.",
type => 'string',
optional => 1,
- pattern => $security_group_pattern,
+ pattern => $security_group_name_pattern,
maxLength => 20,
minLength => 2,
},
raise_param_exc({ type => "security groups not allowed"})
if !$allow_groups;
raise_param_exc({ action => "invalid characters in security group name"})
- if $rule->{action} !~ m/^${security_group_pattern}$/;
+ if $rule->{action} !~ m/^${security_group_name_pattern}$/;
} else {
raise_param_exc({ type => "unknown rule type '$type'"});
}
return 1 if $name =~ m/^venet0-\d+-(:?IN|OUT)$/;
- return 1 if $name =~ m/^vmbr\d+-(:?FW|IN|OUT|IPS)$/;
+ return 1 if $name =~ m/^vmbr\d+(v\d+)?-(:?FW|IN|OUT|IPS)$/;
return 1 if $name =~ m/^GROUP-(:?[^\s\-]+)-(:?IN|OUT)$/;
return undef;
my $dest = $rule->{dest};
if ($source) {
- if ($source =~ m/^(\+)(\S+)$/) {
- die "no such ipset $2" if !$cluster_conf->{ipset}->{$2};
- push @cmd, "-m set --match-set PVEFW-$2 src";
+ if ($source =~ m/^\+/) {
+ if ($source =~ m/^\+(${security_group_name_pattern})$/) {
+ die "no such ipset '$1'\n" if !$cluster_conf->{ipset}->{$1};
+ push @cmd, "-m set --match-set PVEFW-$1 src";
+ } else {
+ die "invalid security group name '$source'\n";
+ }
+ } elsif ($source =~ m/^${ip_alias_pattern}$/){
+ die "no such alias $source\n" if !$cluster_conf->{aliases}->{$source};
+ push @cmd, "-s $cluster_conf->{aliases}->{$source}";
} elsif ($source =~ m/\-/){
push @cmd, "-m iprange --src-range $source";
}
if ($dest) {
- if ($dest =~ m/^(\+)(\S+)$/) {
- die "no such ipset $2" if !$cluster_conf->{ipset}->{$2};
- push @cmd, "-m set --match-set PVEFW-$2 dst";
+ if ($dest =~ m/^\+/) {
+ if ($dest =~ m/^\+(${security_group_name_pattern})$/) {
+ die "no such ipset '$1'\n" if !$cluster_conf->{ipset}->{$1};
+ push @cmd, "-m set --match-set PVEFW-$1 dst";
+ } else {
+ die "invalid security group name '$dest'\n";
+ }
+ } elsif ($dest =~ m/^${ip_alias_pattern}$/){
+ die "no such alias $dest" if !$cluster_conf->{aliases}->{$dest};
+ push @cmd, "-d $cluster_conf->{aliases}->{$dest}";
} elsif ($dest =~ m/^(\d+)\.(\d+).(\d+).(\d+)\-(\d+)\.(\d+).(\d+).(\d+)$/){
push @cmd, "-m iprange --dst-range $dest";
} else {
- push @cmd, "-s $dest";
+ push @cmd, "-d $dest";
}
}
$rules = [ $rule ];
}
+ # update all or nothing
+
+ my @cmds = ();
foreach my $tmp (@$rules) {
if (my $cmdstr = ruleset_generate_cmdstr($ruleset, $chain, $tmp, $actions, $goto, $cluster_conf)) {
- ruleset_addrule($ruleset, $chain, $cmdstr);
+ push @cmds, $cmdstr;
}
}
+
+ foreach my $cmdstr (@cmds) {
+ ruleset_addrule($ruleset, $chain, $cmdstr);
+ }
}
sub ruleset_generate_rule_insert {
}
sub generate_bridge_chains {
- my ($ruleset, $hostfw_conf, $bridge, $routing_table) = @_;
+ my ($ruleset, $hostfw_conf, $bridge, $routing_table, $bridges_config) = @_;
my $options = $hostfw_conf->{options} || {};
if (!ruleset_chain_exist($ruleset, "$bridge-OUT")) {
ruleset_create_chain($ruleset, "$bridge-OUT");
+
+ if($options->{optimize}){
+ foreach my $interface (@{$bridges_config->{$bridge}}) {
+ ruleset_addrule($ruleset, "$bridge-OUT", "-m physdev --physdev-is-bridged --physdev-in $interface -g PVEFW-SET-ACCEPT-MARK");
+ }
+ }
+
ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-in -j $bridge-OUT");
ruleset_insertrule($ruleset, "PVEFW-INPUT", "-i $bridge -m physdev --physdev-is-in -j $bridge-OUT");
}
if (!ruleset_chain_exist($ruleset, "$bridge-IN")) {
ruleset_create_chain($ruleset, "$bridge-IN");
+
+ if($options->{optimize}){
+ foreach my $interface (@{$bridges_config->{$bridge}}) {
+ ruleset_addrule($ruleset, "$bridge-IN", "-m physdev --physdev-is-bridged --physdev-out $interface -j ACCEPT");
+ }
+ }
+
ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-out -j $bridge-IN");
ruleset_addrule($ruleset, "$bridge-FW", "-m mark --mark 1 -j ACCEPT");
# accept traffic to unmanaged bridge ports
}
sub ruleset_create_vm_chain {
- my ($ruleset, $chain, $options, $macaddr, $direction) = @_;
+ my ($ruleset, $chain, $options, $host_options, $macaddr, $direction) = @_;
ruleset_create_chain($ruleset, $chain);
my $accept = generate_nfqueue($options);
- if (!(defined($options->{nosmurfs}) && $options->{nosmurfs} == 0)) {
+ if (!(defined($host_options->{nosmurfs}) && $host_options->{nosmurfs} == 0)) {
ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID,NEW -j PVEFW-smurfs");
}
}
}
- if ($options->{tcpflags}) {
+ if ($host_options->{tcpflags}) {
ruleset_addrule($ruleset, $chain, "-p tcp -j PVEFW-tcpflags");
}
my $lc_direction = lc($direction);
+ my $in_accept = generate_nfqueue($options);
+
foreach my $rule (@$rules) {
next if $rule->{iface} && $rule->{iface} ne $netid;
next if !$rule->{enable};
ruleset_generate_rule($ruleset, $chain, $rule,
{ ACCEPT => "PVEFW-SET-ACCEPT-MARK", REJECT => "PVEFW-reject" }, undef, $cluster_conf);
} else {
- my $accept = generate_nfqueue($options);
- ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept , REJECT => "PVEFW-reject" }, undef, $cluster_conf);
+ ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $in_accept , REJECT => "PVEFW-reject" }, undef, $cluster_conf);
}
}
}
sub generate_nfqueue {
my ($options) = @_;
- my $action = "";
- if($options->{ips}){
- $action = "NFQUEUE";
- if($options->{ips_queues} && $options->{ips_queues} =~ m/^(\d+)(:(\d+))?$/) {
- if(defined($3) && defined($1)) {
+ if ($options->{ips}) {
+ my $action = "NFQUEUE";
+ if ($options->{ips_queues} && $options->{ips_queues} =~ m/^(\d+)(:(\d+))?$/) {
+ if (defined($3) && defined($1)) {
$action .= " --queue-balance $1:$3";
- }elsif (defined($1)) {
+ } elsif (defined($1)) {
$action .= " --queue-num $1";
}
}
- $action .= " --queue-bypass";
- }else{
- $action = "ACCEPT";
+ $action .= " --queue-bypass" if $feature_ipset_nomatch; #need kernel 3.10
+ return $action;
+ } else {
+ return "ACCEPT";
}
-
- return $action;
}
sub ruleset_generate_vm_ipsrules {
}
sub generate_venet_rules_direction {
- my ($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, $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 $rules = $vmfw_conf->{rules};
my $options = $vmfw_conf->{options};
+ my $hostfw_options = $vmfw_conf->{options};
my $loglevel = get_option_log_level($options, "log_level_${lc_direction}");
my $chain = "venet0-$vmid-$direction";
- ruleset_create_vm_chain($ruleset, $chain, $options, undef, $direction);
+ ruleset_create_vm_chain($ruleset, $chain, $options, $hostfw_options, undef, $direction);
ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $chain, 'venet', $direction);
}
sub generate_tap_rules_direction {
- my ($ruleset, $cluster_conf, $iface, $netid, $macaddr, $vmfw_conf, $vmid, $bridge, $direction) = @_;
+ my ($ruleset, $cluster_conf, $hostfw_conf, $iface, $netid, $macaddr, $vmfw_conf, $vmid, $bridge, $direction) = @_;
my $lc_direction = lc($direction);
my $rules = $vmfw_conf->{rules};
my $options = $vmfw_conf->{options};
+ my $hostfw_options = $hostfw_conf->{options};
my $loglevel = get_option_log_level($options, "log_level_${lc_direction}");
my $tapchain = "$iface-$direction";
- ruleset_create_vm_chain($ruleset, $tapchain, $options, $macaddr, $direction);
+ ruleset_create_vm_chain($ruleset, $tapchain, $options, $hostfw_options, $macaddr, $direction);
ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $tapchain, $netid, $direction, $options);
# plug the tap chain to bridge chain
if ($direction eq 'IN') {
- ruleset_insertrule($ruleset, "$bridge-IN",
+ ruleset_addrule($ruleset, "$bridge-IN",
"-m physdev --physdev-is-bridged --physdev-out $iface -j $tapchain");
} else {
- ruleset_insertrule($ruleset, "$bridge-OUT",
+ ruleset_addrule($ruleset, "$bridge-OUT",
"-m physdev --physdev-in $iface -j $tapchain");
}
}
sub enable_host_firewall {
my ($ruleset, $hostfw_conf, $cluster_conf) = @_;
- # fixme: allow security groups
-
my $options = $hostfw_conf->{options};
my $cluster_options = $cluster_conf->{options};
my $rules = $hostfw_conf->{rules};
+ my $cluster_rules = $cluster_conf->{rules};
# host inbound firewall
my $chain = "PVEFW-HOST-IN";
# we use RETURN because we need to check also tap rules
my $accept_action = 'RETURN';
- foreach my $rule (@$rules) {
+ # 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);
}
# we use RETURN because we may want to check other thigs later
$accept_action = 'RETURN';
- foreach my $rule (@$rules) {
+ # 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);
}
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_pattern}$/;
+ die "invalid characters in group name\n" if $action !~ m/^${security_group_name_pattern}$/;
} else {
die "unknown rule type '$type'\n";
}
my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
- if ($line =~ m/^(enable|dhcp|macfilter|nosmurfs|tcpflags|ips):\s*(0|1)\s*$/i) {
+ if ($line =~ m/^(enable|dhcp|macfilter|ips):\s*(0|1)\s*$/i) {
$opt = lc($1);
$value = int($2);
} elsif ($line =~ m/^(log_level_in|log_level_out):\s*(($loglevels)\s*)?$/i) {
return ($opt, $value);
}
+sub parse_clusterfw_alias {
+ my ($line) = @_;
+
+ my ($opt, $value);
+ if ($line =~ m/^(\S+)\s(\S+)$/) {
+ $opt = lc($1);
+ if($2){
+ $2 =~ s|/32$||;
+ pve_verify_ipv4_or_cidr($2) if $2;
+ $value = $2;
+ }
+ } else {
+ chomp $line;
+ die "can't parse option '$line'\n";
+ }
+
+ return ($opt, $value);
+}
+
sub parse_vm_fw_rules {
my ($filename, $fh) = @_;
next;
}
+ if ($line =~ m/^\[aliases\]$/i) {
+ $section = 'aliases';
+ next;
+ }
+
if ($line =~ m/^\[group\s+(\S+)\]\s*(?:#\s*(.*?)\s*)?$/i) {
$section = 'groups';
$group = lc($1);
$res->{options}->{$opt} = $value;
};
warn "$prefix: $@" if $@;
+ } elsif ($section eq 'aliases') {
+ eval {
+ my ($opt, $value) = parse_clusterfw_alias($line);
+ $res->{aliases}->{$opt} = $value;
+ };
+ warn "$prefix: $@" if $@;
} elsif ($section eq 'rules') {
my $rule;
eval { $rule = parse_fw_rule($line, 1, 1); };
my $nomatch = $1;
my $cidr = $2;
- $cidr =~ s|/32$||;
+ if($cidr !~ m/^${ip_alias_pattern}$/) {
+ $cidr =~ s|/32$||;
- eval { pve_verify_ipv4_or_cidr($cidr); };
- if (my $err = $@) {
- warn "$prefix: $cidr - $err";
- next;
+ eval { pve_verify_ipv4_or_cidr($cidr); };
+ if (my $err = $@) {
+ warn "$prefix: $cidr - $err";
+ next;
+ }
}
my $entry = { cidr => $cidr };
return $vmdata;
};
+sub read_bridges_config {
+
+ my $bridgehash = {};
+
+ dir_glob_foreach('/sys/class/net', 'vmbr(\d+)', sub {
+ my ($bridge) = @_;
+
+ dir_glob_foreach("/sys/class/net/$bridge/brif", '((eth|bond)(\d+)(\.(\d+))?)', sub {
+ my ($interface) = @_;
+ push @{$bridgehash->{$bridge}}, $interface;
+ });
+ });
+
+ return $bridgehash;
+};
+
sub load_vmfw_conf {
my ($vmid) = @_;
# same as shorewall smurflog.
my $chain = 'PVEFW-smurflog';
+ $pve_std_chains->{$chain} = [];
push @{$pve_std_chains->{$chain}}, get_log_rule_base($chain, 0, "DROP: ", $loglevel) if $loglevel;
push @{$pve_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} = [];
+
# 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";
my ($ipset_ruleset, $fw_conf) = @_;
foreach my $ipset (keys %{$fw_conf->{ipset}}) {
- generate_ipset($ipset_ruleset, "PVEFW-$ipset", $fw_conf->{ipset}->{$ipset});
+ generate_ipset($ipset_ruleset, "PVEFW-$ipset", $fw_conf->{ipset}->{$ipset}, $fw_conf->{aliases});
}
}
sub generate_ipset {
- my ($ipset_ruleset, $name, $options) = @_;
+ my ($ipset_ruleset, $name, $options, $aliases) = @_;
my $hashsize = scalar(@$options);
if ($hashsize <= 64) {
# remove duplicates
my $nethash = {};
foreach my $entry (@$options) {
+ my $cidr = $entry->{cidr};
+ if ($cidr =~ m/^${ip_alias_pattern}$/) {
+ if ($aliases->{$cidr}) {
+ $entry->{cidr} = $aliases->{$cidr};
+ } else {
+ warn "no such alias '$cidr'\n" if !$aliases->{$cidr};
+ }
+ }
$nethash->{$entry->{cidr}} = $entry;
}
}
sub compile {
+ my ($cluster_conf, $hostfw_conf) = @_;
+
+ $cluster_conf = load_clusterfw_conf() if !$cluster_conf;
+ $hostfw_conf = load_hostfw_conf() if !$hostfw_conf;
+
my $vmdata = read_local_vm_config();
my $vmfw_configs = read_vm_firewall_configs($vmdata);
my $routing_table = read_proc_net_route();
-
- my $cluster_conf = load_clusterfw_conf();
+
+ my $bridges_config = read_bridges_config();
my $ipset_ruleset = {};
generate_ipset_chains($ipset_ruleset, $cluster_conf);
ruleset_create_chain($ruleset, "PVEFW-FORWARD");
- my $hostfw_conf = load_hostfw_conf();
my $hostfw_options = $hostfw_conf->{options} || {};
generate_std_chains($ruleset, $hostfw_options);
$bridge .= "v$net->{tag}" if $net->{tag};
- generate_bridge_chains($ruleset, $hostfw_conf, $bridge, $routing_table);
+ generate_bridge_chains($ruleset, $hostfw_conf, $bridge, $routing_table, $bridges_config);
my $macaddr = $net->{macaddr};
- generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
+ generate_tap_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $iface, $netid, $macaddr,
$vmfw_conf, $vmid, $bridge, 'IN');
- generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
+ generate_tap_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $iface, $netid, $macaddr,
$vmfw_conf, $vmid, $bridge, 'OUT');
}
}
if ($conf->{ip_address} && $conf->{ip_address}->{value}) {
my $ip = $conf->{ip_address}->{value};
- 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, $hostfw_conf, $vmfw_conf, $vmid, $ip, 'IN');
+ generate_venet_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $vmfw_conf, $vmid, $ip, 'OUT');
}
if ($conf->{netif} && $conf->{netif}->{value}) {
next; # fixme?
}
- generate_bridge_chains($ruleset, $hostfw_conf, $bridge, $routing_table);
+ generate_bridge_chains($ruleset, $hostfw_conf, $bridge, $routing_table, $bridges_config);
my $macaddr = $d->{mac};
my $iface = $d->{host_ifname};
- generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
+ generate_tap_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $iface, $netid, $macaddr,
$vmfw_conf, $vmid, $bridge, 'IN');
- generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
+ generate_tap_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $iface, $netid, $macaddr,
$vmfw_conf, $vmid, $bridge, 'OUT');
}
}
ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o vmbr+ -j DROP");
ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i vmbr+ -j DROP");
- return wantarray ? ($ruleset, $hostfw_conf, $ipset_ruleset) : $ruleset;
+ return ($ruleset, $ipset_ruleset);
}
sub get_ruleset_status {
enable_bridge_firewall();
- update_nf_conntrack_max($hostfw_conf);
-
- update_nf_conntrack_tcp_timeout_established($hostfw_conf);
-
my ($ipset_create_cmdlist, $ipset_delete_cmdlist, $ipset_changes) =
get_ipset_cmdlist($ipset_ruleset, undef, $verbose);
}
die "unable to apply firewall changes\n" if $errors;
+
+ update_nf_conntrack_max($hostfw_conf);
+
+ update_nf_conntrack_tcp_timeout_established($hostfw_conf);
+
}
sub update_nf_conntrack_max {
my ($start, $verbose) = @_;
my $code = sub {
+
+ my $cluster_conf = load_clusterfw_conf();
+ my $cluster_options = $cluster_conf->{options};
+
+ my $enable = $cluster_options->{enable};
+
my $status = read_pvefw_status();
- my ($ruleset, $hostfw_conf, $ipset_ruleset) = compile();
+ die "Firewall is disabled - cannot start\n" if !$enable && $start;
+
+ if (!$enable) {
+ PVE::Firewall::remove_pvefw_chains();
+ print "Firewall disabled\n" if $verbose;
+ return;
+ }
+
+ my $hostfw_conf = load_hostfw_conf();
+
+ my ($ruleset, $ipset_ruleset) = compile($cluster_conf, $hostfw_conf);
if ($start || $status eq 'active') {