]>
git.proxmox.com Git - pve-firewall.git/blob - test/fwtester.pl
12 my $outside_iface = 'eth0';
13 my $outside_bridge = 'vmbr0';
28 my ($chain, $rule, $pkg) = @_;
30 $rule =~ s/^-A $chain // || die "got strange rule: $rule";
32 if ($rule =~ s/^-m conntrack\s*//) {
33 return undef; # simply ignore
36 if ($rule =~ s/^-m addrtype\s*//) {
37 return undef; # simply ignore
40 if ($rule =~ s/^-i (\S+)\s*//) {
41 die "missing iface_in" if !$pkg->{iface_in
};
42 return undef if $pkg->{iface_in
} ne $1; # no match
44 if ($rule =~ s/^-o (\S+)\s*//) {
45 die "missing iface_out" if !$pkg->{iface_out
};
46 return undef if $pkg->{iface_out
} ne $1; # no match
49 if ($rule =~ s/^-p (tcp|udp)\s*//) {
50 die "missing proto" if !$pkg->{proto
};
51 return undef if $pkg->{proto
} ne $1; # no match
54 if ($rule =~ s/^--dport (\d+):(\d+)\s*//) {
55 die "missing dport" if !$pkg->{dport
};
56 return undef if ($pkg->{dport
} < $1) || ($pkg->{dport
} > $2); # no match
59 if ($rule =~ s/^--dport (\d+)\s*//) {
60 die "missing dport" if !$pkg->{dport
};
61 return undef if $pkg->{dport
} != $1; # no match
64 if ($rule =~ s/^-s (\S+)\s*//) {
65 die "missing source" if !$pkg->{source
};
66 return undef if $pkg->{source
} ne $1; # no match
69 if ($rule =~ s/^-d (\S+)\s*//) {
70 die "missing destination" if !$pkg->{dest
};
71 return undef if $pkg->{dest
} ne $1; # no match
74 if ($rule =~ s/^-m mac ! --mac-source (\S+)\s*//) {
75 die "missing source mac" if !$pkg->{mac_source
};
76 return undef if $pkg->{mac_source
} eq $1; # no match
79 if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-in (\S+)\s*//) {
82 return undef if !$pkg->{physdev_in
};
83 return undef if $pkg->{physdev_in
} !~ m/^${devre}$/;
86 if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-out (\S+)\s*//) {
89 return undef if !$pkg->{physdev_out
};
90 return undef if $pkg->{physdev_out
} !~ m/^${devre}$/;
93 if ($rule =~ s/^-j MARK --set-mark (\d+)\s*$//) {
98 if ($rule =~ s/^-j (\S+)\s*$//) {
102 if ($rule =~ s/^-g (\S+)\s*$//) {
106 die "unable to parse rule: $rule";
109 sub ruleset_simulate_chain
{
110 my ($ruleset, $chain, $pkg) = @_;
112 add_trace
("ENTER chain $chain\n");
114 if ($chain eq 'PVEFW-Drop') {
115 add_trace
("LEAVE chain $chain\n");
118 if ($chain eq 'PVEFW-reject') {
119 add_trace
("LEAVE chain $chain\n");
123 if ($chain eq 'PVEFW-tcpflags') {
124 add_trace
("LEAVE chain $chain\n");
128 my $rules = $ruleset->{$chain} ||
129 die "no such chain '$chain'";
131 foreach my $rule (@$rules) {
132 my ($goto, $action) = rule_match
($chain, $rule, $pkg);
133 if (!defined($action)) {
134 add_trace
("SKIP: $rule\n");
137 add_trace
("MATCH: $rule\n");
139 if ($action eq 'ACCEPT' || $action eq 'DROP' || $action eq 'REJECT') {
140 add_trace
("TERMINATE chain $chain: $action\n");
142 } elsif ($action eq 'RETURN') {
143 add_trace
("RETURN FROM chain $chain\n");
147 add_trace
("LEAVE chain $chain - goto $action\n");
148 return ruleset_simulate_chain
($ruleset, $action, $pkg)
150 #$rules = $ruleset->{$chain} || die "no such chain '$chain'";
152 if ($action = ruleset_simulate_chain
($ruleset, $action, $pkg)) {
155 add_trace
("CONTINUE chain $chain\n");
160 add_trace
("LEAVE chain $chain\n");
161 if ($chain =~ m/^PVEFW-(INPUT|OUTPUT|FORWARD)$/) {
162 return 'ACCEPT'; # default policy
173 while (my ($k,$v) = each %$pkg) {
180 # Try to simulate packet traversal inside kernel. This invokes iptable
181 # checks several times.
183 my ($ruleset, $ipset_ruleset, $pkg, $from_info, $target, $start_state) = @_;
185 my $route_state = $start_state;
189 while ($route_state ne $target->{iface
}) {
192 my $next_route_state;
195 $pkg->{iface_in
} = $pkg->{iface_out
} = undef;
196 $pkg->{physdev_in
} = $pkg->{physdev_out
} = undef;
198 if ($route_state eq 'from-outside') {
199 $next_route_state = $outside_bridge || die 'internal error';
200 $next_physdev_in = $outside_iface || die 'internal error';
201 } elsif ($route_state eq 'host') {
203 if ($target->{type
} eq 'outside') {
204 $pkg->{iface_in
} = 'lo';
205 $pkg->{iface_out
} = $outside_bridge;
206 $chain = 'PVEFW-OUTPUT';
207 $next_route_state = $outside_iface
208 } elsif ($target->{type
} eq 'ct') {
209 $pkg->{iface_in
} = 'lo';
210 $pkg->{iface_out
} = 'venet0';
211 $chain = 'PVEFW-OUTPUT';
212 $next_route_state = 'venet-in';
213 } elsif ($target->{type
} eq 'vm') {
214 $pkg->{iface_in
} = 'lo';
215 $pkg->{iface_out
} = $target->{bridge
} || die 'internal error';
216 $chain = 'PVEFW-OUTPUT';
217 $next_route_state = 'fwbr-in';
222 } elsif ($route_state eq 'venet-out') {
224 if ($target->{type
} eq 'host') {
226 $chain = 'PVEFW-INPUT';
227 $pkg->{iface_in
} = 'venet0';
228 $pkg->{iface_out
} = 'lo';
229 $next_route_state = 'host';
231 } elsif ($target->{type
} eq 'outside') {
233 $chain = 'PVEFW-FORWARD';
234 $pkg->{iface_in
} = 'venet0';
235 $pkg->{iface_out
} = $outside_bridge;
236 $next_route_state = $outside_iface;
238 } elsif ($target->{type
} eq 'vm') {
240 $chain = 'PVEFW-FORWARD';
241 $pkg->{iface_in
} = 'venet0';
242 $pkg->{iface_out
} = $target->{bridge
} || die 'internal error';
243 $next_route_state = 'fwbr-in';
245 } elsif ($target->{type
} eq 'ct') {
247 $chain = 'PVEFW-FORWARD';
248 $pkg->{iface_in
} = 'venet0';
249 $pkg->{iface_out
} = 'venet0';
250 $next_route_state = 'venet-in';
256 } elsif ($route_state eq 'fwbr-out') {
258 $chain = 'PVEFW-FORWARD';
259 $next_route_state = $from_info->{bridge
} || die 'internal error';
260 $next_physdev_in = $from_info->{fwpr
} || die 'internal error';
261 $pkg->{iface_in
} = $from_info->{fwbr
} || die 'internal error';
262 $pkg->{iface_out
} = $from_info->{fwbr
} || die 'internal error';
263 $pkg->{physdev_in
} = $from_info->{tapdev
} || die 'internal error';
264 $pkg->{physdev_out
} = $from_info->{fwln
} || die 'internal error';
266 } elsif ($route_state eq 'fwbr-in') {
268 $chain = 'PVEFW-FORWARD';
269 $next_route_state = $target->{tapdev
};
270 $pkg->{iface_in
} = $target->{fwbr
} || die 'internal error';
271 $pkg->{iface_out
} = $target->{fwbr
} || die 'internal error';
272 $pkg->{physdev_in
} = $target->{fwln
} || die 'internal error';
273 $pkg->{physdev_out
} = $target->{tapdev
} || die 'internal error';
275 } elsif ($route_state =~ m/^vmbr\d+$/) {
277 die "missing physdev_in - internal error?" if !$physdev_in;
279 if ($target->{type
} eq 'host') {
281 $chain = 'PVEFW-INPUT';
282 $pkg->{iface_in
} = $route_state;
283 $pkg->{iface_out
} = 'lo';
284 $next_route_state = 'host';
286 if ($route_state eq $outside_bridge) {
292 } elsif ($target->{type
} eq 'outside') {
294 $chain = 'PVEFW-FORWARD';
295 $pkg->{iface_in
} = $route_state;
296 $pkg->{iface_out
} = $outside_bridge;
297 $pkg->{physdev_in
} = $physdev_in;
298 # conditionally set physdev_out (same behavior as kernel)
299 if ($route_state eq $outside_bridge) {
300 $pkg->{physdev_out
} = $outside_iface || die 'internal error';
302 $next_route_state = $outside_iface;
304 } elsif ($target->{type
} eq 'ct') {
306 $chain = 'PVEFW-FORWARD';
307 $pkg->{iface_in
} = $route_state;
308 $pkg->{iface_out
} = 'venet0';
309 $next_route_state = 'venet-in';
311 } elsif ($target->{type
} eq 'vm') {
313 $chain = 'PVEFW-FORWARD';
314 $pkg->{iface_in
} = $route_state;
315 $pkg->{iface_out
} = $target->{bridge
};
316 $pkg->{physdev_in
} = $physdev_in;
317 # conditionally set physdev_out (same behavior as kernel)
318 if ($route_state eq $target->{bridge
}) {
319 $pkg->{physdev_out
} = $target->{fwpr
} || die 'internal error';
321 $next_route_state = 'fwbr-in';
328 die "implement me $route_state";
331 die "internal error" if !defined($next_route_state);
334 add_trace
("IPT check at $route_state (chain $chain)\n");
335 add_trace
(Dumper
($pkg));
336 my $res = ruleset_simulate_chain
($ruleset, $chain, $pkg);
337 return $res if $res ne 'ACCEPT';
340 $route_state = $next_route_state;
342 $physdev_in = $next_physdev_in;
348 sub extract_ct_info
{
349 my ($vmdata, $vmid) = @_;
351 my $info = { type
=> 'ct', vmid
=> $vmid };
353 my $conf = $vmdata->{openvz
}->{$vmid} || die "no such CT '$vmid'";
354 if ($conf->{ip_address
}) {
355 $info->{ip_address
} = $conf->{ip_address
}->{value
};
362 sub extract_vm_info
{
363 my ($vmdata, $vmid) = @_;
365 my $info = { type
=> 'vm', vmid
=> $vmid };
367 my $conf = $vmdata->{qemu
}->{$vmid} || die "no such VM '$vmid'";
368 my $net = PVE
::QemuServer
::parse_net
($conf->{net0
});
369 $info->{macaddr
} = $net->{macaddr
} || die "unable to get mac address";
370 $info->{bridge
} = $net->{bridge
} || die "unable to get bridge";
371 $info->{fwbr
} = "fwbr${vmid}i0";
372 $info->{tapdev
} = "tap${vmid}i0";
373 $info->{fwln
} = "fwln${vmid}i0";
374 $info->{fwpr
} = "fwpr${vmid}p0";
379 sub simulate_firewall
{
380 my ($ruleset, $ipset_ruleset, $vmdata, $test) = @_;
382 my $from = delete $test->{from
} || die "missing 'from' field";
383 my $to = delete $test->{to
} || die "missing 'to' field";
384 my $action = delete $test->{action
} || die "missing 'action'";
386 die "from/to needs to be different" if $from eq $to;
392 source
=> '10.11.12.13',
393 dest
=> '10.11.12.14',
396 while (my ($k,$v) = each %$test) {
404 if ($from eq 'host') {
405 $from_info->{type
} = 'host';
406 $start_state = 'host';
407 } elsif ($from eq 'outside') {
408 $from_info->{type
} = 'outside';
409 $start_state = 'from-outside';
410 } elsif ($from =~ m/^ct(\d+)$/) {
412 $from_info = extract_ct_info
($vmdata, $vmid);
413 if ($from_info->{ip_address
}) {
414 $pkg->{source
} = $from_info->{ip_address
};
415 $start_state = 'venet-out';
419 } elsif ($from =~ m/^vm(\d+)$/) {
421 $from_info = extract_vm_info
($vmdata, $vmid);
422 $start_state = 'fwbr-out';
423 $pkg->{mac_source
} = $from_info->{macaddr
};
431 $target->{type
} = 'host';
432 $target->{iface
} = 'host';
433 } elsif ($to eq 'outside') {
434 $target->{type
} = 'outside';
435 $target->{iface
} = $outside_iface;
436 } elsif ($to =~ m/^ct(\d+)$/) {
438 $target = extract_ct_info
($vmdata, $vmid);
439 $target->{iface
} = 'venet-in';
441 if ($target->{ip_address
}) {
442 $pkg->{dest
} = $target->{ip_address
};
446 } elsif ($to =~ m/^vm(\d+)$/) {
448 $target = extract_vm_info
($vmdata, $vmid);
449 $target->{iface
} = $target->{tapdev
};
454 my $res = route_packet
($ruleset, $ipset_ruleset, $pkg, $from_info, $target, $start_state);
456 die "test failed ($res != $action)\n" if $action ne $res;
462 my ($vmdata, $testdir) = @_;
464 $vmdata->{testdir
} = $testdir;
466 my ($ruleset, $ipset_ruleset) =
467 PVE
::Firewall
::compile
(undef, undef, $vmdata);
469 my $testfile = "$testdir/tests";
470 my $fh = IO
::File-
>new($testfile) ||
471 die "unable to open '$testfile' - $!\n";
473 while (defined(my $line = <$fh>)) {
474 next if $line =~ m/^\s*$/;
475 next if $line =~ m/^#.*$/;
476 if ($line =~ m/^\{.*\}\s*$/) {
477 my $test = eval $line;
480 print Dumper
($ruleset) if $debug;
481 eval { simulate_firewall
($ruleset, $ipset_ruleset, $vmdata, $test); };
484 print Dumper
($ruleset) if !$debug;
486 print "$trace\n" if !$debug;
488 print "$testfile line $.: $line";
490 print "test failed: $err\n";
499 print "PASS: $testfile\n";
507 net0
=> "e1000=0E:0B:38:B8:B3:21,bridge=vmbr0",
510 net0
=> "e1000=0E:0B:38:B8:B3:22,bridge=vmbr0",
514 net0
=> "e1000=0E:0B:38:B8:B4:21,bridge=vmbr1",
519 ip_address
=> { value
=> '10.0.200.1' },
522 ip_address
=> { value
=> '10.0.200.2' },
527 foreach my $dir (<test-
*>) {
529 run_tests
($vmdata, $dir);
532 print "OK - all tests passed\n";