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