use Data::Dumper;
use Digest::SHA;
use PVE::INotify;
+use PVE::JSONSchema qw(get_standard_option);
use PVE::Cluster;
use PVE::ProcFSTools;
use PVE::Tools;
'address-mask-reply' => 1,
};
-sub get_firewall_macros {
-
- return $pve_fw_parsed_macros if $pve_fw_parsed_macros;
+sub init_firewall_macros {
$pve_fw_parsed_macros = {};
foreach my $k (keys %$pve_fw_macros) {
- my $name = lc($k);
-
- my $macro = $pve_fw_macros->{$k};
- $pve_fw_preferred_macro_names->{$name} = $k;
- $pve_fw_parsed_macros->{$name} = $macro;
+ my $lc_name = lc($k);
+ my $macro = $pve_fw_macros->{$k};
+ $pve_fw_preferred_macro_names->{$lc_name} = $k;
+ $pve_fw_parsed_macros->{$k} = $macro;
}
-
- return $pve_fw_parsed_macros;
}
+init_firewall_macros();
+
my $etc_services;
sub get_etc_services {
return ($nbports);
}
+PVE::JSONSchema::register_format('pve-fw-port-spec', \&pve_fw_verify_port_spec);
+sub pve_fw_verify_port_spec {
+ my ($portstr) = @_;
+
+ parse_port_name_number_or_range($portstr);
+
+ return $portstr;
+}
+
+PVE::JSONSchema::register_format('pve-fw-v4addr-spec', \&pve_fw_verify_v4addr_spec);
+sub pve_fw_verify_v4addr_spec {
+ my ($list) = @_;
+
+ parse_address_list($list);
+
+ return $list;
+}
+
+PVE::JSONSchema::register_format('pve-fw-protocol-spec', \&pve_fw_verify_protocol_spec);
+sub pve_fw_verify_protocol_spec {
+ my ($proto) = @_;
+
+ my $protocols = get_etc_protocols();
+
+ die "unknown protocol '$proto'\n" if $proto &&
+ !(defined($protocols->{byname}->{$proto}) ||
+ defined($protocols->{byid}->{$proto}));
+
+ return $proto;
+}
+
+
# helper function for API
+
+my $rule_properties = {
+ pos => {
+ description => "Update rule at position <pos>.",
+ type => 'integer',
+ minimum => 0,
+ optional => 1,
+ },
+ digest => {
+ type => 'string',
+ optional => 1,
+ maxLength => 27,
+ minLength => 27,
+ },
+ type => {
+ type => 'string',
+ optional => 1,
+ enum => ['in', 'out', 'group'],
+ },
+ action => {
+ type => 'string',
+ optional => 1,
+ enum => ['ACCEPT', 'DROP', 'REJECT'],
+ },
+ macro => {
+ type => 'string',
+ optional => 1,
+ maxLength => 128,
+ },
+ iface => get_standard_option('pve-iface', { optional => 1 }),
+ source => {
+ type => 'string', format => 'pve-fw-v4addr-spec',
+ optional => 1,
+ },
+ dest => {
+ type => 'string', format => 'pve-fw-v4addr-spec',
+ optional => 1,
+ },
+ proto => {
+ type => 'string', format => 'pve-fw-protocol-spec',
+ optional => 1,
+ },
+ enable => {
+ type => 'boolean',
+ optional => 1,
+ },
+ sport => {
+ type => 'string', format => 'pve-fw-port-spec',
+ optional => 1,
+ },
+ dport => {
+ type => 'string', format => 'pve-fw-port-spec',
+ optional => 1,
+ },
+ comment => {
+ type => 'string',
+ optional => 1,
+ },
+};
+
sub cleanup_fw_rule {
my ($rule, $digest, $pos) = @_;
my $r = {};
foreach my $k (keys %$rule) {
- next if $k eq 'nbdport';
- next if $k eq 'nbsport';
+ next if !$rule_properties->{$k};
my $v = $rule->{$k};
next if !defined($v);
$r->{$k} = $v;
return $r;
}
+sub add_rule_properties {
+ my ($properties) = @_;
+
+ foreach my $k (keys %$rule_properties) {
+ $properties->{$k} = $rule_properties->{$k};
+ }
+
+ return $properties;
+}
+
+sub copy_rule_data {
+ my ($rule, $param) = @_;
+
+ foreach my $k (keys %$rule_properties) {
+ if (defined(my $v = $param->{$k})) {
+ if ($v eq '' || $v eq '-') {
+ delete $rule->{$k};
+ } else {
+ $rule->{$k} = $v;
+ }
+ } else {
+ delete $rule->{$k};
+ }
+ }
+ return $rule;
+}
+
+# core functions
my $bridge_firewall_enabled = 0;
sub enable_bridge_firewall {
return 1 if $name =~ m/^venet0-\d+-(:?IN|OUT)$/;
- return 1 if $name =~ m/^vmbr\d+-(:?FW|IN|OUT)$/;
+ return 1 if $name =~ m/^vmbr\d+-(:?FW|IN|OUT|IPS)$/;
return 1 if $name =~ m/^GROUP-(:?[^\s\-]+)-(:?IN|OUT)$/;
return undef;
return if defined($rule->{enable}) && !$rule->{enable};
+ die "unable to emit macro - internal error" if $rule->{macro}; # should not happen
+
+ my $nbdport = defined($rule->{dport}) ? parse_port_name_number_or_range($rule->{dport}) : 0;
+ my $nbsport = defined($rule->{sport}) ? parse_port_name_number_or_range($rule->{sport}) : 0;
+ my $nbsource = $rule->{source} ? parse_address_list( $rule->{source}) : 0;
+ my $nbdest = $rule->{dest} ? parse_address_list($rule->{dest}) : 0;
+
my @cmd = ();
push @cmd, "-i $rule->{iface_in}" if $rule->{iface_in};
push @cmd, "-o $rule->{iface_out}" if $rule->{iface_out};
- push @cmd, "-m iprange --src-range" if $rule->{nbsource} && $rule->{nbsource} > 1;
+ push @cmd, "-m iprange --src-range" if $nbsource > 1;
push @cmd, "-s $rule->{source}" if $rule->{source};
- push @cmd, "-m iprange --dst-range" if $rule->{nbdest} && $rule->{nbdest} > 1;
+ push @cmd, "-m iprange --dst-range" if $nbdest > 1;
push @cmd, "-d $rule->{dest}" if $rule->{dest};
if ($rule->{proto}) {
push @cmd, "-p $rule->{proto}";
my $multiport = 0;
- $multiport++ if $rule->{nbdport} && ($rule->{nbdport} > 1);
- $multiport++ if $rule->{nbsport} && ($rule->{nbsport} > 1);
+ $multiport++ if $nbdport > 1;
+ $multiport++ if $nbsport > 1;
push @cmd, "--match multiport" if $multiport;
die "unknown icmp-type '$rule->{dport}'\n" if !defined($icmp_type_names->{$rule->{dport}});
push @cmd, "-m icmp --icmp-type $rule->{dport}";
} else {
- if ($rule->{nbdport} && $rule->{nbdport} > 1) {
+ if ($nbdport > 1) {
if ($multiport == 2) {
push @cmd, "--ports $rule->{dport}";
} else {
}
if ($rule->{sport}) {
- if ($rule->{nbsport} && $rule->{nbsport} > 1) {
+ if ($nbsport > 1) {
push @cmd, "--sports $rule->{sport}" if $multiport != 2;
} else {
push @cmd, "--sport $rule->{sport}";
return scalar(@cmd) ? join(' ', @cmd) : undef;
}
+my $apply_macro = sub {
+ my ($macro_name, $param) = @_;
+
+ my $macro_rules = $pve_fw_parsed_macros->{$macro_name};
+ die "unknown macro '$macro_name'\n" if !$macro_rules; # should not happen
+
+ my $rules = [];
+
+ foreach my $templ (@$macro_rules) {
+ my $rule = {};
+ my $param_used = {};
+ foreach my $k (keys %$templ) {
+ my $v = $templ->{$k};
+ if ($v eq 'PARAM') {
+ $v = $param->{$k};
+ $param_used->{$k} = 1;
+ } elsif ($v eq 'DEST') {
+ $v = $param->{dest};
+ $param_used->{dest} = 1;
+ } elsif ($v eq 'SOURCE') {
+ $v = $param->{source};
+ $param_used->{source} = 1;
+ }
+
+ die "missing parameter '$k' in macro '$macro_name'\n" if !defined($v);
+ $rule->{$k} = $v;
+ }
+ foreach my $k (keys %$param) {
+ next if $k eq 'macro';
+ next if !defined($param->{$k});
+ next if $param_used->{$k};
+ if (defined($rule->{$k})) {
+ die "parameter '$k' already define in macro (value = '$rule->{$k}')\n"
+ if $rule->{$k} ne $param->{$k};
+ } else {
+ $rule->{$k} = $param->{$k};
+ }
+ }
+ push @$rules, $rule;
+ }
+
+ return $rules;
+};
+
sub ruleset_generate_rule {
my ($ruleset, $chain, $rule, $actions, $goto) = @_;
- if (my $cmdstr = ruleset_generate_cmdstr($ruleset, $chain, $rule, $actions, $goto)) {
- ruleset_addrule($ruleset, $chain, $cmdstr);
+ my $rules;
+
+ if ($rule->{macro}) {
+ $rules = &$apply_macro($rule->{macro}, $rule);
+ } else {
+ $rules = [ $rule ];
+ }
+
+ foreach my $tmp (@$rules) {
+ if (my $cmdstr = ruleset_generate_cmdstr($ruleset, $chain, $tmp, $actions, $goto)) {
+ ruleset_addrule($ruleset, $chain, $cmdstr);
+ }
}
}
sub ruleset_generate_rule_insert {
my ($ruleset, $chain, $rule, $actions, $goto) = @_;
+ die "implement me" if $rule->{macro}; # not implemented, because not needed so far
+
if (my $cmdstr = ruleset_generate_cmdstr($ruleset, $chain, $rule, $actions, $goto)) {
ruleset_insertrule($ruleset, $chain, $cmdstr);
}
return $action;
}
+sub ruleset_generate_vm_ipsrules {
+ my ($ruleset, $options, $direction, $iface, $bridge) = @_;
+
+ if ($options->{ips} && $direction eq 'IN') {
+ my $nfqueue = generate_nfqueue($options);
+
+ if (!ruleset_chain_exist($ruleset, "$bridge-IPS")) {
+ ruleset_create_chain($ruleset, "PVEFW-IPS");
+ }
+
+ if (!ruleset_chain_exist($ruleset, "$bridge-IPS")) {
+ ruleset_create_chain($ruleset, "$bridge-IPS");
+ ruleset_insertrule($ruleset, "PVEFW-IPS", "-o $bridge -m physdev --physdev-is-out -j $bridge-IPS");
+ }
+
+ ruleset_addrule($ruleset, "$bridge-IPS", "-m physdev --physdev-out $iface --physdev-is-bridged -j $nfqueue");
+ }
+}
+
sub generate_venet_rules_direction {
my ($ruleset, $groups_conf, $vmfw_conf, $vmid, $ip, $direction) = @_;
ruleset_generate_vm_rules($ruleset, $rules, $groups_conf, $tapchain, $netid, $direction, $options);
+ ruleset_generate_vm_ipsrules($ruleset, $options, $direction, $iface, $bridge);
+
# implement policy
my $policy;
sub parse_fw_rule {
my ($line, $need_iface, $allow_groups) = @_;
- my $macros = get_firewall_macros();
- my $protocols = get_etc_protocols();
-
my ($type, $action, $iface, $source, $dest, $proto, $dport, $sport);
# we can add single line comments to the end of the rule
die "incomplete rule\n" if ! ($type && $action);
my $macro;
- my $macro_name;
$type = lc($type);
if ($action =~ m/^(ACCEPT|DROP|REJECT)$/) {
# OK
} elsif ($action =~ m/^(\S+)\((ACCEPT|DROP|REJECT)\)$/) {
- ($macro_name, $action) = ($1, $2);
- my $lc_macro_name = lc($macro_name);
- my $preferred_name = $pve_fw_preferred_macro_names->{$lc_macro_name};
- $macro_name = $preferred_name if $preferred_name;
- $macro = $macros->{$lc_macro_name};
- die "unknown macro '$macro_name'\n" if !$macro;
+ $action = $2;
+ my $preferred_name = $pve_fw_preferred_macro_names->{lc($1)};
+ die "unknown macro '$1'\n" if !$preferred_name;
+ $macro = $preferred_name;
} else {
die "unknown action '$action'\n";
}
}
$proto = undef if $proto && $proto eq '-';
- die "unknown protokol '$proto'\n" if $proto &&
- !(defined($protocols->{byname}->{$proto}) ||
- defined($protocols->{byid}->{$proto}));
+ pve_fw_verify_protocol_spec($proto) if $proto;
$source = undef if $source && $source eq '-';
$dest = undef if $dest && $dest eq '-';
$dport = undef if $dport && $dport eq '-';
$sport = undef if $sport && $sport eq '-';
- my $nbsource = undef;
- my $nbdest = undef;
-
- $nbsource = parse_address_list($source) if $source;
- $nbdest = parse_address_list($dest) if $dest;
+ parse_port_name_number_or_range($dport) if defined($dport);
+ parse_port_name_number_or_range($sport) if defined($sport);
+
+ parse_address_list($source) if $source;
+ parse_address_list($dest) if $dest;
- my $rules = [];
-
- my $param = {
+ return {
type => $type,
enable => $enable,
comment => $comment,
action => $action,
+ macro => $macro,
iface => $iface,
source => $source,
dest => $dest,
- nbsource => $nbsource,
- nbdest => $nbdest,
proto => $proto,
dport => $dport,
sport => $sport,
};
-
- if ($macro) {
- foreach my $templ (@$macro) {
- my $rule = {};
- my $param_used = {};
- foreach my $k (keys %$templ) {
- my $v = $templ->{$k};
- if ($v eq 'PARAM') {
- $v = $param->{$k};
- $param_used->{$k} = 1;
- } elsif ($v eq 'DEST') {
- $v = $param->{dest};
- $param_used->{dest} = 1;
- } elsif ($v eq 'SOURCE') {
- $v = $param->{source};
- $param_used->{source} = 1;
- }
-
- die "missing parameter '$k' in macro '$macro_name'\n" if !defined($v);
- $rule->{$k} = $v;
- }
- foreach my $k (keys %$param) {
- next if !defined($param->{$k});
- next if $param_used->{$k};
- if (defined($rule->{$k})) {
- die "parameter '$k' already define in macro (value = '$rule->{$k}')\n"
- if $rule->{$k} ne $param->{$k};
- } else {
- $rule->{$k} = $param->{$k};
- }
- }
- push @$rules, $rule;
- }
- } else {
- push @$rules, $param;
- }
-
- foreach my $rule (@$rules) {
- $rule->{nbdport} = parse_port_name_number_or_range($rule->{dport})
- if defined($rule->{dport});
- $rule->{nbsport} = parse_port_name_number_or_range($rule->{sport})
- if defined($rule->{sport});
- }
-
- return $rules;
}
sub parse_vmfw_option {
next;
}
- my $rules;
- eval { $rules = parse_fw_rule($line, 1, 1); };
+ my $rule;
+ eval { $rule = parse_fw_rule($line, 1, 1); };
if (my $err = $@) {
warn "$prefix: $err";
next;
}
- push @{$res->{$section}}, @$rules;
+ push @{$res->{$section}}, $rule;
}
$res->{digest} = $digest->b64digest;
next;
}
- my $rules;
- eval { $rules = parse_fw_rule($line, 1, 1); };
+ my $rule;
+ eval { $rule = parse_fw_rule($line, 1, 1); };
if (my $err = $@) {
warn "$prefix: $err";
next;
}
-
- push @{$res->{$section}}, @$rules;
+
+ push @{$res->{$section}}, $rule;
}
$res->{digest} = $digest->b64digest;
next;
}
- my $rules;
- eval { $rules = parse_fw_rule($line, 0, 0); };
+ my $rule;
+ eval { $rule = parse_fw_rule($line, 0, 0); };
if (my $err = $@) {
warn "$prefix: $err";
next;
}
-
- push @{$res->{$section}->{$group}}, @$rules;
+
+ push @{$res->{$section}->{$group}}, $rule;
}
$res->{digest} = $digest->b64digest;
return $groups_conf;
}
+sub save_security_groups {
+ my ($groups_conf) = @_;
+
+ my $raw = '';
+ my $filename = "/etc/pve/firewall/groups.fw";
+
+ foreach my $group (sort keys %{$groups_conf->{rules}}) {
+ my $rules = $groups_conf->{rules}->{$group};
+ $raw .= "[group $group]\n\n";
+
+ foreach my $rule (@$rules) {
+ if ($rule->{type} eq 'in' || $rule->{type} eq 'out') {
+ $raw .= '|' if defined($rule->{enable}) && !$rule->{enable};
+ $raw .= uc($rule->{type});
+ $raw .= " " . $rule->{action};
+ $raw .= " " . ($rule->{source} || '-');
+ $raw .= " " . ($rule->{dest} || '-');
+ $raw .= " " . ($rule->{proto} || '-');
+ $raw .= " " . ($rule->{dport} || '-');
+ $raw .= " " . ($rule->{sport} || '-');
+ $raw .= " # " . encode('utf8', $rule->{comment})
+ if $rule->{comment} && $rule->{comment} !~ m/^\s*$/;
+ $raw .= "\n";
+ } else {
+ die "implement me '$rule->{type}'";
+ }
+ }
+
+ $raw .= "\n";
+ }
+
+ PVE::Tools::file_set_contents($filename, $raw);
+}
+
sub load_hostfw_conf {
my $hostfw_conf = {};
enable_host_firewall($ruleset, $hostfw_conf, $groups_conf) if $hostfw_enable;
+ my $ips_enable = undef;
+
# generate firewall rules for QEMU VMs
foreach my $vmid (keys %{$vmdata->{qemu}}) {
my $conf = $vmdata->{qemu}->{$vmid};
next if !$vmfw_conf;
next if defined($vmfw_conf->{options}->{enable}) && ($vmfw_conf->{options}->{enable} == 0);
+ $ips_enable = 1 if $vmfw_conf->{options}->{ips};
+
foreach my $netid (keys %$conf) {
next if $netid !~ m/^net(\d+)$/;
my $net = PVE::QemuServer::parse_net($conf->{$netid});
next if !$vmfw_conf;
next if defined($vmfw_conf->{options}->{enable}) && ($vmfw_conf->{options}->{enable} == 0);
+ $ips_enable = 1 if $vmfw_conf->{options}->{ips};
+
if ($conf->{ip_address} && $conf->{ip_address}->{value}) {
my $ip = $conf->{ip_address}->{value};
generate_venet_rules_direction($ruleset, $groups_conf, $vmfw_conf, $vmid, $ip, 'IN');
}
if($hostfw_options->{optimize}){
- ruleset_insertrule($ruleset, "PVEFW-FORWARD", "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
+
+ my $accept = $ips_enable ? "PVEFW-IPS" : "ACCEPT";
+ ruleset_insertrule($ruleset, "PVEFW-FORWARD", "-m conntrack --ctstate RELATED,ESTABLISHED -j $accept");
ruleset_insertrule($ruleset, "PVEFW-FORWARD", "-m conntrack --ctstate INVALID -j DROP");
}