]> git.proxmox.com Git - pve-firewall.git/blobdiff - src/PVE/FirewallSimulator.pm
bump version to 5.0.5
[pve-firewall.git] / src / PVE / FirewallSimulator.pm
index 30ae84718a2dd6f85e5a05b37f8054a20710ed2b..fa5ed0e5e8baa4b6ea10fe7f0035bf97ccfc5028 100644 (file)
@@ -7,18 +7,41 @@ use PVE::Firewall;
 use File::Basename;
 use Net::IP;
 
-my $mark;
+use base 'Exporter';
+our @EXPORT_OK = qw(
+$bridge_name_pattern
+$bridge_interface_pattern
+);
+
+# dynamically include PVE::QemuServer and PVE::LXC
+# to avoid dependency problems
+my $have_qemu_server;
+eval {
+    require PVE::QemuServer;
+    $have_qemu_server = 1;
+};
+
+my $have_lxc;
+eval {
+    require PVE::LXC;
+    $have_lxc = 1;
+};
+
+my $mark = 0;
 my $trace;
 my $debug = 0;
 
+my $NUMBER_RE = qr/0x[0-9a-fA-F]+|\d+/;
+
+our $bridge_name_pattern = '[a-zA-Z][a-zA-Z0-9]{0,9}';
+our $bridge_interface_pattern = "($bridge_name_pattern)/(\\S+)";
+
 sub debug {
     my $new_value = shift;
-
     $debug = $new_value if defined($new_value);
-
     return $debug;
 }
-    
+
 sub reset_trace {
     $trace = '';
 }
