fwtester: implement new 'outside' zone
[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
9 my $mark;
10 my $trace;
11
12 my $outside_iface = 'eth0';
13 my $outside_bridge = 'vmbr0';
14
15 my $debug = 0;
16
17 sub add_trace {
18     my ($text) = @_;
19
20     if ($debug) {
21         print $text;
22     } else {
23         $trace .= $text;
24     }
25 }
26
27 sub rule_match {
28     my ($chain, $rule, $pkg) = @_;
29
30     $rule =~ s/^-A $chain // || die "got strange rule: $rule";
31
32     if ($rule =~ s/^-m conntrack\s*//) {
33         return undef; # simply ignore
34     }
35
36     if ($rule =~ s/^-m addrtype\s*//) {
37         return undef; # simply ignore
38     }
39
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
43     }
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
47     }
48
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
52     }
53
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
57     }
58
59     if ($rule =~ s/^--dport (\d+)\s*//) {
60         die "missing dport" if !$pkg->{dport};
61         return undef if $pkg->{dport} != $1; # no match
62     }
63
64     if ($rule =~ s/^-s (\S+)\s*//) {
65         die "missing source" if !$pkg->{source};
66         return undef if $pkg->{source} ne $1; # no match
67     }
68     
69     if ($rule =~ s/^-d (\S+)\s*//) {
70         die "missing destination" if !$pkg->{dest};
71         return undef if $pkg->{dest} ne $1; # no match
72     }
73
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
77     }
78
79     if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-in (\S+)\s*//) {
80         my $devre = $1;
81         $devre =~ s/\+/\.\*/;
82         return undef if !$pkg->{physdev_in};
83         return undef if $pkg->{physdev_in} !~ m/^${devre}$/;
84     }
85
86     if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-out (\S+)\s*//) {
87         my $devre = $1;
88         $devre =~ s/\+/\.\*/;
89         return undef if !$pkg->{physdev_out};
90         return undef if $pkg->{physdev_out} !~ m/^${devre}$/;
91     }
92
93     if ($rule =~ s/^-j MARK --set-mark (\d+)\s*$//) {
94         $mark = $1;
95         return undef;
96     }
97
98     if ($rule =~ s/^-j (\S+)\s*$//) {
99         return (0, $1);
100     }
101
102     if ($rule =~ s/^-g (\S+)\s*$//) {
103         return (1, $1);
104     }
105
106     die "unable to parse rule: $rule";
107 }
108
109 sub ruleset_simulate_chain {
110     my ($ruleset, $chain, $pkg) = @_;
111
112     add_trace("ENTER chain $chain\n");
113     
114     if ($chain eq 'PVEFW-Drop') {
115         add_trace("LEAVE chain $chain\n");
116         return 'DROP';
117     }
118     if ($chain eq 'PVEFW-reject') {
119         add_trace("LEAVE chain $chain\n");
120         return 'REJECT';
121     }
122
123     if ($chain eq 'PVEFW-tcpflags') {
124         add_trace("LEAVE chain $chain\n");
125         return undef;
126     }
127
128     my $rules = $ruleset->{$chain} ||
129         die "no such chain '$chain'";
130
131     foreach my $rule (@$rules) {
132         my ($goto, $action) = rule_match($chain, $rule, $pkg);
133         if (!defined($action)) {
134             add_trace("SKIP: $rule\n");
135             next;
136         }
137         add_trace("MATCH: $rule\n");
138         
139         if ($action eq 'ACCEPT' || $action eq 'DROP' || $action eq 'REJECT') {
140             add_trace("TERMINATE chain $chain: $action\n");
141             return $action;
142         } elsif ($action eq 'RETURN') {
143             add_trace("RETURN FROM chain $chain\n");
144             last;
145         } else {
146             if ($goto) {
147                 add_trace("LEAVE chain $chain - goto $action\n");
148                 return ruleset_simulate_chain($ruleset, $action, $pkg)
149                 #$chain = $action;
150                 #$rules = $ruleset->{$chain} || die "no such chain '$chain'";
151             } else {
152                 if ($action = ruleset_simulate_chain($ruleset, $action, $pkg)) {
153                     return $action;
154                 }
155                 add_trace("CONTINUE chain $chain\n");
156             }
157         }
158     }
159
160     add_trace("LEAVE chain $chain\n");
161     if ($chain =~ m/^PVEFW-(INPUT|OUTPUT|FORWARD)$/) {
162         return 'ACCEPT'; # default policy
163     }
164
165     return undef;
166 }
167
168 sub copy_packet {
169     my ($pkg) = @_;
170
171     my $res = {};
172
173     while (my ($k,$v) = each %$pkg) {
174         $res->{$k} = $v;
175     }
176
177     return $res;
178 }
179
180 # Try to simulate packet traversal inside kernel. This invokes iptable
181 # checks several times.
182 sub route_packet {
183     my ($ruleset, $ipset_ruleset, $pkg, $from_info, $target, $start_state) = @_;
184
185     my $route_state = $start_state;
186
187     my $physdev_in;
188
189     while ($route_state ne $target->{iface}) {
190
191         my $chain;
192         my $next_route_state;
193         my $next_physdev_in;
194
195         $pkg->{iface_in} = $pkg->{iface_out} = undef;
196         $pkg->{physdev_in} = $pkg->{physdev_out} = undef;
197
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') {
202
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';
218             } else {
219                 die "implement me";
220             }
221
222         } elsif ($route_state eq 'venet-out') {
223
224             if ($target->{type} eq 'host') {
225
226                 $chain = 'PVEFW-INPUT';
227                 $pkg->{iface_in} = 'venet0';
228                 $pkg->{iface_out} = 'lo';
229                 $next_route_state = 'host';
230
231             } elsif ($target->{type} eq 'outside') {
232                 
233                 $chain = 'PVEFW-FORWARD';
234                 $pkg->{iface_in} = 'venet0';
235                 $pkg->{iface_out} = $outside_bridge;
236                 $next_route_state = $outside_iface;
237
238             } elsif ($target->{type} eq 'vm') {
239
240                 $chain = 'PVEFW-FORWARD';
241                 $pkg->{iface_in} = 'venet0';
242                 $pkg->{iface_out} = $target->{bridge} || die 'internal error';
243                 $next_route_state = 'fwbr-in';
244
245             } elsif ($target->{type} eq 'ct') {
246
247                 $chain = 'PVEFW-FORWARD';
248                 $pkg->{iface_in} = 'venet0';
249                 $pkg->{iface_out} = 'venet0';
250                 $next_route_state = 'venet-in';
251
252             } else {
253                 die "implement me";
254             }
255
256         } elsif ($route_state eq 'fwbr-out') {
257
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';
265         
266         } elsif ($route_state eq 'fwbr-in') {
267
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';
274
275         } elsif ($route_state =~ m/^vmbr\d+$/) {
276             
277             die "missing physdev_in - internal error?" if !$physdev_in;
278
279             if ($target->{type} eq 'host') {
280
281                 $chain = 'PVEFW-INPUT';
282                 $pkg->{iface_in} = $route_state;
283                 $pkg->{iface_out} = 'lo';
284                 $next_route_state = 'host';
285
286                 if ($route_state eq $outside_bridge) {
287
288                 } else {
289
290                 }
291
292             } elsif ($target->{type} eq 'outside') {
293
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';
301                 }
302                 $next_route_state = $outside_iface;
303
304             } elsif ($target->{type} eq 'ct') {
305
306                 $chain = 'PVEFW-FORWARD';
307                 $pkg->{iface_in} = $route_state;
308                 $pkg->{iface_out} = 'venet0';
309                 $next_route_state = 'venet-in';
310
311             } elsif ($target->{type} eq 'vm') {
312
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';
320                 }
321                 $next_route_state = 'fwbr-in';
322
323             } else {
324                 die "implement me";
325             }
326
327         } else {
328             die "implement me $route_state";
329         }
330
331         die "internal error" if !defined($next_route_state);
332
333         if ($chain) {
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';
338         } 
339
340         $route_state = $next_route_state;
341
342         $physdev_in = $next_physdev_in;
343     }
344
345     return 'ACCEPT';
346 }
347
348 sub extract_ct_info {
349     my ($vmdata, $vmid) = @_;
350
351     my $info = { type => 'ct', vmid => $vmid };
352
353     my $conf = $vmdata->{openvz}->{$vmid} || die "no such CT '$vmid'";
354     if ($conf->{ip_address}) {
355         $info->{ip_address} = $conf->{ip_address}->{value};
356     } else {
357         die "implement me";
358     }
359     return $info;
360 }
361
362 sub extract_vm_info {
363     my ($vmdata, $vmid) = @_;
364
365     my $info = { type => 'vm', vmid => $vmid };
366
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";
375
376     return $info;
377 }
378
379 sub simulate_firewall {
380     my ($ruleset, $ipset_ruleset, $vmdata, $test) = @_;
381
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'";
385
386     die "from/to needs to be different" if $from eq $to;
387
388     my $pkg = {
389         proto => 'tcp',
390         sport => '1234',
391         dport => '4321',
392         source => '10.11.12.13',
393         dest => '10.11.12.14',
394     };
395
396     while (my ($k,$v) = each %$test) {
397         $pkg->{$k} = $v;
398     }
399
400     my $from_info = {};
401
402     my $start_state;
403
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+)$/) {
411         my $vmid = $1;
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';
416         } else {
417             die "implement me";
418         }
419     } elsif ($from =~ m/^vm(\d+)$/) {
420         my $vmid = $1;
421         $from_info = extract_vm_info($vmdata, $vmid);
422         $start_state = 'fwbr-out'; 
423         $pkg->{mac_source} = $from_info->{macaddr};
424     } else {
425         die "implement me";
426     }
427
428     my $target;
429
430     if ($to eq 'host') {
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+)$/) {
437         my $vmid = $1;
438         $target = extract_ct_info($vmdata, $vmid);
439         $target->{iface} = 'venet-in';
440
441         if ($target->{ip_address}) {
442             $pkg->{dest} = $target->{ip_address};
443         } else {
444             die "implement me";
445         }
446    } elsif ($to =~ m/^vm(\d+)$/) {
447         my $vmid = $1;
448         $target = extract_vm_info($vmdata, $vmid);
449         $target->{iface} = $target->{tapdev};
450     } else {
451         die "implement me";
452     }
453
454     my $res = route_packet($ruleset, $ipset_ruleset, $pkg, $from_info, $target, $start_state);
455
456     die "test failed ($res != $action)\n" if $action ne $res;
457
458     return undef; 
459 }
460
461 sub run_tests {
462     my ($vmdata, $testdir) = @_;
463
464     $vmdata->{testdir} = $testdir;
465
466     my ($ruleset, $ipset_ruleset) = 
467         PVE::Firewall::compile(undef, undef, $vmdata);
468
469     my $testfile = "$testdir/tests";
470     my $fh = IO::File->new($testfile) ||
471         die "unable to open '$testfile' - $!\n";
472
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;
478             die $@ if $@;
479             $trace = '';
480             print Dumper($ruleset) if $debug;
481             eval { simulate_firewall($ruleset, $ipset_ruleset, $vmdata, $test); };
482             if (my $err = $@) {
483
484                 print Dumper($ruleset) if !$debug;
485
486                 print "$trace\n" if !$debug;
487
488                 print "$testfile line $.: $line";
489
490                 print "test failed: $err\n";
491
492                 exit(-1);
493             }
494         } else {
495             die "parse error";
496         }
497     }
498
499     print "PASS: $testfile\n";
500
501     return undef;
502 }
503
504 my $vmdata = {
505     qemu => {
506         100 => {
507             net0 => "e1000=0E:0B:38:B8:B3:21,bridge=vmbr0",
508         },
509         101 => {
510             net0 => "e1000=0E:0B:38:B8:B3:22,bridge=vmbr0",
511         },
512         # on bridge vmbr1
513         110 => {
514             net0 => "e1000=0E:0B:38:B8:B4:21,bridge=vmbr1",
515         },
516     },
517     openvz => {
518         200 => {
519             ip_address => { value => '10.0.200.1' },
520         },
521         201 => {
522             ip_address => { value => '10.0.200.2' },
523         },
524     },
525 };
526
527 foreach my $dir (<test-*>) {
528     next if ! -d $dir;
529     run_tests($vmdata, $dir);
530 }
531
532 print "OK - all tests passed\n";
533
534 exit(0);