+ my $ipversion;
+ my $set_ip_version = sub {
+ my $vers = shift;
+ if ($vers) {
+ die "detected mixed ipv4/ipv6 adresses in rule\n"
+ if $ipversion && ($vers != $ipversion);
+ $ipversion = $vers;
+ }
+ };
+
+ my $check_ipset_or_alias_property = sub {
+ my ($name, $expected_ipversion) = @_;
+
+ if (my $value = $rule->{$name}) {
+ if ($value =~ m/^\+/) {
+ if ($value =~ m/^\+(${ipset_name_pattern})$/) {
+ &$add_error($name, "no such ipset '$1'")
+ if !($cluster_conf->{ipset}->{$1} || ($fw_conf && $fw_conf->{ipset}->{$1}));
+
+ } else {
+ &$add_error($name, "invalid ipset name '$value'");
+ }
+ } elsif ($value =~ m/^${ip_alias_pattern}$/){
+ my $alias = lc($value);
+ &$add_error($name, "no such alias '$value'")
+ if !($cluster_conf->{aliases}->{$alias} || ($fw_conf && $fw_conf->{aliases}->{$alias}));
+ my $e = $fw_conf ? $fw_conf->{aliases}->{$alias} : undef;
+ $e = $cluster_conf->{aliases}->{$alias} if !$e && $cluster_conf;
+
+ &$set_ip_version($e->{ipversion});
+ }
+ }
+ };
+
+ my $type = $rule->{type};
+ my $action = $rule->{action};
+
+ &$add_error('type', "missing property") if !$type;
+ &$add_error('action', "missing property") if !$action;
+
+ if ($type) {
+ if ($type eq 'in' || $type eq 'out') {
+ &$add_error('action', "unknown action '$action'")
+ if $action && ($action !~ m/^(ACCEPT|DROP|REJECT)$/);
+ } elsif ($type eq 'group') {
+ &$add_error('type', "security groups not allowed")
+ if !$allow_groups;
+ &$add_error('action', "invalid characters in security group name")
+ if $action && ($action !~ m/^${security_group_name_pattern}$/);
+ } else {
+ &$add_error('type', "unknown rule type '$type'");
+ }
+ }
+
+ if ($rule->{iface}) {
+ &$add_error('type', "parameter -i not allowed for this rule type")
+ if !$allow_iface;
+ eval { PVE::JSONSchema::pve_verify_iface($rule->{iface}); };
+ &$add_error('iface', $@) if $@;
+ if ($rule_env eq 'vm') {
+ &$add_error('iface', "value does not match the regex pattern 'net\\d+'")
+ if $rule->{iface} !~ m/^net(\d+)$/;
+ } elsif ($rule_env eq 'ct') {
+ &$add_error('iface', "value does not match the regex pattern '(venet|eth\\d+)'")
+ if $rule->{iface} !~ m/^(venet|eth(\d+))$/;
+ }
+ }
+
+ if ($rule->{macro}) {
+ if (my $preferred_name = $pve_fw_preferred_macro_names->{lc($rule->{macro})}) {
+ $rule->{macro} = $preferred_name;
+ } else {
+ &$add_error('macro', "unknown macro '$rule->{macro}'");
+ }
+ }
+
+ if ($rule->{proto}) {
+ eval { pve_fw_verify_protocol_spec($rule->{proto}); };
+ &$add_error('proto', $@) if $@;
+ &$set_ip_version(4) if $rule->{proto} eq 'icmp';
+ &$set_ip_version(6) if $rule->{proto} eq 'icmpv6';
+ }
+
+ if ($rule->{dport}) {
+ eval { parse_port_name_number_or_range($rule->{dport}); };
+ &$add_error('dport', $@) if $@;
+ &$add_error('proto', "missing property - 'dport' requires this property")
+ if !$rule->{proto};
+ }
+
+ if ($rule->{sport}) {
+ eval { parse_port_name_number_or_range($rule->{sport}); };
+ &$add_error('sport', $@) if $@;
+ &$add_error('proto', "missing property - 'sport' requires this property")
+ if !$rule->{proto};
+ }
+
+ if ($rule->{source}) {
+ eval {
+ my $source_ipversion = parse_address_list($rule->{source});
+ &$set_ip_version($source_ipversion);
+ };
+ &$add_error('source', $@) if $@;
+ &$check_ipset_or_alias_property('source', $ipversion);
+ }
+
+ if ($rule->{dest}) {
+ eval {
+ my $dest_ipversion = parse_address_list($rule->{dest});
+ &$set_ip_version($dest_ipversion);
+ };
+ &$add_error('dest', $@) if $@;
+ &$check_ipset_or_alias_property('dest', $ipversion);
+ }
+
+ $rule->{ipversion} = $ipversion if $ipversion;
+
+ if ($rule->{macro} && !$error_count) {
+ eval { &$apply_macro($rule->{macro}, $rule, 1, $ipversion); };
+ if (my $err = $@) {
+ if (ref($err) eq "PVE::Exception" && $err->{errors}) {
+ my $eh = $err->{errors};
+ foreach my $p (keys %$eh) {
+ &$add_error($p, $eh->{$p});
+ }
+ } else {
+ &$add_error('macro', "$err");
+ }
+ }
+ }
+
+ $rule->{errors} = $errors if $error_count;
+
+ return $rule;
+}
+
+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;
+ }
+ }
+ }
+
+ 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;
+
+sub enable_bridge_firewall {
+
+ return if $bridge_firewall_enabled; # only once
+
+ PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-iptables", "1");
+ PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-ip6tables", "1");
+
+ # make sure syncookies are enabled (which is default on newer 3.X kernels anyways)
+ PVE::ProcFSTools::write_proc_entry("/proc/sys/net/ipv4/tcp_syncookies", "1");
+
+ $bridge_firewall_enabled = 1;
+}
+
+my $rule_format = "%-15s %-30s %-30s %-15s %-15s %-15s\n";
+
+sub iptables_restore_cmdlist {
+ my ($cmdlist) = @_;
+
+ run_command("/sbin/iptables-restore -n", input => $cmdlist, errmsg => "iptables_restore_cmdlist");
+}
+
+sub ip6tables_restore_cmdlist {
+ my ($cmdlist) = @_;
+
+ run_command("/sbin/ip6tables-restore -n", input => $cmdlist, errmsg => "iptables_restore_cmdlist");
+}
+
+sub ipset_restore_cmdlist {
+ my ($cmdlist) = @_;
+
+ run_command("/usr/sbin/ipset restore", input => $cmdlist, errmsg => "ipset_restore_cmdlist");
+}
+
+sub iptables_get_chains {
+ my ($iptablescmd) = @_;
+
+ $iptablescmd = "iptables" if !$iptablescmd;
+
+ my $res = {};
+
+ # check what chains we want to track
+ my $is_pvefw_chain = sub {
+ my $name = shift;
+
+ return 1 if $name =~ m/^PVEFW-\S+$/;
+
+ return 1 if $name =~ m/^tap\d+i\d+-(:?IN|OUT)$/;
+
+ return 1 if $name =~ m/^veth\d+.\d+-(:?IN|OUT)$/; # fixme: dev name is configurable
+
+ return 1 if $name =~ m/^venet0-\d+-(:?IN|OUT)$/;
+
+ return 1 if $name =~ m/^fwbr\d+(v\d+)?-(:?FW|IN|OUT|IPS)$/;
+ return 1 if $name =~ m/^GROUP-(:?[^\s\-]+)-(:?IN|OUT)$/;
+
+ return undef;
+ };
+
+ my $table = '';
+
+ my $hooks = {};
+
+ my $parser = sub {
+ my $line = shift;
+
+ return if $line =~ m/^#/;
+ return if $line =~ m/^\s*$/;
+
+ if ($line =~ m/^\*(\S+)$/) {
+ $table = $1;
+ return;
+ }
+
+ return if $table ne 'filter';
+
+ if ($line =~ m/^:(\S+)\s/) {
+ my $chain = $1;
+ return if !&$is_pvefw_chain($chain);
+ $res->{$chain} = "unknown";
+ } elsif ($line =~ m/^-A\s+(\S+)\s.*--comment\s+\"PVESIG:(\S+)\"/) {
+ my ($chain, $sig) = ($1, $2);
+ return if !&$is_pvefw_chain($chain);
+ $res->{$chain} = $sig;
+ } elsif ($line =~ m/^-A\s+(INPUT|OUTPUT|FORWARD)\s+-j\s+PVEFW-\1$/) {
+ $hooks->{$1} = 1;
+ } else {
+ # simply ignore the rest
+ return;
+ }
+ };
+
+ run_command("/sbin/$iptablescmd-save", outfunc => $parser);
+
+ return wantarray ? ($res, $hooks) : $res;
+}
+
+sub iptables_chain_digest {
+ my ($rules) = @_;
+ my $digest = Digest::SHA->new('sha1');
+ foreach my $rule (@$rules) { # order is important
+ $digest->add($rule);
+ }
+ return $digest->b64digest;
+}
+
+sub ipset_chain_digest {
+ my ($rules) = @_;
+
+ my $digest = Digest::SHA->new('sha1');
+ foreach my $rule (sort @$rules) { # note: sorted
+ $digest->add($rule);
+ }
+ return $digest->b64digest;
+}
+
+sub ipset_get_chains {
+
+ my $res = {};
+ my $chains = {};
+
+ my $parser = sub {
+ my $line = shift;
+
+ return if $line =~ m/^#/;
+ return if $line =~ m/^\s*$/;
+ if ($line =~ m/^(?:\S+)\s(PVEFW-\S+)\s(?:\S+).*/) {
+ my $chain = $1;
+ $line =~ s/\s+$//; # delete trailing white space
+ push @{$chains->{$chain}}, $line;
+ } else {
+ # simply ignore the rest
+ return;
+ }
+ };
+
+ run_command("/usr/sbin/ipset save", outfunc => $parser);
+
+ # compute digest for each chain
+ foreach my $chain (keys %$chains) {
+ $res->{$chain} = ipset_chain_digest($chains->{$chain});
+ }
+
+ return $res;
+}
+
+sub ruleset_generate_cmdstr {
+ my ($ruleset, $chain, $ipversion, $rule, $actions, $goto, $cluster_conf, $fw_conf) = @_;
+
+ return if defined($rule->{enable}) && !$rule->{enable};
+ return if $rule->{errors};
+
+ 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 @cmd = ();
+
+ push @cmd, "-i $rule->{iface_in}" if $rule->{iface_in};
+ push @cmd, "-o $rule->{iface_out}" if $rule->{iface_out};
+
+ my $source = $rule->{source};
+ my $dest = $rule->{dest};
+
+ if ($source) {
+ if ($source =~ m/^\+/) {
+ if ($source =~ m/^\+(${ipset_name_pattern})$/) {
+ my $name = $1;
+ if ($fw_conf && $fw_conf->{ipset}->{$name}) {
+ my $ipset_chain = compute_ipset_chain_name($fw_conf->{vmid}, $name, $ipversion);
+ push @cmd, "-m set --match-set ${ipset_chain} src";
+ } elsif ($cluster_conf && $cluster_conf->{ipset}->{$name}) {
+ my $ipset_chain = compute_ipset_chain_name(0, $name, $ipversion);
+ push @cmd, "-m set --match-set ${ipset_chain} src";
+ } else {
+ die "no such ipset '$name'\n";
+ }
+ } else {
+ die "invalid security group name '$source'\n";
+ }
+ } elsif ($source =~ m/^${ip_alias_pattern}$/){
+ my $alias = lc($source);
+ my $e = $fw_conf ? $fw_conf->{aliases}->{$alias} : undef;
+ $e = $cluster_conf->{aliases}->{$alias} if !$e && $cluster_conf;
+ die "no such alias '$source'\n" if !$e;
+ push @cmd, "-s $e->{cidr}";
+ } elsif ($source =~ m/\-/){
+ push @cmd, "-m iprange --src-range $source";
+ } else {
+ push @cmd, "-s $source";
+ }
+ }
+
+ if ($dest) {
+ if ($dest =~ m/^\+/) {
+ if ($dest =~ m/^\+(${ipset_name_pattern})$/) {
+ my $name = $1;
+ if ($fw_conf && $fw_conf->{ipset}->{$name}) {
+ my $ipset_chain = compute_ipset_chain_name($fw_conf->{vmid}, $name, $ipversion);
+ push @cmd, "-m set --match-set ${ipset_chain} dst";
+ } elsif ($cluster_conf && $cluster_conf->{ipset}->{$name}) {
+ my $ipset_chain = compute_ipset_chain_name(0, $name, $ipversion);
+ push @cmd, "-m set --match-set ${ipset_chain} dst";
+ } else {
+ die "no such ipset '$name'\n";
+ }
+ } else {
+ die "invalid security group name '$dest'\n";
+ }
+ } elsif ($dest =~ m/^${ip_alias_pattern}$/){
+ my $alias = lc($dest);
+ my $e = $fw_conf ? $fw_conf->{aliases}->{$alias} : undef;
+ $e = $cluster_conf->{aliases}->{$alias} if !$e && $cluster_conf;
+ die "no such alias '$dest'\n" if !$e;
+ push @cmd, "-d $e->{cidr}";
+ } elsif ($dest =~ m/^(\d+)\.(\d+).(\d+).(\d+)\-(\d+)\.(\d+).(\d+).(\d+)$/){
+ push @cmd, "-m iprange --dst-range $dest";
+ } else {
+ push @cmd, "-d $dest";
+ }
+ }
+
+ if ($rule->{proto}) {
+ push @cmd, "-p $rule->{proto}";
+
+ my $multiport = 0;
+ $multiport++ if $nbdport > 1;
+ $multiport++ if $nbsport > 1;
+
+ push @cmd, "--match multiport" if $multiport;
+
+ die "multiport: option '--sports' cannot be used together with '--dports'\n"
+ if ($multiport == 2) && ($rule->{dport} ne $rule->{sport});
+
+ if ($rule->{dport}) {
+ if ($rule->{proto} && $rule->{proto} eq 'icmp') {
+ # Note: we use dport to store --icmp-type
+ die "unknown icmp-type '$rule->{dport}'\n" if !defined($icmp_type_names->{$rule->{dport}});
+ push @cmd, "-m icmp --icmp-type $rule->{dport}";
+ } elsif ($rule->{proto} && $rule->{proto} eq 'icmpv6') {
+ # Note: we use dport to store --icmpv6-type
+ die "unknown icmpv6-type '$rule->{dport}'\n" if !defined($icmpv6_type_names->{$rule->{dport}});
+ push @cmd, "-m icmpv6 --icmpv6-type $rule->{dport}";
+ } else {
+ if ($nbdport > 1) {
+ if ($multiport == 2) {
+ push @cmd, "--ports $rule->{dport}";
+ } else {
+ push @cmd, "--dports $rule->{dport}";
+ }
+ } else {
+ push @cmd, "--dport $rule->{dport}";
+ }
+ }
+ }
+
+ if ($rule->{sport}) {
+ if ($nbsport > 1) {
+ push @cmd, "--sports $rule->{sport}" if $multiport != 2;
+ } else {
+ push @cmd, "--sport $rule->{sport}";
+ }
+ }
+ } elsif ($rule->{dport} || $rule->{sport}) {
+ die "destination port '$rule->{dport}', but no protocol specified\n" if $rule->{dport};
+ die "source port '$rule->{sport}', but no protocol specified\n" if $rule->{sport};
+ }
+
+ push @cmd, "-m addrtype --dst-type $rule->{dsttype}" if $rule->{dsttype};
+
+ if (my $action = $rule->{action}) {
+ $action = $actions->{$action} if defined($actions->{$action});
+ $goto = 1 if !defined($goto) && $action eq 'PVEFW-SET-ACCEPT-MARK';
+ push @cmd, $goto ? "-g $action" : "-j $action";
+ }
+
+ return scalar(@cmd) ? join(' ', @cmd) : undef;
+}
+
+sub ruleset_generate_rule {
+ my ($ruleset, $chain, $ipversion, $rule, $actions, $goto, $cluster_conf, $fw_conf) = @_;
+
+ my $rules;
+
+ if ($rule->{macro}) {
+ $rules = &$apply_macro($rule->{macro}, $rule, 0, $ipversion);
+ } else {
+ $rules = [ $rule ];
+ }
+
+ # update all or nothing
+
+ my @cmds = ();
+ foreach my $tmp (@$rules) {
+ if (my $cmdstr = ruleset_generate_cmdstr($ruleset, $chain, $ipversion, $tmp, $actions, $goto, $cluster_conf, $fw_conf)) {
+ push @cmds, $cmdstr;
+ }
+ }
+
+ foreach my $cmdstr (@cmds) {
+ ruleset_addrule($ruleset, $chain, $cmdstr);
+ }
+}
+
+sub ruleset_generate_rule_insert {
+ my ($ruleset, $chain, $ipversion, $rule, $actions, $goto) = @_;
+
+ die "implement me" if $rule->{macro}; # not implemented, because not needed so far
+
+ if (my $cmdstr = ruleset_generate_cmdstr($ruleset, $chain, $ipversion, $rule, $actions, $goto)) {
+ ruleset_insertrule($ruleset, $chain, $cmdstr);
+ }
+}
+
+sub ruleset_create_chain {
+ my ($ruleset, $chain) = @_;
+
+ die "Invalid chain name '$chain' (28 char max)\n" if length($chain) > 28;
+ die "chain name may not contain collons\n" if $chain =~ m/:/; # because of log format
+
+ die "chain '$chain' already exists\n" if $ruleset->{$chain};
+
+ $ruleset->{$chain} = [];
+}
+
+sub ruleset_chain_exist {
+ my ($ruleset, $chain) = @_;
+
+ return $ruleset->{$chain} ? 1 : undef;
+}
+
+sub ruleset_addrule {
+ my ($ruleset, $chain, $rule) = @_;
+
+ die "no such chain '$chain'\n" if !$ruleset->{$chain};
+
+ push @{$ruleset->{$chain}}, "-A $chain $rule";
+}
+
+sub ruleset_insertrule {
+ my ($ruleset, $chain, $rule) = @_;
+
+ die "no such chain '$chain'\n" if !$ruleset->{$chain};
+
+ unshift @{$ruleset->{$chain}}, "-A $chain $rule";
+}
+
+sub get_log_rule_base {
+ my ($chain, $vmid, $msg, $loglevel) = @_;
+
+ die "internal error - no log level" if !defined($loglevel);
+
+ $vmid = 0 if !defined($vmid);
+
+ # Note: we use special format for prefix to pass further
+ # info to log daemon (VMID, LOGVELEL and CHAIN)
+
+ return "-j NFLOG --nflog-prefix \":$vmid:$loglevel:$chain: $msg\"";
+}
+
+sub ruleset_addlog {
+ my ($ruleset, $chain, $vmid, $msg, $loglevel, $rule) = @_;
+
+ return if !defined($loglevel);
+
+ my $logrule = get_log_rule_base($chain, $vmid, $msg, $loglevel);
+
+ $logrule = "$rule $logrule" if defined($rule);
+
+ ruleset_addrule($ruleset, $chain, $logrule);
+}
+
+sub ruleset_add_chain_policy {
+ my ($ruleset, $chain, $ipversion, $vmid, $policy, $loglevel, $accept_action) = @_;
+
+ if ($policy eq 'ACCEPT') {
+
+ ruleset_generate_rule($ruleset, $chain, $ipversion, { action => 'ACCEPT' },
+ { ACCEPT => $accept_action});
+
+ } elsif ($policy eq 'DROP') {
+
+ ruleset_addrule($ruleset, $chain, "-j PVEFW-Drop");
+
+ ruleset_addlog($ruleset, $chain, $vmid, "policy $policy: ", $loglevel);
+
+ ruleset_addrule($ruleset, $chain, "-j DROP");
+ } elsif ($policy eq 'REJECT') {
+ ruleset_addrule($ruleset, $chain, "-j PVEFW-Reject");
+
+ ruleset_addlog($ruleset, $chain, $vmid, "policy $policy: ", $loglevel);
+
+ ruleset_addrule($ruleset, $chain, "-g PVEFW-reject");