@@ -53,21 +76,40 @@ sub nf_dev_match {
 }
 
 sub ipset_match {
-    my ($ipsetname, $ipset, $ipaddr) = @_;
+    my ($ipset_ruleset, $ipsetname, $ipaddr) = @_;
+
+    my $ipset = $ipset_ruleset->{$ipsetname};
+    die "no such ipset '$ipsetname'" if !$ipset;
 
     my $ip = Net::IP->new($ipaddr);
 
-    foreach my $entry (@$ipset) {
-       next if $entry =~ m/^create/; # simply ignore
-       if ($entry =~ m/add \S+ (\S+)$/) {
-           my $test = Net::IP->new($1);
-           if ($test->overlaps($ip)) {
-               add_trace("IPSET $ipsetname match $ipaddr\n");
-               return 1;
+    my $first = $ipset->[0];
+    if ($first =~ m/^create\s+\S+\s+list:/) {
+       foreach my $entry (@$ipset) {
+           next if $entry =~ m/^create/; # simply ignore
+           if ($entry =~ m/add \S+ (\S+)$/) {
+               return 1 if ipset_match($ipset_ruleset, $1, $ipaddr);
+           } else {
+               die "implement me";
+           }
+       }
+       return 0;
+    } elsif ($first =~ m/^create\s+\S+\s+hash:net/) {
+       foreach my $entry (@$ipset) {
+           next if $entry =~ m/^create/; # simply ignore
+           if ($entry =~ m/add \S+ (\S+)$/) {
+               my $test = Net::IP->new($1);
+               if ($test->overlaps($ip)) {
+                   add_trace("IPSET $ipsetname match $ipaddr\n");
+                   return 1;
+               }
+           } else {
+               die "implement me";
            }
-       } else {
-           die "implement me";
        }
+       return 0;
+    } else {
+       die "unknown ipset type '$first' - not implemented\n";
     }
 
     return 0;
@@ -76,7 +118,7 @@ sub ipset_match {
 sub rule_match {
     my ($ipset_ruleset, $chain, $rule, $pkg) = @_;
 
-    $rule =~ s/^-A $chain // || die "got strange rule: $rule";
+    $rule =~ s/^-A $chain +// || die "got strange rule: $rule";
 
     while (length($rule)) {
 
@@ -85,26 +127,33 @@ sub rule_match {
 
            return undef if $cstate eq 'INVALID'; # no match
            return undef if $cstate eq 'RELATED,ESTABLISHED'; # no match
-           
+
            next if $cstate =~ m/NEW/;
-           
+
            die "cstate test '$cstate' not implemented\n";
        }
 
        if ($rule =~ s/^-m addrtype --src-type (\S+)\s*//) {
            my $atype = $1;
-           die "missing source address type (srctype)\n" 
+           die "missing source address type (srctype)\n"
                if !$pkg->{srctype};
            return undef if $atype ne $pkg->{srctype};
        }
 
        if ($rule =~ s/^-m addrtype --dst-type (\S+)\s*//) {
            my $atype = $1;
-           die "missing destination address type (dsttype)\n" 
+           die "missing destination address type (dsttype)\n"
                if !$pkg->{dsttype};
            return undef if $atype ne $pkg->{dsttype};
        }
 
+       if ($rule =~ s/^-m icmp(v6)? --icmp-type (\S+)\s*//) {
+           my $icmpv6 = !!$1;
+           my $icmptype = $2;
+           die "missing destination address type (dsttype)\n" if !defined($pkg->{dport});
+           return undef if $icmptype ne $pkg->{dport};
+       }
+
        if ($rule =~ s/^-i (\S+)\s*//) {
            my $devre = $1;
            die "missing interface (iface_in)\n" if !$pkg->{iface_in};
@@ -143,7 +192,7 @@ sub rule_match {
            return undef if !$ip->overlaps(Net::IP->new($pkg->{source})); # no match
            next;
        }
-    
+
        if ($rule =~ s/^-d (\S+)\s*//) {
            die "missing destination" if !$pkg->{dest};
            my $ip = Net::IP->new($1);
@@ -154,21 +203,19 @@ sub rule_match {
        if ($rule =~ s/^-m set (!\s+)?--match-set (\S+) src\s*//) {
            die "missing source" if !$pkg->{source};
            my $neg = $1;
-           my $ipset = $ipset_ruleset->{$2};
-           die "no such ip set '$2'" if !$ipset;
+           my $ipset_name = $2;
            if ($neg) {
-               return undef if ipset_match($1, $ipset, $pkg->{source});
+               return undef if ipset_match($ipset_ruleset, $ipset_name, $pkg->{source});
            } else {
-               return undef if !ipset_match($1, $ipset, $pkg->{source});
+               return undef if !ipset_match($ipset_ruleset, $ipset_name, $pkg->{source});
            }
            next;
        }
 
        if ($rule =~ s/^-m set --match-set (\S+) dst\s*//) {
            die "missing destination" if !$pkg->{dest};
-           my $ipset = $ipset_ruleset->{$1};
-           die "no such ip set '$1'" if !$ipset;
-           return undef if !ipset_match($1, $ipset, $pkg->{dest});
+           my $ipset_name = $1;
+           return undef if !ipset_match($ipset_ruleset, $ipset_name, $pkg->{dest});
            next;
        }
 
@@ -192,15 +239,17 @@ sub rule_match {
            next;
        }
 
-       if ($rule =~ s/^-m mark --mark (\d+)\s*//) {
-           return undef if !defined($mark) || $mark != $1;
+       if ($rule =~ s@^-m mark --mark ($NUMBER_RE)(?:/($NUMBER_RE))?\s*@@) {
+           my ($value, $mask) = PVE::Firewall::get_mark_values($1, $2);
+           return undef if ($mark & $mask) != $value;
            next;
        }
 
        # final actions
 
-       if ($rule =~ s/^-j MARK --set-mark (\d+)\s*$//) {
-           $mark = $1;
+       if ($rule =~ s@^-j MARK --set-mark ($NUMBER_RE)(?:/($NUMBER_RE))?\s*$@@) {
+           my ($value, $mask) = PVE::Firewall::get_mark_values($1, $2);
+           $mark = ($mark & ~$mask) | $value;
            return undef;
        }
 
@@ -213,7 +262,7 @@ sub rule_match {
        }
 
        if ($rule =~ s/^-j NFLOG --nflog-prefix \"[^\"]+\"$//) {
-           return undef; 
+           return undef;
        }
 
        last;
@@ -226,7 +275,7 @@ sub ruleset_simulate_chain {
     my ($ruleset, $ipset_ruleset, $chain, $pkg) = @_;
 
     add_trace("ENTER chain $chain\n");
-    
+
     my $counter = 0;
 
     if ($chain eq 'PVEFW-Drop') {
@@ -254,7 +303,7 @@ sub ruleset_simulate_chain {
            next;
        }
        add_trace("MATCH: $rule\n");
-       
+
        if ($action eq 'ACCEPT' || $action eq 'DROP' || $action eq 'REJECT') {
            add_trace("TERMINATE chain $chain: $action\n");
            return ($action, $counter);
@@ -301,6 +350,8 @@ sub copy_packet {
 sub route_packet {
     my ($ruleset, $ipset_ruleset, $pkg, $from_info, $target, $start_state) = @_;
 
+    $pkg->{ipversion} = 4; # fixme: allow ipv6
+
     my $route_state = $start_state;
 
     my $physdev_in;
@@ -327,12 +378,7 @@ sub route_packet {
                $pkg->{iface_out} = $target->{bridge} || die 'internal error';
                $chain = 'PVEFW-OUTPUT';
                $next_route_state = $target->{iface} || die 'internal error';
-           } elsif ($target->{type} eq 'ct') {
-               $pkg->{iface_in} = 'lo';
-               $pkg->{iface_out} = 'venet0';
-               $chain = 'PVEFW-OUTPUT';
-               $next_route_state = 'venet-in';
-           } elsif ($target->{type} eq 'vm') {
+           } elsif ($target->{type} eq 'vm' || $target->{type} eq 'ct') {
                $pkg->{iface_in} = 'lo';
                $pkg->{iface_out} = $target->{bridge} || die 'internal error';
                $chain = 'PVEFW-OUTPUT';
@@ -341,40 +387,6 @@ sub route_packet {
                die "implement me";
            }
 
-       } elsif ($route_state eq 'venet-out') {
-
-           if ($target->{type} eq 'host') {
-
-               $chain = 'PVEFW-INPUT';
-               $pkg->{iface_in} = 'venet0';
-               $pkg->{iface_out} = 'lo';
-               $next_route_state = 'host';
-
-           } elsif ($target->{type} eq 'bport') {
-               
-               $chain = 'PVEFW-FORWARD';
-               $pkg->{iface_in} = 'venet0';
-               $pkg->{iface_out} = $target->{bridge} || die 'internal error';
-               $next_route_state = $target->{iface} || die 'internal error';
-
-           } elsif ($target->{type} eq 'vm') {
-
-               $chain = 'PVEFW-FORWARD';
-               $pkg->{iface_in} = 'venet0';
-               $pkg->{iface_out} = $target->{bridge} || die 'internal error';
-               $next_route_state = 'fwbr-in';
-
-           } elsif ($target->{type} eq 'ct') {
-
-               $chain = 'PVEFW-FORWARD';
-               $pkg->{iface_in} = 'venet0';
-               $pkg->{iface_out} = 'venet0';
-               $next_route_state = 'venet-in';
-
-           } else {
-               die "implement me";
-           }
-
        } elsif ($route_state eq 'fwbr-out') {
 
            $chain = 'PVEFW-FORWARD';
@@ -384,7 +396,7 @@ sub route_packet {
            $pkg->{iface_out} = $from_info->{fwbr} || die 'internal error';
            $pkg->{physdev_in} = $from_info->{tapdev} || die 'internal error';
            $pkg->{physdev_out} = $from_info->{fwln} || die 'internal error';
-       
+
        } elsif ($route_state eq 'fwbr-in') {
 
            $chain = 'PVEFW-FORWARD';
@@ -394,8 +406,8 @@ sub route_packet {
            $pkg->{physdev_in} = $target->{fwln} || die 'internal error';
            $pkg->{physdev_out} = $target->{tapdev} || die 'internal error';
 
-       } elsif ($route_state =~ m/^vmbr\d+$/) {
-           
+       } elsif ($route_state =~ m/^$bridge_name_pattern$/) {
+
            die "missing physdev_in - internal error?" if !$physdev_in;
            $pkg->{physdev_in} = $physdev_in;
 
@@ -417,14 +429,7 @@ sub route_packet {
                }
                $next_route_state = $target->{iface};
 
-           } elsif ($target->{type} eq 'ct') {
-
-               $chain = 'PVEFW-FORWARD';
-               $pkg->{iface_in} = $route_state;
-               $pkg->{iface_out} = 'venet0';
-               $next_route_state = 'venet-in';
-
-           } elsif ($target->{type} eq 'vm') {
+           } elsif ($target->{type} eq 'vm' || $target->{type} eq 'ct') {
 
                $chain = 'PVEFW-FORWARD';
                $pkg->{iface_in} = $route_state;
@@ -452,7 +457,7 @@ sub route_packet {
            my ($res, $ctr) = ruleset_simulate_chain($ruleset, $ipset_ruleset, $chain, $pkg);
            $rule_check_counter += $ctr;
            return ($res, $ipt_invocation_counter, $rule_check_counter) if $res ne 'ACCEPT';
-       } 
+       }
 
        $route_state = $next_route_state;
 
@@ -463,16 +468,20 @@ sub route_packet {
 }
 
 sub extract_ct_info {
-    my ($vmdata, $vmid) = @_;
+    my ($vmdata, $vmid, $netnum) = @_;
 
     my $info = { type => 'ct', vmid => $vmid };
 
-    my $conf = $vmdata->{openvz}->{$vmid} || die "no such CT '$vmid'";
-    if ($conf->{ip_address}) {
-       $info->{ip_address} = $conf->{ip_address}->{value};
-    } else {
-       die "implement me";
-    }
+    my $conf = $vmdata->{lxc}->{$vmid} || die "no such CT '$vmid'";
+    my $net = PVE::LXC::Config->parse_lxc_network($conf->{"net$netnum"});
+    $info->{macaddr} = $net->{hwaddr} || die "unable to get mac address";
+    $info->{bridge} = $net->{bridge} || die "unable to get bridge";
+    $info->{fwbr} = "fwbr${vmid}i$netnum";
+    $info->{tapdev} = "veth${vmid}i$netnum";
+    $info->{fwln} = "fwln${vmid}i$netnum";
+    $info->{fwpr} = "fwpr${vmid}p$netnum";
+    $info->{ip_address} = $net->{ip} || die "unable to get ip address";
+
     return $info;
 }
 
@@ -499,9 +508,9 @@ sub simulate_firewall {
     my $from = $test->{from} || die "missing 'from' field";
     my $to = $test->{to} || die "missing 'to' field";
     my $action = $test->{action} || die "missing 'action'";
-    
+
     my $testid = $test->{id};
-    
+
     die "from/to needs to be different" if $from eq $to;
 
     my $pkg = {
@@ -531,11 +540,6 @@ sub simulate_firewall {
        $from_info->{type} = 'host';
        $start_state = 'host';
        $pkg->{source} = $host_ip if !defined($pkg->{source});
-    } elsif ($from =~ m|^(vmbr\d+)/(\S+)$|) {
-       $from_info->{type} = 'bport';
-       $from_info->{bridge} = $1;
-       $from_info->{iface} = $2;
-       $start_state = 'from-bport';
     } elsif ($from eq 'outside') {
        $from_info->{type} = 'bport';
        $from_info->{bridge} = 'vmbr0';
@@ -547,20 +551,23 @@ sub simulate_firewall {
        $from_info->{iface} = 'tapXYZ';
        $start_state = 'from-bport';
     } elsif ($from =~ m/^ct(\d+)$/) {
+       return 'SKIPPED' if !$have_lxc;
        my $vmid = $1;
-       $from_info = extract_ct_info($vmdata, $vmid);
-       if ($from_info->{ip_address}) {
-           $pkg->{source} = $from_info->{ip_address} if !defined($pkg->{source});
-           $start_state = 'venet-out';
-       } else {
-           die "implement me";
-       }
+       $from_info = extract_ct_info($vmdata, $vmid, 0);
+       $start_state = 'fwbr-out';
+       $pkg->{mac_source} = $from_info->{macaddr};
     } elsif ($from =~ m/^vm(\d+)(i(\d))?$/) {
+       return 'SKIPPED' if !$have_qemu_server;
        my $vmid = $1;
        my $netnum = $3 || 0;
        $from_info = extract_vm_info($vmdata, $vmid, $netnum);
-       $start_state = 'fwbr-out'; 
+       $start_state = 'fwbr-out';
        $pkg->{mac_source} = $from_info->{macaddr};
+    } elsif ($from =~ m|^$bridge_interface_pattern$|) {
+       $from_info->{type} = 'bport';
+       $from_info->{bridge} = $1;
+       $from_info->{iface} = $2;
+       $start_state = 'from-bport';
     } else {
        die "unable to parse \"from => '$from'\"\n";
     }
@@ -571,10 +578,6 @@ sub simulate_firewall {
        $target->{type} = 'host';
        $target->{iface} = 'host';
        $pkg->{dest} = $host_ip if !defined($pkg->{dest});
-    } elsif ($to =~ m|^(vmbr\d+)/(\S+)$|) {
-       $target->{type} = 'bport';
-       $target->{bridge} = $1;
-       $target->{iface} = $2;
     } elsif ($to eq 'outside') {
        $target->{type} = 'bport';
        $target->{bridge} = 'vmbr0';
@@ -584,19 +587,19 @@ sub simulate_firewall {
        $target->{bridge} = 'vmbr0';
        $target->{iface} = 'tapXYZ';
     } elsif ($to =~ m/^ct(\d+)$/) {
+       return 'SKIPPED' if !$have_lxc;
        my $vmid = $1;
-       $target = extract_ct_info($vmdata, $vmid);
-       $target->{iface} = 'venet-in';
-
-       if ($target->{ip_address}) {
-           $pkg->{dest} = $target->{ip_address};
-       } else {
-           die "implement me";
-       }
+       $target = extract_ct_info($vmdata, $vmid, 0);
+       $target->{iface} = $target->{tapdev};
    } elsif ($to =~ m/^vm(\d+)$/) {
+       return 'SKIPPED' if !$have_qemu_server;
        my $vmid = $1;
        $target = extract_vm_info($vmdata, $vmid, 0);
        $target->{iface} = $target->{tapdev};
+    } elsif ($to =~ m|^$bridge_interface_pattern$|) {
+       $target->{type} = 'bport';
+       $target->{bridge} = $1;
+       $target->{iface} = $2;
     } else {
        die "unable to parse \"to => '$to'\"\n";
     }
@@ -604,16 +607,16 @@ sub simulate_firewall {
     $pkg->{source} = '100.100.1.2' if !defined($pkg->{source});
     $pkg->{dest} = '100.200.3.4' if !defined($pkg->{dest});
 
-    my ($res, $ic, $rc) = route_packet($ruleset, $ipset_ruleset, $pkg, 
+    my ($res, $ic, $rc) = route_packet($ruleset, $ipset_ruleset, $pkg,
                                       $from_info, $target, $start_state);
 
     add_trace("IPT statistics: invocation = $ic, checks = $rc\n");
+
     return $res if $action eq 'QUERY';
 
     die "test failed ($res != $action)\n" if $action ne $res;
 
-    return undef; 
+    return undef;
 }
 
 1;