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