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