implement macros
[pve-firewall.git] / PVE / Firewall.pm
1 package PVE::Firewall;
2
3 use warnings;
4 use strict;
5 use Data::Dumper;
6 use Digest::SHA;
7 use PVE::Tools;
8 use PVE::QemuServer;
9 use File::Path;
10 use IO::File;
11 use Net::IP;
12 use PVE::Tools qw(run_command lock_file);
13
14 use Data::Dumper;
15
16 my $pve_fw_lock_filename = "/var/lock/pvefw.lck";
17
18 # todo: define more MACROS
19 # inspired by: /usr/share/shorewall/macro.*
20 my $pve_fw_macros = {
21     BitTorrent => [
22         { action => 'PARAM', proto => 'tcp', dport => '6881:6889' },
23         { action => 'PARAM', proto => 'udp', dport => '6881' } 
24     ],
25     HTTP => [
26         { action => 'PARAM', proto => 'tcp', dport => '80' },
27     ],
28     HTTPS => [
29         { action => 'PARAM', proto => 'tcp', dport => '443' },
30     ],
31 };
32
33 my $pve_fw_parsed_macros;
34 my $pve_fw_preferred_macro_names = {};
35
36 sub get_firewall_macros {
37
38     return $pve_fw_parsed_macros if $pve_fw_parsed_macros;
39     
40     $pve_fw_parsed_macros = {};
41
42     foreach my $k (keys %$pve_fw_macros) {
43         my $name = lc($k);
44
45         my $macro =  $pve_fw_macros->{$k};
46         $pve_fw_preferred_macro_names->{$name} = $k;
47         $pve_fw_parsed_macros->{$name} = $macro;
48     }
49
50     return $pve_fw_parsed_macros;
51 }
52
53 my $etc_services;
54
55 sub get_etc_services {
56
57     return $etc_services if $etc_services;
58
59     my $filename = "/etc/services";
60
61     my $fh = IO::File->new($filename, O_RDONLY);
62     if (!$fh) {
63         warn "unable to read '$filename' - $!\n";
64         return {};
65     }
66
67     my $services = {};
68
69     while (my $line = <$fh>) {
70         chomp ($line);
71         next if $line =~m/^#/;
72         next if ($line =~m/^\s*$/);
73
74         if ($line =~ m!^(\S+)\s+(\S+)/(tcp|udp).*$!) {
75             $services->{byid}->{$2}->{name} = $1;
76             $services->{byid}->{$2}->{$3} = 1;
77             $services->{byname}->{$1} = $services->{byid}->{$2};
78         }
79     }
80
81     close($fh);
82
83     $etc_services = $services;    
84     
85
86     return $etc_services;
87 }
88
89 my $etc_protocols;
90
91 sub get_etc_protocols {
92     return $etc_protocols if $etc_protocols;
93
94     my $filename = "/etc/protocols";
95
96     my $fh = IO::File->new($filename, O_RDONLY);
97     if (!$fh) {
98         warn "unable to read '$filename' - $!\n";
99         return {};
100     }
101
102     my $protocols = {};
103
104     while (my $line = <$fh>) {
105         chomp ($line);
106         next if $line =~m/^#/;
107         next if ($line =~m/^\s*$/);
108
109         if ($line =~ m!^(\S+)\s+(\d+)\s+.*$!) {
110             $protocols->{byid}->{$2}->{name} = $1;
111             $protocols->{byname}->{$1} = $protocols->{byid}->{$2};
112         }
113     }
114
115     close($fh);
116
117     $etc_protocols = $protocols;
118
119     return $etc_protocols;
120 }
121
122 sub parse_address_list {
123     my ($str) = @_;
124
125     my $nbaor = 0;
126     foreach my $aor (split(/,/, $str)) {
127         if (!Net::IP->new($aor)) {
128             my $err = Net::IP::Error();
129             die "invalid IP address: $err\n";
130         }else{
131             $nbaor++;
132         }
133     }
134     return $nbaor;
135 }
136
137 sub parse_port_name_number_or_range {
138     my ($str) = @_;
139
140     my $services = PVE::Firewall::get_etc_services();
141     my $nbports = 0;
142     foreach my $item (split(/,/, $str)) {
143         my $portlist = "";
144         foreach my $pon (split(':', $item, 2)) {
145             if ($pon =~ m/^\d+$/){
146                 die "invalid port '$pon'\n" if $pon < 0 && $pon > 65536;
147             }else{
148                 die "invalid port $services->{byname}->{$pon}\n" if !$services->{byname}->{$pon};
149             }
150             $nbports++;
151         }
152     }
153
154     return ($nbports);
155 }
156
157 my $bridge_firewall_enabled = 0;
158
159 sub enable_bridge_firewall {
160
161     return if $bridge_firewall_enabled; # only once
162
163     system("echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables");
164     system("echo 1 > /proc/sys/net/bridge/bridge-nf-call-ip6tables");
165
166     $bridge_firewall_enabled = 1;
167 }
168
169 my $rule_format = "%-15s %-30s %-30s %-15s %-15s %-15s\n";
170
171 sub iptables {
172     my ($cmd) = @_;
173
174     run_command("/sbin/iptables $cmd", outfunc => sub {}, errfunc => sub {});
175 }
176
177 sub iptables_restore_cmdlist {
178     my ($cmdlist) = @_;
179
180     run_command("/sbin/iptables-restore -n", input => $cmdlist);
181 }
182
183 sub iptables_get_chains {
184
185     my $res = {};
186
187     # check what chains we want to track
188     my $is_pvefw_chain = sub {
189         my $name = shift;
190
191         return 1 if $name =~ m/^PVEFW-\S+$/;
192
193         return 1 if $name =~ m/^tap\d+i\d+-(:?IN|OUT)$/;
194         return 1 if $name =~ m/^vmbr\d+-(:?IN|OUT)$/;
195         return 1 if $name =~ m/^GROUP-(:?[^\s\-]+)-(:?IN|OUT)$/;
196
197         return undef;
198     };
199
200     my $table = '';
201
202     my $parser = sub {
203         my $line = shift;
204
205         return if $line =~ m/^#/;
206         return if $line =~ m/^\s*$/;
207
208         if ($line =~ m/^\*(\S+)$/) {
209             $table = $1;
210             return;
211         }
212
213         return if $table ne 'filter';
214
215         if ($line =~ m/^:(\S+)\s/) {
216             my $chain = $1;
217             return if !&$is_pvefw_chain($chain);
218             $res->{$chain} = "unknown";
219         } elsif ($line =~ m/^-A\s+(\S+)\s.*--comment\s+\"PVESIG:(\S+)\"/) {
220             my ($chain, $sig) = ($1, $2);
221             return if !&$is_pvefw_chain($chain);
222             $res->{$chain} = $sig;
223         } else {
224             # simply ignore the rest
225             return;
226         }
227     };
228
229     run_command("/sbin/iptables-save", outfunc => $parser);
230
231     return $res;
232 }
233
234 sub iptables_chain_exist {
235     my ($chain) = @_;
236
237     eval{
238         iptables("-n --list $chain");
239     };
240     return undef if $@;
241
242     return 1;
243 }
244
245 sub iptables_rule_exist {
246     my ($rule) = @_;
247
248     eval{
249         iptables("-C $rule");
250     };
251     return undef if $@;
252
253     return 1;
254 }
255
256 sub ruleset_generate_rule {
257     my ($ruleset, $chain, $rule, $goto) = @_;
258
259     my $cmd = '';
260
261     $cmd .= " -m iprange --src-range" if $rule->{nbsource} && $rule->{nbsource} > 1;
262     $cmd .= " -s $rule->{source}" if $rule->{source};
263     $cmd .= " -m iprange --dst-range" if $rule->{nbdest} && $rule->{nbdest} > 1;
264     $cmd .= " -d $rule->{dest}" if $rule->{destination};
265     $cmd .= " -p $rule->{proto}" if $rule->{proto};
266     $cmd .= "  --match multiport" if $rule->{nbdport} && $rule->{nbdport} > 1;
267     $cmd .= " --dport $rule->{dport}" if $rule->{dport};
268     $cmd .= "  --match multiport" if $rule->{nbsport} && $rule->{nbsport} > 1;
269     $cmd .= " --sport $rule->{sport}" if $rule->{sport};
270
271     if (my $action = $rule->{action}) {
272         $goto = 1 if !defined($goto) && $action eq 'PVEFW-SET-ACCEPT-MARK';
273         $cmd .= $goto ? " -g $action" : " -j $action";
274     };
275
276     ruleset_addrule($ruleset, $chain, $cmd) if $cmd;
277 }
278
279 sub ruleset_create_chain {
280     my ($ruleset, $chain) = @_;
281
282     die "Invalid chain name '$chain' (28 char max)\n" if length($chain) > 28;
283
284     die "chain '$chain' already exists\n" if $ruleset->{$chain};
285
286     $ruleset->{$chain} = [];
287 }
288
289 sub ruleset_chain_exist {
290     my ($ruleset, $chain) = @_;
291
292     return $ruleset->{$chain} ? 1 : undef;
293 }
294
295 sub ruleset_addrule {
296    my ($ruleset, $chain, $rule) = @_;
297
298    die "no such chain '$chain'\n" if !$ruleset->{$chain};
299
300    push @{$ruleset->{$chain}}, "-A $chain $rule";
301 }
302
303 sub ruleset_insertrule {
304    my ($ruleset, $chain, $rule) = @_;
305
306    die "no such chain '$chain'\n" if !$ruleset->{$chain};
307
308    unshift @{$ruleset->{$chain}}, "-A $chain $rule";
309 }
310
311 sub generate_bridge_chains {
312     my ($ruleset, $bridge) = @_;
313
314     if (!ruleset_chain_exist($ruleset, "PVEFW-BRIDGE-IN")){
315         ruleset_create_chain($ruleset, "PVEFW-BRIDGE-IN");
316     }
317
318     if (!ruleset_chain_exist($ruleset, "PVEFW-BRIDGE-OUT")){
319         ruleset_create_chain($ruleset, "PVEFW-BRIDGE-OUT");
320     }
321
322     if (!ruleset_chain_exist($ruleset, "PVEFW-FORWARD")){
323         ruleset_create_chain($ruleset, "PVEFW-FORWARD");
324
325         ruleset_addrule($ruleset, "PVEFW-FORWARD", "-m state --state RELATED,ESTABLISHED -j ACCEPT");
326         ruleset_addrule($ruleset, "PVEFW-FORWARD", "-m physdev --physdev-is-in --physdev-is-bridged -j PVEFW-BRIDGE-OUT");
327         ruleset_addrule($ruleset, "PVEFW-FORWARD", "-m physdev --physdev-is-out --physdev-is-bridged -j PVEFW-BRIDGE-IN");
328     }
329
330     if (!ruleset_chain_exist($ruleset, "$bridge-IN")) {
331         ruleset_create_chain($ruleset, "$bridge-IN");
332         ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i $bridge -j DROP");  # disable interbridge routing
333         ruleset_addrule($ruleset, "PVEFW-BRIDGE-IN", "-j $bridge-IN");
334         ruleset_addrule($ruleset, "$bridge-IN", "-j ACCEPT");
335     }
336
337     if (!ruleset_chain_exist($ruleset, "$bridge-OUT")) {
338         ruleset_create_chain($ruleset, "$bridge-OUT");
339         ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o $bridge -j DROP"); # disable interbridge routing
340         ruleset_addrule($ruleset, "PVEFW-BRIDGE-OUT", "-j $bridge-OUT");
341     }
342 }
343
344 sub generate_tap_rules_direction {
345     my ($ruleset, $group_rules, $iface, $netid, $macaddr, $rules, $bridge, $direction) = @_;
346
347     my $tapchain = "$iface-$direction";
348
349     ruleset_create_chain($ruleset, $tapchain);
350
351     ruleset_addrule($ruleset, $tapchain, "-m state --state INVALID -j DROP");
352     ruleset_addrule($ruleset, $tapchain, "-m state --state RELATED,ESTABLISHED -j ACCEPT");
353
354     if ($direction eq 'OUT' && defined($macaddr)) {
355         ruleset_addrule($ruleset, $tapchain, "-m mac ! --mac-source $macaddr -j DROP");
356     }
357
358     if ($rules) {
359         foreach my $rule (@$rules) {
360             next if $rule->{iface} && $rule->{iface} ne $netid;
361             # we go to $bridge-IN if accept in out rules
362             if($rule->{action}  =~ m/^(GROUP-(\S+))$/){
363                 $rule->{action} .= "-$direction";
364                 # generate empty group rule if don't exist
365                 if(!ruleset_chain_exist($ruleset, $rule->{action})){
366                     generate_group_rules($ruleset, $group_rules, $2);
367                 }
368                 ruleset_generate_rule($ruleset, $tapchain, $rule);
369                 ruleset_addrule($ruleset, $tapchain, "-m mark --mark 1 -g $bridge-IN")
370                     if $direction eq 'OUT';
371             } else {
372                 $rule->{action} = "$bridge-IN" if $rule->{action} eq 'ACCEPT' && $direction eq 'OUT';
373                 ruleset_generate_rule($ruleset, $tapchain, $rule);
374             }
375         }
376     }
377
378     ruleset_addrule($ruleset, $tapchain, "-j LOG --log-prefix \"$tapchain-dropped: \" --log-level 4");
379     ruleset_addrule($ruleset, $tapchain, "-j DROP");
380
381     # plug the tap chain to bridge chain
382     my $physdevdirection = $direction eq 'IN' ? "out" : "in";
383     my $rule = "-m physdev --physdev-$physdevdirection $iface --physdev-is-bridged -j $tapchain";
384     ruleset_insertrule($ruleset, "$bridge-$direction", $rule);
385
386     if ($direction eq 'OUT'){
387         # add tap->host rules
388         my $rule = "-m physdev --physdev-$physdevdirection $iface -j $tapchain";
389         ruleset_addrule($ruleset, "PVEFW-INPUT", $rule);
390     }
391 }
392
393 sub enablehostfw {
394     my ($ruleset, $rules, $group_rules) = @_;
395
396     # fixme: allow security groups
397
398     # host inbound firewall
399     my $chain = "PVEFW-HOST-IN";
400     ruleset_create_chain($ruleset, $chain);
401
402     ruleset_addrule($ruleset, $chain, "-m state --state INVALID -j DROP");
403     ruleset_addrule($ruleset, $chain, "-m state --state RELATED,ESTABLISHED -j ACCEPT");
404     ruleset_addrule($ruleset, $chain, "-i lo -j ACCEPT");
405     ruleset_addrule($ruleset, $chain, "-m addrtype --dst-type MULTICAST -j ACCEPT");
406     ruleset_addrule($ruleset, $chain, "-p udp -m state --state NEW -m multiport --dports 5404,5405 -j ACCEPT");
407     ruleset_addrule($ruleset, $chain, "-p udp -m udp --dport 9000 -j ACCEPT");  #corosync
408
409     if ($rules->{in}) {
410         foreach my $rule (@{$rules->{in}}) {
411             # we use RETURN because we need to check also tap rules
412             $rule->{action} = 'RETURN' if $rule->{action} eq 'ACCEPT';
413             ruleset_generate_rule($ruleset, $chain, $rule);
414         }
415     }
416
417     ruleset_addrule($ruleset, $chain, "-j LOG --log-prefix \"kvmhost-IN dropped: \" --log-level 4");
418     ruleset_addrule($ruleset, $chain, "-j DROP");
419
420     # host outbound firewall
421     $chain = "PVEFW-HOST-OUT";
422     ruleset_create_chain($ruleset, $chain);
423
424     ruleset_addrule($ruleset, $chain, "-m state --state INVALID -j DROP");
425     ruleset_addrule($ruleset, $chain, "-m state --state RELATED,ESTABLISHED -j ACCEPT");
426     ruleset_addrule($ruleset, $chain, "-o lo -j ACCEPT");
427     ruleset_addrule($ruleset, $chain, "-m addrtype --dst-type MULTICAST -j ACCEPT");
428     ruleset_addrule($ruleset, $chain, "-p udp -m state --state NEW -m multiport --dports 5404,5405 -j ACCEPT");
429     ruleset_addrule($ruleset, $chain, "-p udp -m udp --dport 9000 -j ACCEPT"); #corosync
430
431     if ($rules->{out}) {
432         foreach my $rule (@{$rules->{out}}) {
433             # we use RETURN because we need to check also tap rules
434             $rule->{action} = 'RETURN' if $rule->{action} eq 'ACCEPT';
435             ruleset_generate_rule($ruleset, $chain, $rule);
436         }
437     }
438
439     ruleset_addrule($ruleset, $chain, "-j LOG --log-prefix \"kvmhost-OUT dropped: \" --log-level 4");
440     ruleset_addrule($ruleset, $chain, "-j DROP");
441     
442     ruleset_addrule($ruleset, "PVEFW-OUTPUT", "-j PVEFW-HOST-OUT");
443     ruleset_addrule($ruleset, "PVEFW-INPUT", "-j PVEFW-HOST-IN");
444 }
445
446 sub generate_group_rules {
447     my ($ruleset, $group_rules, $group) = @_;
448
449     my $rules = $group_rules->{$group};
450
451     die "no such security group '$group'\n" if !$rules;
452     
453     my $chain = "GROUP-${group}-IN";
454
455     ruleset_create_chain($ruleset, $chain);
456
457     if ($rules->{in}) {
458         foreach my $rule (@{$rules->{in}}) {
459             ruleset_generate_rule($ruleset, $chain, $rule);
460         }
461     }
462
463     $chain = "GROUP-${group}-OUT";
464
465     ruleset_create_chain($ruleset, $chain);
466     ruleset_addrule($ruleset, $chain, "-j MARK --set-mark 0"); # clear mark
467
468     if ($rules->{out}) {
469         foreach my $rule (@{$rules->{out}}) {
470             # we go the PVEFW-SET-ACCEPT-MARK Instead of ACCEPT) because we need to 
471             # check also other tap rules (and group rules can be set on any bridge, 
472             # so we can't go to VMBRXX-IN)
473             $rule->{action} = 'PVEFW-SET-ACCEPT-MARK' if $rule->{action} eq 'ACCEPT';
474             ruleset_generate_rule($ruleset, $chain, $rule);
475         }
476     }
477 }
478
479 my $MAX_NETS = 32;
480 my $valid_netdev_names = {};
481 for (my $i = 0; $i < $MAX_NETS; $i++)  {
482     $valid_netdev_names->{"net$i"} = 1;
483 }
484
485 sub parse_fw_rule {
486     my ($line, $need_iface, $allow_groups) = @_;
487
488     my $macros = get_firewall_macros();
489     my $protocols = get_etc_protocols();
490
491     my ($action, $iface, $source, $dest, $proto, $dport, $sport);
492
493     $line =~ s/#.*$//;
494
495     my @data = split(/\s+/, $line);
496     my $expected_elements = $need_iface ? 7 : 6;
497
498     die "wrong number of rule elements\n" if scalar(@data) > $expected_elements;
499
500     if ($need_iface) {
501         ($action, $iface, $source, $dest, $proto, $dport, $sport) = @data
502     } else {
503         ($action, $source, $dest, $proto, $dport, $sport) =  @data;
504     }
505
506     die "incomplete rule\n" if !$action;
507
508     my $macro;
509     my $macro_name;
510
511     if ($action =~ m/^(ACCEPT|DROP|REJECT)$/) {
512         # OK
513     } elsif ($allow_groups && $action =~ m/^GROUP-(:?\S+)$/) {
514         # OK
515     } elsif ($action =~ m/^(\S+)\((ACCEPT|DROP|REJECT)\)$/) {
516         ($macro_name, $action) = ($1, $2);
517         my $lc_macro_name = lc($macro_name);
518         my $preferred_name = $pve_fw_preferred_macro_names->{$lc_macro_name};
519         $macro_name = $preferred_name if $preferred_name;
520         $macro = $macros->{$lc_macro_name};
521         die "unknown macro '$macro_name'\n" if !$macro;
522     } else {
523         die "unknown action '$action'\n";
524     }
525
526     if ($need_iface) {
527         $iface = undef if $iface && $iface eq '-';
528         die "unknown interface '$iface'\n" 
529             if defined($iface) && !$valid_netdev_names->{$iface};
530     }
531
532     $proto = undef if $proto && $proto eq '-';
533     die "unknown protokol '$proto'\n" if $proto && 
534         !(defined($protocols->{byname}->{$proto}) ||
535           defined($protocols->{byid}->{$proto}));
536
537     $source = undef if $source && $source eq '-';
538     $dest = undef if $dest && $dest eq '-';
539
540     $dport = undef if $dport && $dport eq '-';
541     $sport = undef if $sport && $sport eq '-';
542
543     my $nbsource = undef;
544     my $nbdest = undef;
545
546     $nbsource = parse_address_list($source) if $source;
547     $nbdest = parse_address_list($dest) if $dest;
548  
549     my $rules = [];
550
551     my $param = {
552         action => $action,
553         iface => $iface,
554         source => $source,
555         dest => $dest,
556         nbsource => $nbsource,
557         nbdest => $nbdest,
558         proto => $proto,
559         dport => $dport,
560         sport => $sport,
561     };
562
563     if ($macro) {
564         foreach my $templ (@$macro) {
565             my $rule = {};
566             foreach my $k (keys %$templ) {
567                 my $v = $templ->{$k};
568                 $v = $param->{$k} if $v eq 'PARAM';
569                 die "missing parameter '$k' in macro '$macro_name'\n" if !defined($v);
570                 $rule->{$k} = $v;
571             }
572             push @$rules, $rule;
573         }
574     } else {
575         push @$rules, $param;
576     }
577
578     foreach my $rule (@$rules) {
579         $rule->{nbdport} = parse_port_name_number_or_range($rule->{dport}) 
580             if defined($rule->{dport});
581         $rule->{nbsport} = parse_port_name_number_or_range($rule->{sport}) 
582             if defined($rule->{sport});
583     }
584
585     return $rules;
586 }
587
588 sub parse_vm_fw_rules {
589     my ($filename, $fh) = @_;
590
591     my $res = { in => [], out => [] };
592
593     my $section;
594
595     while (defined(my $line = <$fh>)) {
596         next if $line =~ m/^#/;
597         next if $line =~ m/^\s*$/;
598
599         my $linenr = $fh->input_line_number();
600         my $prefix = "$filename (line $linenr)";
601
602         if ($line =~ m/^\[(in|out)\]\s*$/i) {
603             $section = lc($1);
604             next;
605         }
606         if (!$section) {
607             warn "$prefix: skip line - no section";
608             next;
609         }
610
611         my $rules;
612         eval { $rules = parse_fw_rule($line, 1, 1); };
613         if (my $err = $@) {
614             warn "$prefix: $err";
615             next;
616         }
617
618         push @{$res->{$section}}, @$rules;
619     }
620
621     return $res;
622 }
623
624 sub parse_host_fw_rules {
625     my ($filename, $fh) = @_;
626
627     my $res = { in => [], out => [] };
628
629     my $section;
630
631     while (defined(my $line = <$fh>)) {
632         next if $line =~ m/^#/;
633         next if $line =~ m/^\s*$/;
634
635         my $linenr = $fh->input_line_number();
636         my $prefix = "$filename (line $linenr)";
637
638         if ($line =~ m/^\[(in|out)\]\s*$/i) {
639             $section = lc($1);
640             next;
641         }
642         if (!$section) {
643             warn "$prefix: skip line - no section";
644             next;
645         }
646
647         my $rules;
648         eval { $rules = parse_fw_rule($line, 1, 1); };
649         if (my $err = $@) {
650             warn "$prefix: $err";
651             next;
652         }
653
654         push @{$res->{$section}}, @$rules;
655     }
656
657     return $res;
658 }
659
660 sub parse_group_fw_rules {
661     my ($filename, $fh) = @_;
662
663     my $section;
664     my $group;
665
666     my $res = { in => [], out => [] };
667    
668     while (defined(my $line = <$fh>)) {
669         next if $line =~ m/^#/;
670         next if $line =~ m/^\s*$/;
671
672         my $linenr = $fh->input_line_number();
673         my $prefix = "$filename (line $linenr)";
674
675         if ($line =~ m/^\[(in|out):(\S+)\]\s*$/i) {
676             $section = lc($1);
677             $group = lc($2);
678             next;
679         }
680         if (!$section || !$group) {
681             warn "$prefix: skip line - no section";
682             next;
683         }
684
685         my $rules;
686         eval { $rules = parse_fw_rule($line, 0, 0); };
687         if (my $err = $@) {
688             warn "$prefix: $err";
689             next;
690         }
691
692         push @{$res->{$group}->{$section}}, @$rules;
693     }
694
695     return $res;
696 }
697
698 sub run_locked {
699     my ($code, @param) = @_;
700
701     my $timeout = 10;
702
703     my $res = lock_file($pve_fw_lock_filename, $timeout, $code, @param);
704
705     die $@ if $@;
706
707     return $res;
708 }
709
710 sub read_local_vm_config {
711
712     my $openvz = {};
713
714     my $qemu = {};
715
716     my $list = PVE::QemuServer::config_list();
717
718     foreach my $vmid (keys %$list) {
719         my $cfspath = PVE::QemuServer::cfs_config_path($vmid);
720         if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
721             $qemu->{$vmid} = $conf;
722         }
723     }
724
725     my $vmdata = { openvz => $openvz, qemu => $qemu };
726
727     return $vmdata;
728 };
729
730 sub read_vm_firewall_rules {
731     my ($vmdata) = @_;
732     my $rules = {};
733     foreach my $vmid (keys %{$vmdata->{qemu}}, keys %{$vmdata->{openvz}}) {
734         my $filename = "/etc/pve/firewall/$vmid.fw";
735         my $fh = IO::File->new($filename, O_RDONLY);
736         next if !$fh;
737
738         $rules->{$vmid} = parse_vm_fw_rules($filename, $fh);
739     }
740
741     return $rules;
742 }
743
744 sub compile {
745     my $vmdata = read_local_vm_config();
746     my $rules = read_vm_firewall_rules($vmdata);
747     
748     my $group_rules = {};
749     my $filename = "/etc/pve/firewall/groups.fw";
750     if (my $fh = IO::File->new($filename, O_RDONLY)) {
751         $group_rules = parse_group_fw_rules($filename, $fh);
752     }
753
754     #print Dumper($rules);
755
756     my $ruleset = {};
757
758     # setup host firewall rules
759     ruleset_create_chain($ruleset, "PVEFW-INPUT");
760     ruleset_create_chain($ruleset, "PVEFW-OUTPUT");
761
762     ruleset_create_chain($ruleset, "PVEFW-SET-ACCEPT-MARK");
763     ruleset_addrule($ruleset, "PVEFW-SET-ACCEPT-MARK", "-j MARK --set-mark 1");
764
765     $filename = "/etc/pve/local/host.fw";
766     if (my $fh = IO::File->new($filename, O_RDONLY)) {
767         my $host_rules = parse_host_fw_rules($filename, $fh);
768         enablehostfw($ruleset, $host_rules, $group_rules);
769     }
770
771     # generate firewall rules for QEMU VMs 
772     foreach my $vmid (keys %{$vmdata->{qemu}}) {
773         my $conf = $vmdata->{qemu}->{$vmid};
774         next if !$rules->{$vmid};
775
776         foreach my $netid (keys %$conf) {
777             next if $netid !~ m/^net(\d+)$/;
778             my $net = PVE::QemuServer::parse_net($conf->{$netid});
779             next if !$net;
780             my $iface = "tap${vmid}i$1";
781
782             my $bridge = $net->{bridge};
783             next if !$bridge; # fixme: ?
784
785             $bridge .= "v$net->{tag}" if $net->{tag};
786
787             generate_bridge_chains($ruleset, $bridge);
788
789             my $macaddr = $net->{macaddr};
790             generate_tap_rules_direction($ruleset, $group_rules, $iface, $netid, $macaddr, $rules->{$vmid}->{in}, $bridge, 'IN');
791             generate_tap_rules_direction($ruleset, $group_rules, $iface, $netid, $macaddr, $rules->{$vmid}->{out}, $bridge, 'OUT');
792         }
793     }
794     return $ruleset;
795 }
796
797 sub get_ruleset_status {
798     my ($ruleset, $verbose) = @_;
799
800     my $active_chains = iptables_get_chains();
801
802     my $statushash = {};
803
804     foreach my $chain (sort keys %$ruleset) {
805         my $digest = Digest::SHA->new('sha1');
806         foreach my $cmd (@{$ruleset->{$chain}}) {
807              $digest->add("$cmd\n");
808         }
809         my $sig = $digest->b64digest;
810         $statushash->{$chain}->{sig} = $sig;
811
812         my $oldsig = $active_chains->{$chain};
813         if (!defined($oldsig)) {
814             $statushash->{$chain}->{action} = 'create';
815         } else {
816             if ($oldsig eq $sig) {
817                 $statushash->{$chain}->{action} = 'exists';
818             } else {
819                 $statushash->{$chain}->{action} = 'update';
820             }
821         }
822         print "$statushash->{$chain}->{action} $chain ($sig)\n" if $verbose;
823         foreach my $cmd (@{$ruleset->{$chain}}) {
824             print "\t$cmd\n" if $verbose;
825         }
826     }
827
828     foreach my $chain (sort keys %$active_chains) {
829         if (!defined($ruleset->{$chain})) {
830             my $sig = $active_chains->{$chain};
831             $statushash->{$chain}->{action} = 'delete';
832             $statushash->{$chain}->{sig} = $sig;
833             print "delete $chain ($sig)\n" if $verbose;
834         }
835     }    
836
837     return $statushash;
838 }
839
840 sub print_ruleset {
841     my ($ruleset) = @_;
842
843     get_ruleset_status($ruleset, 1);
844 }
845
846 sub print_sig_rule {
847     my ($chain, $sig) = @_;
848
849     # We just use this to store a SHA1 checksum used to detect changes
850     return "-A $chain -m comment --comment \"PVESIG:$sig\"\n";
851 }
852
853 sub apply_ruleset {
854     my ($ruleset, $verbose) = @_;
855
856     enable_bridge_firewall();
857
858     my $cmdlist = "*filter\n"; # we pass this to iptables-restore;
859
860     my $statushash = get_ruleset_status($ruleset, $verbose);
861
862     # create missing chains first
863     foreach my $chain (sort keys %$ruleset) {
864         my $stat = $statushash->{$chain};
865         die "internal error" if !$stat;
866         next if $stat->{action} ne 'create';
867
868         $cmdlist .= ":$chain - [0:0]\n";
869     }
870
871     my $rule = "INPUT -j PVEFW-INPUT";
872     if (!PVE::Firewall::iptables_rule_exist($rule)) {
873         $cmdlist .= "-A $rule\n";
874     }
875     $rule = "OUTPUT -j PVEFW-OUTPUT";
876     if (!PVE::Firewall::iptables_rule_exist($rule)) {
877         $cmdlist .= "-A $rule\n";
878     }
879
880     $rule = "FORWARD -j PVEFW-FORWARD";
881     if (!PVE::Firewall::iptables_rule_exist($rule)) {
882         $cmdlist .= "-A $rule\n";
883     }
884
885     foreach my $chain (sort keys %$ruleset) {
886         my $stat = $statushash->{$chain};
887         die "internal error" if !$stat;
888
889         if ($stat->{action} eq 'update' || $stat->{action} eq 'create') {
890             $cmdlist .= "-F $chain\n";
891             foreach my $cmd (@{$ruleset->{$chain}}) {
892                 $cmdlist .= "$cmd\n";
893             }
894             $cmdlist .= print_sig_rule($chain, $stat->{sig});
895         } elsif ($stat->{action} eq 'delete') {
896             die "internal error"; # this should not happen
897         } elsif ($stat->{action} eq 'exists') {
898             # do nothing
899         } else {
900             die "internal error - unknown status '$stat->{action}'";
901         }
902     }
903
904     foreach my $chain (keys %$statushash) {
905         next if $statushash->{$chain}->{action} ne 'delete';
906         $cmdlist .= "-F $chain\n";
907     }
908     foreach my $chain (keys %$statushash) {
909         next if $statushash->{$chain}->{action} ne 'delete';
910         $cmdlist .= "-X $chain\n";
911     }
912
913     $cmdlist .= "COMMIT\n";
914
915     print $cmdlist if $verbose;
916
917     iptables_restore_cmdlist($cmdlist);
918
919     # test: re-read status and check if everything is up to date 
920     $statushash = get_ruleset_status($ruleset);
921
922     my $errors;
923     foreach my $chain (sort keys %$ruleset) {
924         my $stat = $statushash->{$chain};
925         if ($stat->{action} ne 'exists') {
926             warn "unable to update chain '$chain'\n";
927             $errors = 1;
928         }
929     }
930
931     die "unable to apply firewall changes\n" if $errors;
932 }
933
934 1;