]> git.proxmox.com Git - pve-firewall.git/blame - test/fwtester.pl
fwtester: support dev regex with -i and -o
[pve-firewall.git] / test / fwtester.pl
CommitLineData
f1bafd37
DM
1#!/usr/bin/perl
2
3use lib '../src';
4use strict;
5use warnings;
6use Data::Dumper;
7use PVE::Firewall;
ec2e28f6
DM
8use Getopt::Long;
9use File::Basename;
f1bafd37
DM
10
11my $mark;
12my $trace;
13
31dc73f1
DM
14my $outside_iface = 'eth0';
15my $outside_bridge = 'vmbr0';
16
d1486f38
DM
17my $debug = 0;
18
ec2e28f6
DM
19sub print_usage_and_exit {
20 die "usage: $0 [--debug] [testfile [testid]]\n";
21}
22
23if (!GetOptions ('debug' => \$debug)) {
24 print_usage_and_exit();
25}
26
27my $testfilename = shift;
28my $testid = shift;
29
d1486f38
DM
30sub add_trace {
31 my ($text) = @_;
32
33 if ($debug) {
34 print $text;
35 } else {
36 $trace .= $text;
37 }
38}
39
e4e5fcaf
DM
40sub nf_dev_match {
41 my ($devre, $dev) = @_;
42
43 $devre =~ s/\+$/\.\*/;
44 return ($dev =~ m/^${devre}$/) ? 1 : 0;
45}
46
f1bafd37
DM
47sub rule_match {
48 my ($chain, $rule, $pkg) = @_;
49
50 $rule =~ s/^-A $chain // || die "got strange rule: $rule";
51
52 if ($rule =~ s/^-m conntrack\s*//) {
53 return undef; # simply ignore
54 }
55
56 if ($rule =~ s/^-m addrtype\s*//) {
57 return undef; # simply ignore
58 }
59
60 if ($rule =~ s/^-i (\S+)\s*//) {
e4e5fcaf 61 my $devre = $1;
f1bafd37 62 die "missing iface_in" if !$pkg->{iface_in};
e4e5fcaf 63 return undef if !nf_dev_match($devre, $pkg->{iface_in});
f1bafd37
DM
64 }
65 if ($rule =~ s/^-o (\S+)\s*//) {
e4e5fcaf 66 my $devre = $1;
f1bafd37 67 die "missing iface_out" if !$pkg->{iface_out};
e4e5fcaf 68 return undef if !nf_dev_match($devre, $pkg->{iface_out});
f1bafd37
DM
69 }
70
71 if ($rule =~ s/^-p (tcp|udp)\s*//) {
72 die "missing proto" if !$pkg->{proto};
73 return undef if $pkg->{proto} ne $1; # no match
74 }
75
76 if ($rule =~ s/^--dport (\d+):(\d+)\s*//) {
77 die "missing dport" if !$pkg->{dport};
78 return undef if ($pkg->{dport} < $1) || ($pkg->{dport} > $2); # no match
79 }
80
81 if ($rule =~ s/^--dport (\d+)\s*//) {
82 die "missing dport" if !$pkg->{dport};
83 return undef if $pkg->{dport} != $1; # no match
84 }
85
86 if ($rule =~ s/^-s (\S+)\s*//) {
87 die "missing source" if !$pkg->{source};
88 return undef if $pkg->{source} ne $1; # no match
89 }
90
91 if ($rule =~ s/^-d (\S+)\s*//) {
92 die "missing destination" if !$pkg->{dest};
93 return undef if $pkg->{dest} ne $1; # no match
94 }
95
96 if ($rule =~ s/^-m mac ! --mac-source (\S+)\s*//) {
97 die "missing source mac" if !$pkg->{mac_source};
98 return undef if $pkg->{mac_source} eq $1; # no match
99 }
100
101 if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-in (\S+)\s*//) {
102 my $devre = $1;
f1bafd37 103 return undef if !$pkg->{physdev_in};
e4e5fcaf 104 return undef if !nf_dev_match($devre, $pkg->{physdev_in});
f1bafd37
DM
105 }
106
107 if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-out (\S+)\s*//) {
108 my $devre = $1;
f1bafd37 109 return undef if !$pkg->{physdev_out};
e4e5fcaf 110 return undef if !nf_dev_match($devre, $pkg->{physdev_out});
f1bafd37
DM
111 }
112
113 if ($rule =~ s/^-j MARK --set-mark (\d+)\s*$//) {
114 $mark = $1;
115 return undef;
116 }
117
118 if ($rule =~ s/^-j (\S+)\s*$//) {
119 return (0, $1);
120 }
121
122 if ($rule =~ s/^-g (\S+)\s*$//) {
123 return (1, $1);
124 }
125
126 die "unable to parse rule: $rule";
127}
128
129sub ruleset_simulate_chain {
130 my ($ruleset, $chain, $pkg) = @_;
131
d1486f38 132 add_trace("ENTER chain $chain\n");
f1bafd37 133
111cce51
DM
134 my $counter = 2; # ENTER + LEAVE = 2
135
f1bafd37 136 if ($chain eq 'PVEFW-Drop') {
d1486f38 137 add_trace("LEAVE chain $chain\n");
111cce51 138 return ('DROP', $counter);
f1bafd37
DM
139 }
140 if ($chain eq 'PVEFW-reject') {
d1486f38 141 add_trace("LEAVE chain $chain\n");
111cce51 142 return ('REJECT', $counter);
f1bafd37
DM
143 }
144
145 if ($chain eq 'PVEFW-tcpflags') {
d1486f38 146 add_trace("LEAVE chain $chain\n");
111cce51 147 return (undef, $counter);
f1bafd37
DM
148 }
149
150 my $rules = $ruleset->{$chain} ||
151 die "no such chain '$chain'";
152
153 foreach my $rule (@$rules) {
111cce51 154 $counter++;
f1bafd37
DM
155 my ($goto, $action) = rule_match($chain, $rule, $pkg);
156 if (!defined($action)) {
d1486f38 157 add_trace("SKIP: $rule\n");
f1bafd37
DM
158 next;
159 }
d1486f38 160 add_trace("MATCH: $rule\n");
f1bafd37
DM
161
162 if ($action eq 'ACCEPT' || $action eq 'DROP' || $action eq 'REJECT') {
d1486f38 163 add_trace("TERMINATE chain $chain: $action\n");
111cce51 164 return ($action, $counter);
f1bafd37 165 } elsif ($action eq 'RETURN') {
d1486f38 166 add_trace("RETURN FROM chain $chain\n");
f1bafd37
DM
167 last;
168 } else {
169 if ($goto) {
d1486f38 170 add_trace("LEAVE chain $chain - goto $action\n");
f1bafd37
DM
171 return ruleset_simulate_chain($ruleset, $action, $pkg)
172 #$chain = $action;
173 #$rules = $ruleset->{$chain} || die "no such chain '$chain'";
174 } else {
111cce51
DM
175 my ($act, $ctr) = ruleset_simulate_chain($ruleset, $action, $pkg);
176 $counter += $ctr;
177 return ($act, $counter) if $act;
d1486f38 178 add_trace("CONTINUE chain $chain\n");
f1bafd37
DM
179 }
180 }
181 }
182
d1486f38 183 add_trace("LEAVE chain $chain\n");
f1bafd37 184 if ($chain =~ m/^PVEFW-(INPUT|OUTPUT|FORWARD)$/) {
111cce51 185 return ('ACCEPT', $counter); # default policy
f1bafd37
DM
186 }
187
111cce51 188 return (undef, $counter);
f1bafd37
DM
189}
190
191sub copy_packet {
192 my ($pkg) = @_;
193
194 my $res = {};
195
196 while (my ($k,$v) = each %$pkg) {
197 $res->{$k} = $v;
198 }
199
200 return $res;
201}
202
d1486f38
DM
203# Try to simulate packet traversal inside kernel. This invokes iptable
204# checks several times.
205sub route_packet {
206 my ($ruleset, $ipset_ruleset, $pkg, $from_info, $target, $start_state) = @_;
207
208 my $route_state = $start_state;
209
210 my $physdev_in;
211
111cce51
DM
212 my $ipt_invocation_counter = 0;
213 my $rule_check_counter = 0;
214
d1486f38
DM
215 while ($route_state ne $target->{iface}) {
216
217 my $chain;
218 my $next_route_state;
219 my $next_physdev_in;
220
221 $pkg->{iface_in} = $pkg->{iface_out} = undef;
222 $pkg->{physdev_in} = $pkg->{physdev_out} = undef;
223
31dc73f1
DM
224 if ($route_state eq 'from-outside') {
225 $next_route_state = $outside_bridge || die 'internal error';
226 $next_physdev_in = $outside_iface || die 'internal error';
227 } elsif ($route_state eq 'host') {
d1486f38 228
31dc73f1
DM
229 if ($target->{type} eq 'outside') {
230 $pkg->{iface_in} = 'lo';
231 $pkg->{iface_out} = $outside_bridge;
232 $chain = 'PVEFW-OUTPUT';
233 $next_route_state = $outside_iface
234 } elsif ($target->{type} eq 'ct') {
d1486f38
DM
235 $pkg->{iface_in} = 'lo';
236 $pkg->{iface_out} = 'venet0';
237 $chain = 'PVEFW-OUTPUT';
238 $next_route_state = 'venet-in';
239 } elsif ($target->{type} eq 'vm') {
240 $pkg->{iface_in} = 'lo';
241 $pkg->{iface_out} = $target->{bridge} || die 'internal error';
242 $chain = 'PVEFW-OUTPUT';
243 $next_route_state = 'fwbr-in';
244 } else {
245 die "implement me";
246 }
247
248 } elsif ($route_state eq 'venet-out') {
249
250 if ($target->{type} eq 'host') {
251
252 $chain = 'PVEFW-INPUT';
253 $pkg->{iface_in} = 'venet0';
254 $pkg->{iface_out} = 'lo';
255 $next_route_state = 'host';
256
31dc73f1
DM
257 } elsif ($target->{type} eq 'outside') {
258
259 $chain = 'PVEFW-FORWARD';
260 $pkg->{iface_in} = 'venet0';
261 $pkg->{iface_out} = $outside_bridge;
262 $next_route_state = $outside_iface;
263
d1486f38
DM
264 } elsif ($target->{type} eq 'vm') {
265
266 $chain = 'PVEFW-FORWARD';
267 $pkg->{iface_in} = 'venet0';
268 $pkg->{iface_out} = $target->{bridge} || die 'internal error';
269 $next_route_state = 'fwbr-in';
270
271 } elsif ($target->{type} eq 'ct') {
272
273 $chain = 'PVEFW-FORWARD';
274 $pkg->{iface_in} = 'venet0';
275 $pkg->{iface_out} = 'venet0';
276 $next_route_state = 'venet-in';
277
278 } else {
279 die "implement me";
280 }
281
282 } elsif ($route_state eq 'fwbr-out') {
283
284 $chain = 'PVEFW-FORWARD';
285 $next_route_state = $from_info->{bridge} || die 'internal error';
286 $next_physdev_in = $from_info->{fwpr} || die 'internal error';
287 $pkg->{iface_in} = $from_info->{fwbr} || die 'internal error';
288 $pkg->{iface_out} = $from_info->{fwbr} || die 'internal error';
289 $pkg->{physdev_in} = $from_info->{tapdev} || die 'internal error';
290 $pkg->{physdev_out} = $from_info->{fwln} || die 'internal error';
291
292 } elsif ($route_state eq 'fwbr-in') {
293
294 $chain = 'PVEFW-FORWARD';
295 $next_route_state = $target->{tapdev};
296 $pkg->{iface_in} = $target->{fwbr} || die 'internal error';
297 $pkg->{iface_out} = $target->{fwbr} || die 'internal error';
298 $pkg->{physdev_in} = $target->{fwln} || die 'internal error';
299 $pkg->{physdev_out} = $target->{tapdev} || die 'internal error';
300
301 } elsif ($route_state =~ m/^vmbr\d+$/) {
302
303 die "missing physdev_in - internal error?" if !$physdev_in;
eb5faed9 304 $pkg->{physdev_in} = $physdev_in;
d1486f38
DM
305
306 if ($target->{type} eq 'host') {
307
308 $chain = 'PVEFW-INPUT';
309 $pkg->{iface_in} = $route_state;
310 $pkg->{iface_out} = 'lo';
311 $next_route_state = 'host';
312
31dc73f1
DM
313 } elsif ($target->{type} eq 'outside') {
314
315 $chain = 'PVEFW-FORWARD';
316 $pkg->{iface_in} = $route_state;
317 $pkg->{iface_out} = $outside_bridge;
31dc73f1
DM
318 # conditionally set physdev_out (same behavior as kernel)
319 if ($route_state eq $outside_bridge) {
320 $pkg->{physdev_out} = $outside_iface || die 'internal error';
321 }
322 $next_route_state = $outside_iface;
323
d1486f38
DM
324 } elsif ($target->{type} eq 'ct') {
325
326 $chain = 'PVEFW-FORWARD';
327 $pkg->{iface_in} = $route_state;
328 $pkg->{iface_out} = 'venet0';
329 $next_route_state = 'venet-in';
330
331 } elsif ($target->{type} eq 'vm') {
332
333 $chain = 'PVEFW-FORWARD';
31dc73f1
DM
334 $pkg->{iface_in} = $route_state;
335 $pkg->{iface_out} = $target->{bridge};
31dc73f1 336 # conditionally set physdev_out (same behavior as kernel)
d1486f38 337 if ($route_state eq $target->{bridge}) {
d1486f38 338 $pkg->{physdev_out} = $target->{fwpr} || die 'internal error';
d1486f38
DM
339 }
340 $next_route_state = 'fwbr-in';
341
342 } else {
343 die "implement me";
344 }
345
346 } else {
347 die "implement me $route_state";
348 }
349
350 die "internal error" if !defined($next_route_state);
351
352 if ($chain) {
353 add_trace("IPT check at $route_state (chain $chain)\n");
354 add_trace(Dumper($pkg));
111cce51
DM
355 $ipt_invocation_counter++;
356 my ($res, $ctr) = ruleset_simulate_chain($ruleset, $chain, $pkg);
357 $rule_check_counter += $ctr;
358 return ($res, $ipt_invocation_counter, $rule_check_counter) if $res ne 'ACCEPT';
d1486f38
DM
359 }
360
361 $route_state = $next_route_state;
362
363 $physdev_in = $next_physdev_in;
364 }
365
111cce51 366 return ('ACCEPT', $ipt_invocation_counter, $rule_check_counter);
d1486f38
DM
367}
368
369sub extract_ct_info {
370 my ($vmdata, $vmid) = @_;
371
372 my $info = { type => 'ct', vmid => $vmid };
373
374 my $conf = $vmdata->{openvz}->{$vmid} || die "no such CT '$vmid'";
375 if ($conf->{ip_address}) {
376 $info->{ip_address} = $conf->{ip_address}->{value};
377 } else {
378 die "implement me";
379 }
380 return $info;
381}
382
383sub extract_vm_info {
384 my ($vmdata, $vmid) = @_;
385
386 my $info = { type => 'vm', vmid => $vmid };
387
388 my $conf = $vmdata->{qemu}->{$vmid} || die "no such VM '$vmid'";
389 my $net = PVE::QemuServer::parse_net($conf->{net0});
390 $info->{macaddr} = $net->{macaddr} || die "unable to get mac address";
391 $info->{bridge} = $net->{bridge} || die "unable to get bridge";
392 $info->{fwbr} = "fwbr${vmid}i0";
393 $info->{tapdev} = "tap${vmid}i0";
394 $info->{fwln} = "fwln${vmid}i0";
395 $info->{fwpr} = "fwpr${vmid}p0";
396
397 return $info;
398}
f1bafd37
DM
399
400sub simulate_firewall {
401 my ($ruleset, $ipset_ruleset, $vmdata, $test) = @_;
402
403 my $from = delete $test->{from} || die "missing 'from' field";
404 my $to = delete $test->{to} || die "missing 'to' field";
405 my $action = delete $test->{action} || die "missing 'action'";
8215a0da
DM
406
407 my $testid = delete $test->{id};
408
f1bafd37
DM
409 die "from/to needs to be different" if $from eq $to;
410
411 my $pkg = {
f1bafd37 412 proto => 'tcp',
8215a0da
DM
413 sport => undef,
414 dport => undef,
415 source => undef,
416 dest => undef,
f1bafd37
DM
417 };
418
419 while (my ($k,$v) = each %$test) {
8215a0da 420 die "unknown attribute '$k'\n" if !exists($pkg->{$k});
f1bafd37
DM
421 $pkg->{$k} = $v;
422 }
423
d1486f38
DM
424 my $from_info = {};
425
426 my $start_state;
f1bafd37
DM
427
428 if ($from eq 'host') {
d1486f38
DM
429 $from_info->{type} = 'host';
430 $start_state = 'host';
31dc73f1
DM
431 } elsif ($from eq 'outside') {
432 $from_info->{type} = 'outside';
433 $start_state = 'from-outside';
f1bafd37
DM
434 } elsif ($from =~ m/^ct(\d+)$/) {
435 my $vmid = $1;
d1486f38
DM
436 $from_info = extract_ct_info($vmdata, $vmid);
437 if ($from_info->{ip_address}) {
438 $pkg->{source} = $from_info->{ip_address};
439 $start_state = 'venet-out';
f1bafd37
DM
440 } else {
441 die "implement me";
442 }
f1bafd37
DM
443 } elsif ($from =~ m/^vm(\d+)$/) {
444 my $vmid = $1;
d1486f38
DM
445 $from_info = extract_vm_info($vmdata, $vmid);
446 $start_state = 'fwbr-out';
447 $pkg->{mac_source} = $from_info->{macaddr};
f1bafd37
DM
448 } else {
449 die "implement me";
450 }
451
d1486f38
DM
452 my $target;
453
f1bafd37 454 if ($to eq 'host') {
d1486f38
DM
455 $target->{type} = 'host';
456 $target->{iface} = 'host';
31dc73f1
DM
457 } elsif ($to eq 'outside') {
458 $target->{type} = 'outside';
459 $target->{iface} = $outside_iface;
f1bafd37
DM
460 } elsif ($to =~ m/^ct(\d+)$/) {
461 my $vmid = $1;
d1486f38
DM
462 $target = extract_ct_info($vmdata, $vmid);
463 $target->{iface} = 'venet-in';
464
465 if ($target->{ip_address}) {
466 $pkg->{dest} = $target->{ip_address};
f1bafd37
DM
467 } else {
468 die "implement me";
469 }
f1bafd37
DM
470 } elsif ($to =~ m/^vm(\d+)$/) {
471 my $vmid = $1;
d1486f38
DM
472 $target = extract_vm_info($vmdata, $vmid);
473 $target->{iface} = $target->{tapdev};
f1bafd37
DM
474 } else {
475 die "implement me";
476 }
477
111cce51
DM
478 my ($res, $ic, $rc) = route_packet($ruleset, $ipset_ruleset, $pkg,
479 $from_info, $target, $start_state);
f1bafd37 480
111cce51
DM
481 add_trace("IPT statistics: invocation = $ic, checks = $rc\n");
482
d1486f38 483 die "test failed ($res != $action)\n" if $action ne $res;
f1bafd37 484
d1486f38 485 return undef;
f1bafd37
DM
486}
487
488sub run_tests {
ec2e28f6
DM
489 my ($vmdata, $testdir, $testfile, $testid) = @_;
490
491 $testfile = 'tests' if !$testfile;
f1bafd37
DM
492
493 $vmdata->{testdir} = $testdir;
494
495 my ($ruleset, $ipset_ruleset) =
496 PVE::Firewall::compile(undef, undef, $vmdata);
497
ec2e28f6
DM
498 my $filename = "$testdir/$testfile";
499 my $fh = IO::File->new($filename) ||
500 die "unable to open '$filename' - $!\n";
f1bafd37 501
ec2e28f6 502 my $testcount = 0;
f1bafd37
DM
503 while (defined(my $line = <$fh>)) {
504 next if $line =~ m/^\s*$/;
505 next if $line =~ m/^#.*$/;
506 if ($line =~ m/^\{.*\}\s*$/) {
507 my $test = eval $line;
508 die $@ if $@;
ec2e28f6 509 next if defined($testid) && (!defined($test->{id}) || ($testid ne $test->{id}));
f1bafd37 510 $trace = '';
d1486f38 511 print Dumper($ruleset) if $debug;
ec2e28f6 512 $testcount++;
f1bafd37
DM
513 eval { simulate_firewall($ruleset, $ipset_ruleset, $vmdata, $test); };
514 if (my $err = $@) {
515
d1486f38 516 print Dumper($ruleset) if !$debug;
f1bafd37 517
d1486f38 518 print "$trace\n" if !$debug;
f1bafd37 519
ec2e28f6 520 print "$filename line $.: $line";
f1bafd37
DM
521
522 print "test failed: $err\n";
523
524 exit(-1);
525 }
526 } else {
527 die "parse error";
528 }
529 }
530
ec2e28f6
DM
531 die "no tests found\n" if $testcount <= 0;
532
533 print "PASS: $filename\n";
f1bafd37
DM
534
535 return undef;
536}
537
538my $vmdata = {
539 qemu => {
540 100 => {
d1486f38
DM
541 net0 => "e1000=0E:0B:38:B8:B3:21,bridge=vmbr0",
542 },
543 101 => {
544 net0 => "e1000=0E:0B:38:B8:B3:22,bridge=vmbr0",
545 },
546 # on bridge vmbr1
547 110 => {
548 net0 => "e1000=0E:0B:38:B8:B4:21,bridge=vmbr1",
f1bafd37
DM
549 },
550 },
551 openvz => {
552 200 => {
553 ip_address => { value => '10.0.200.1' },
554 },
d1486f38
DM
555 201 => {
556 ip_address => { value => '10.0.200.2' },
557 },
f1bafd37
DM
558 },
559};
560
ec2e28f6
DM
561if ($testfilename) {
562 my $testfile;
563 my $dir;
564
565 if (-d $testfilename) {
566 $dir = $testfilename;
567 } elsif (-f $testfilename) {
568 $dir = dirname($testfilename);
569 $testfile = basename($testfilename);
570 } else {
571 die "no such file/dir '$testfilename'\n";
572 }
573
574 run_tests($vmdata, $dir, $testfile, $testid);
575
576} else {
577 foreach my $dir (<test-*>) {
578 next if ! -d $dir;
579 run_tests($vmdata, $dir);
580 }
f1bafd37
DM
581}
582
583print "OK - all tests passed\n";
584
585exit(0);