]> git.proxmox.com Git - pve-firewall.git/blob - PVE/Firewall.pm
use input parameter to feed iptables-restore
[pve-firewall.git] / PVE / Firewall.pm
1 package PVE::Firewall;
2
3 use warnings;
4 use strict;
5 use Data::Dumper;
6 use PVE::Tools;
7 use PVE::QemuServer;
8 use File::Path;
9 use IO::File;
10 use Net::IP;
11 use PVE::Tools qw(run_command lock_file);
12
13 use Data::Dumper;
14
15 my $pve_fw_lock_filename = "/var/lock/pvefw.lck";
16
17 my $macros;
18 my @ruleset = ();
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 $rule_format = "%-15s %-30s %-30s %-15s %-15s %-15s\n";
141
142 sub iptables {
143 my ($cmd) = @_;
144
145 run_command("/sbin/iptables $cmd", outfunc => sub {}, errfunc => sub {});
146 }
147
148 sub iptables_restore {
149
150 unshift (@ruleset, '*filter');
151 push (@ruleset, 'COMMIT');
152
153 my $cmdlist = join("\n", @ruleset);
154
155 run_command("/sbin/iptables-restore -n", input => $cmdlist, outfunc => sub {});
156 }
157
158 sub iptables_addrule {
159 my ($rule) = @_;
160
161 push (@ruleset, $rule);
162 }
163
164 sub iptables_chain_exist {
165 my ($chain) = @_;
166
167 eval{
168 iptables("-n --list $chain");
169 };
170 return undef if $@;
171
172 return 1;
173 }
174
175 sub iptables_rule_exist {
176 my ($rule) = @_;
177
178 eval{
179 iptables("-C $rule");
180 };
181 return undef if $@;
182
183 return 1;
184 }
185
186 sub iptables_generate_rule {
187 my ($chain, $rule) = @_;
188
189 my $cmd = "-A $chain";
190
191 $cmd .= " -m iprange --src-range" if $rule->{nbsource} && $rule->{nbsource} > 1;
192 $cmd .= " -s $rule->{source}" if $rule->{source};
193 $cmd .= " -m iprange --dst-range" if $rule->{nbdest} && $rule->{nbdest} > 1;
194 $cmd .= " -d $rule->{dest}" if $rule->{destination};
195 $cmd .= " -p $rule->{proto}" if $rule->{proto};
196 $cmd .= " --match multiport" if $rule->{nbdport} && $rule->{nbdport} > 1;
197 $cmd .= " --dport $rule->{dport}" if $rule->{dport};
198 $cmd .= " --match multiport" if $rule->{nbsport} && $rule->{nbsport} > 1;
199 $cmd .= " --sport $rule->{sport}" if $rule->{sport};
200 $cmd .= " -j $rule->{action}" if $rule->{action};
201
202 iptables_addrule($cmd);
203
204 }
205
206 sub generate_bridge_rules {
207 my ($bridge) = @_;
208
209 if(!iptables_chain_exist("BRIDGEFW-OUT")){
210 iptables_addrule(":BRIDGEFW-OUT - [0:0]");
211 }
212
213 if(!iptables_chain_exist("BRIDGEFW-IN")){
214 iptables_addrule(":BRIDGEFW-IN - [0:0]");
215 }
216
217 if(!iptables_chain_exist("proxmoxfw-FORWARD")){
218 iptables_addrule(":proxmoxfw-FORWARD - [0:0]");
219 iptables_addrule("-I FORWARD -j proxmoxfw-FORWARD");
220 iptables_addrule("-A proxmoxfw-FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT");
221 iptables_addrule("-A proxmoxfw-FORWARD -m physdev --physdev-is-in --physdev-is-bridged -j BRIDGEFW-OUT");
222 iptables_addrule("-A proxmoxfw-FORWARD -m physdev --physdev-is-out --physdev-is-bridged -j BRIDGEFW-IN");
223
224 }
225
226 generate_proxmoxfwinput();
227
228 if(!iptables_chain_exist("$bridge-IN")){
229 iptables_addrule(":$bridge-IN - [0:0]");
230 iptables_addrule("-A proxmoxfw-FORWARD -i $bridge -j DROP"); #disable interbridge routing
231 iptables_addrule("-A BRIDGEFW-IN -j $bridge-IN");
232 iptables_addrule("-A $bridge-IN -j ACCEPT");
233
234 }
235
236 if(!iptables_chain_exist("$bridge-OUT")){
237 iptables_addrule(":$bridge-OUT - [0:0]");
238 iptables_addrule("-A proxmoxfw-FORWARD -o $bridge -j DROP"); # disable interbridge routing
239 iptables_addrule("-A BRIDGEFW-OUT -j $bridge-OUT");
240
241 }
242
243 }
244
245
246 sub generate_tap_rules_direction {
247 my ($iface, $netid, $rules, $bridge, $direction) = @_;
248
249 my $tapchain = "$iface-$direction";
250
251 iptables_addrule(":$tapchain - [0:0]");
252
253 iptables_addrule("-A $tapchain -m state --state INVALID -j DROP");
254 iptables_addrule("-A $tapchain -m state --state RELATED,ESTABLISHED -j ACCEPT");
255
256 if (scalar(@$rules)) {
257 foreach my $rule (@$rules) {
258 next if $rule->{iface} && $rule->{iface} ne $netid;
259 if($rule->{action} =~ m/^(GROUP-(\S+))$/){
260 $rule->{action} .= "-$direction";
261 #generate empty group rule if don't exist
262 if(!iptables_chain_exist($rule->{action})){
263 generate_group_rules($2);
264 }
265 }
266 #we go to vmbr-IN if accept in out rules
267 $rule->{action} = "$bridge-IN" if $rule->{action} eq 'ACCEPT' && $direction eq 'OUT';
268 iptables_generate_rule($tapchain, $rule);
269 }
270 }
271
272 iptables_addrule("-A $tapchain -j LOG --log-prefix \"$tapchain-dropped: \" --log-level 4");
273 iptables_addrule("-A $tapchain -j DROP");
274
275 #plug the tap chain to bridge chain
276 my $physdevdirection = $direction eq 'IN' ? "out":"in";
277 my $rule = "$bridge-$direction -m physdev --physdev-$physdevdirection $iface --physdev-is-bridged -j $tapchain";
278
279 if(!iptables_rule_exist($rule)){
280 iptables_addrule("-I $rule");
281 }
282
283 if($direction eq 'OUT'){
284 #add tap->host rules
285 my $rule = "proxmoxfw-INPUT -m physdev --physdev-$physdevdirection $iface -j $tapchain";
286
287 if(!iptables_rule_exist($rule)){
288 iptables_addrule("-A $rule");
289 }
290 }
291 }
292
293 sub generate_tap_rules {
294 my ($net, $netid, $vmid) = @_;
295
296 my $filename = "/etc/pve/firewall/$vmid.fw";
297 my $fh = IO::File->new($filename, O_RDONLY);
298 return if !$fh;
299
300 #generate bridge rules
301 my $bridge = $net->{bridge};
302 my $tag = $net->{tag};
303 $bridge .= "v$tag" if $tag;
304
305 #generate tap chain
306 my $rules = parse_fw_rules($filename, $fh);
307
308 my $inrules = $rules->{in};
309 my $outrules = $rules->{out};
310
311 my $iface = "tap".$vmid."i".$1 if $netid =~ m/net(\d+)/;
312
313 generate_bridge_rules($bridge);
314 generate_tap_rules_direction($iface, $netid, $inrules, $bridge, 'IN');
315 generate_tap_rules_direction($iface, $netid, $outrules, $bridge, 'OUT');
316 iptables_restore();
317 }
318
319 sub flush_tap_rules {
320 my ($net, $netid, $vmid) = @_;
321
322 my $bridge = $net->{bridge};
323 my $iface = "tap".$vmid."i".$1 if $netid =~ m/net(\d+)/;
324
325 flush_tap_rules_direction($iface, $bridge, 'IN');
326 flush_tap_rules_direction($iface, $bridge, 'OUT');
327 iptables_restore();
328 }
329
330 sub flush_tap_rules_direction {
331 my ($iface, $bridge, $direction) = @_;
332
333 my $tapchain = "$iface-$direction";
334
335 if(iptables_chain_exist($tapchain)){
336 iptables_addrule("-F $tapchain");
337
338 my $physdevdirection = $direction eq 'IN' ? "out":"in";
339 my $rule = "$bridge-$direction -m physdev --physdev-$physdevdirection $iface --physdev-is-bridged -j $tapchain";
340 if(iptables_rule_exist($rule)){
341 iptables_addrule("-D $rule");
342 }
343
344 if($direction eq 'OUT'){
345 my $rule = "proxmoxfw-INPUT -m physdev --physdev-$physdevdirection $iface -j $tapchain";
346 if(iptables_rule_exist($rule)){
347 iptables_addrule("-D $rule");
348 }
349 }
350
351 iptables_addrule("-X $tapchain");
352 }
353 }
354
355 sub enablehostfw {
356
357 generate_proxmoxfwinput();
358 generate_proxmoxfwoutput();
359
360 my $filename = "/etc/pve/local/host.fw";
361 my $fh = IO::File->new($filename, O_RDONLY);
362 return if !$fh;
363
364 my $rules = parse_fw_rules($filename, $fh);
365 my $inrules = $rules->{in};
366 my $outrules = $rules->{out};
367
368 #host inbound firewall
369 iptables_addrule(":host-IN - [0:0]");
370 iptables_addrule("-A host-IN -m state --state INVALID -j DROP");
371 iptables_addrule("-A host-IN -m state --state RELATED,ESTABLISHED -j ACCEPT");
372 iptables_addrule("-A host-IN -i lo -j ACCEPT");
373 iptables_addrule("-A host-IN -m addrtype --dst-type MULTICAST -j ACCEPT");
374 iptables_addrule("-A host-IN -p udp -m state --state NEW -m multiport --dports 5404,5405 -j ACCEPT");
375 iptables_addrule("-A host-IN -p udp -m udp --dport 9000 -j ACCEPT"); #corosync
376
377 if (scalar(@$inrules)) {
378 foreach my $rule (@$inrules) {
379 #we use RETURN because we need to check also tap rules
380 $rule->{action} = 'RETURN' if $rule->{action} eq 'ACCEPT';
381 iptables_generate_rule('host-IN', $rule);
382 }
383 }
384
385 iptables_addrule("-A host-IN -j LOG --log-prefix \"kvmhost-IN dropped: \" --log-level 4");
386 iptables_addrule("-A host-IN -j DROP");
387
388 #host outbound firewall
389 iptables_addrule(":host-OUT - [0:0]");
390 iptables_addrule("-A host-OUT -m state --state INVALID -j DROP");
391 iptables_addrule("-A host-OUT -m state --state RELATED,ESTABLISHED -j ACCEPT");
392 iptables_addrule("-A host-OUT -o lo -j ACCEPT");
393 iptables_addrule("-A host-OUT -m addrtype --dst-type MULTICAST -j ACCEPT");
394 iptables_addrule("-A host-OUT -p udp -m state --state NEW -m multiport --dports 5404,5405 -j ACCEPT");
395 iptables_addrule("-A host-OUT -p udp -m udp --dport 9000 -j ACCEPT"); #corosync
396
397 if (scalar(@$outrules)) {
398 foreach my $rule (@$outrules) {
399 #we use RETURN because we need to check also tap rules
400 $rule->{action} = 'RETURN' if $rule->{action} eq 'ACCEPT';
401 iptables_generate_rule('host-OUT', $rule);
402 }
403 }
404
405 iptables_addrule("-A host-OUT -j LOG --log-prefix \"kvmhost-OUT dropped: \" --log-level 4");
406 iptables_addrule("-A host-OUT -j DROP");
407
408
409 my $rule = "proxmoxfw-INPUT -j host-IN";
410 if(!iptables_rule_exist($rule)){
411 iptables_addrule("-I $rule");
412 }
413
414 $rule = "proxmoxfw-OUTPUT -j host-OUT";
415 if(!iptables_rule_exist($rule)){
416 iptables_addrule("-I $rule");
417 }
418
419 iptables_restore();
420
421
422 }
423
424 sub disablehostfw {
425
426 my $chain = "host-IN";
427
428 my $rule = "proxmoxfw-INPUT -j $chain";
429 if(iptables_rule_exist($rule)){
430 iptables_addrule("-D $rule");
431 }
432
433 if(iptables_chain_exist($chain)){
434 iptables_addrule("-F $chain");
435 iptables_addrule("-X $chain");
436 }
437
438 $chain = "host-OUT";
439
440 $rule = "proxmoxfw-OUTPUT -j $chain";
441 if(iptables_rule_exist($rule)){
442 iptables_addrule("-D $rule");
443 }
444
445 if(iptables_chain_exist($chain)){
446 iptables_addrule("-F $chain");
447 iptables_addrule("-X $chain");
448 }
449
450 iptables_restore();
451 }
452
453 sub generate_proxmoxfwinput {
454
455 if(!iptables_chain_exist("proxmoxfw-INPUT")){
456 iptables_addrule(":proxmoxfw-INPUT - [0:0]");
457 iptables_addrule("-I INPUT -j proxmoxfw-INPUT");
458 iptables_addrule("-A INPUT -j ACCEPT");
459 }
460 }
461
462 sub generate_proxmoxfwoutput {
463
464 if(!iptables_chain_exist("proxmoxfw-OUTPUT")){
465 iptables_addrule(":proxmoxfw-OUTPUT - [0:0]");
466 iptables_addrule("-I OUTPUT -j proxmoxfw-OUTPUT");
467 iptables_addrule("-A OUTPUT -j ACCEPT");
468 }
469
470 }
471
472 sub enable_group_rules {
473 my ($group) = @_;
474
475 generate_group_rules($group);
476 iptables_restore();
477 }
478
479 sub generate_group_rules {
480 my ($group) = @_;
481
482 my $filename = "/etc/pve/firewall/groups.fw";
483 my $fh = IO::File->new($filename, O_RDONLY);
484 return if !$fh;
485
486 my $rules = parse_fw_rules($filename, $fh, $group);
487 my $inrules = $rules->{in};
488 my $outrules = $rules->{out};
489
490 my $chain = "GROUP-".$group."-IN";
491
492 iptables_addrule(":$chain - [0:0]");
493
494 if (scalar(@$inrules)) {
495 foreach my $rule (@$inrules) {
496 iptables_generate_rule($chain, $rule);
497 }
498 }
499
500 $chain = "GROUP-".$group."-OUT";
501
502 iptables_addrule(":$chain - [0:0]");
503
504 if(!iptables_chain_exist("BRIDGEFW-OUT")){
505 iptables_addrule(":BRIDGEFW-OUT - [0:0]");
506 }
507
508 if(!iptables_chain_exist("BRIDGEFW-IN")){
509 iptables_addrule(":BRIDGEFW-IN - [0:0]");
510 }
511
512 if (scalar(@$outrules)) {
513 foreach my $rule (@$outrules) {
514 #we go the BRIDGEFW-IN because we need to check also other tap rules
515 #(and group rules can be set on any bridge, so we can't go to VMBRXX-IN)
516 $rule->{action} = 'BRIDGEFW-IN' if $rule->{action} eq 'ACCEPT';
517 iptables_generate_rule($chain, $rule);
518 }
519 }
520
521 }
522
523 sub disable_group_rules {
524 my ($group) = @_;
525
526 my $chain = "GROUP-".$group."-IN";
527
528 if(iptables_chain_exist($chain)){
529 iptables_addrule("-F $chain");
530 iptables_addrule("-X $chain");
531 }
532
533 $chain = "GROUP-".$group."-OUT";
534
535 if(iptables_chain_exist($chain)){
536 iptables_addrule("-F $chain");
537 iptables_addrule("-X $chain");
538 }
539
540 #iptables_restore will die if security group is linked in a tap chain
541 #maybe can we improve that, parsing each vm config, or parsing iptables -S
542 #to see if the security group is linked or not
543 iptables_restore();
544 }
545
546
547 my $generate_input_rule = sub {
548 my ($zoneinfo, $rule, $net, $netid) = @_;
549
550 my $zone = $net->{zone} || die "internal error";
551 my $zid = $zoneinfo->{$zone}->{zoneref} || die "internal error";
552 my $tap = $net->{tap} || die "internal error";
553
554 my $dest = "$zid:$tap";
555
556 if ($rule->{dest}) {
557 $dest .= ":$rule->{dest}";
558 }
559
560 my $action = $rule->{service} ?
561 "$rule->{service}($rule->{action})" : $rule->{action};
562
563 my $sources = [];
564
565 if (!$rule->{source}) {
566 push @$sources, 'all';
567 } elsif ($zoneinfo->{$zone}->{type} eq 'bport') {
568 my $bridge_zone = $zoneinfo->{$zone}->{bridge_zone} || die "internal error";
569 my $zoneref = $zoneinfo->{$bridge_zone}->{zoneref} || die "internal error";
570
571 # using 'all' does not work, so we create one rule for
572 # each related zone on the same bridge
573 push @$sources, "${zoneref}:$rule->{source}";
574 foreach my $z (keys %$zoneinfo) {
575 next if $z eq $zone;
576 next if !$zoneinfo->{$z}->{bridge_zone};
577 next if $zoneinfo->{$z}->{bridge_zone} ne $bridge_zone;
578 $zoneref = $zoneinfo->{$z}->{zoneref} || die "internal error";
579 push @$sources, "${zoneref}:$rule->{source}";
580 }
581 } else {
582 push @$sources, "all:$rule->{source}";
583 }
584
585 my $out = '';
586
587 foreach my $source (@$sources) {
588 $out .= sprintf($rule_format, $action, $source, $dest, $rule->{proto} || '-',
589 $rule->{dport} || '-', $rule->{sport} || '-');
590 }
591
592 return $out;
593 };
594
595 my $generate_output_rule = sub {
596 my ($zoneinfo, $rule, $net, $netid) = @_;
597
598 my $zone = $net->{zone} || die "internal error";
599 my $zid = $zoneinfo->{$zone}->{zoneref} || die "internal error";
600 my $tap = $net->{tap} || die "internal error";
601
602 my $action = $rule->{service} ?
603 "$rule->{service}($rule->{action})" : $rule->{action};
604
605 my $dest;
606
607 if (!$rule->{dest}) {
608 $dest = 'all';
609 } else {
610 $dest = "all:$rule->{dest}";
611 }
612
613 return sprintf($rule_format, $action, "$zid:$tap", $dest,
614 $rule->{proto} || '-', $rule->{dport} || '-', $rule->{sport} || '-');
615 };
616
617 # we need complete VM configuration of all VMs (openvz/qemu)
618 # in vmdata
619
620 my $compile_shorewall = sub {
621 my ($targetdir, $vmdata, $rules) = @_;
622
623 # remove existing data ?
624 foreach my $file (qw(params zones rules interfaces maclist policy)) {
625 unlink "$targetdir/$file";
626 }
627
628 my $netinfo;
629
630 my $zoneinfo = {
631 fw => { type => 'firewall' },
632 };
633
634 my $maclist = {};
635
636 my $register_bridge;
637
638 $register_bridge = sub {
639 my ($bridge, $vlan) = @_;
640
641 my $zone = $bridge;
642
643 return $zone if $zoneinfo->{$zone};
644
645 my $ext_zone = "${bridge}_ext";
646
647 $zoneinfo->{$zone} = {
648 type => 'bridge',
649 bridge => $bridge,
650 bridge_ext_zone => $ext_zone,
651 };
652
653 # physical input devices
654 my $dir = "/sys/class/net/$bridge/brif";
655 my $physical = {};
656 PVE::Tools::dir_glob_foreach($dir, '((eth|bond).+)', sub {
657 my ($slave) = @_;
658 $physical->{$slave} = 1;
659 });
660
661 $zoneinfo->{$ext_zone} = {
662 type => 'bport',
663 bridge_zone => $zone,
664 ifaces => $physical,
665 };
666
667 return &$register_bridge("${bridge}v${vlan}") if defined($vlan);
668
669 return $zone;
670 };
671
672 my $register_bridge_port = sub {
673 my ($bridge, $vlan, $vmzone, $tap) = @_;
674
675 my $bridge_zone = &$register_bridge($bridge, $vlan);
676 my $zone = $bridge_zone . '_' . $vmzone;
677
678 if (!$zoneinfo->{$zone}) {
679 $zoneinfo->{$zone} = {
680 type => 'bport',
681 bridge_zone => $bridge_zone,
682 ifaces => {},
683 };
684 }
685
686 $zoneinfo->{$zone}->{ifaces}->{$tap} = 1;
687
688 return $zone;
689 };
690
691 foreach my $vmid (keys %{$vmdata->{qemu}}) {
692 $netinfo->{$vmid} = {};
693 my $conf = $vmdata->{qemu}->{$vmid};
694 foreach my $opt (keys %$conf) {
695 next if $opt !~ m/^net(\d+)$/;
696 my $netnum = $1;
697 my $net = PVE::QemuServer::parse_net($conf->{$opt});
698 next if !$net;
699 die "implement me" if !$net->{bridge};
700
701 my $vmzone = $conf->{zone} || "vm$vmid";
702 $net->{tap} = "tap${vmid}i${netnum}";
703 $maclist->{$net->{tap}} = $net->{macaddr} || die "internal error";
704 $net->{zone} = &$register_bridge_port($net->{bridge}, $net->{tag}, $vmzone, $net->{tap});
705 $netinfo->{$vmid}->{$opt} = $net;
706 }
707 }
708
709 #print Dumper($netinfo);
710
711 # NOTE: zone names have length limit, so we need to
712 # translate them into shorter names
713
714 my $zoneid = 0;
715 my $zonemap = { fw => 'fw' };
716
717 my $lookup_zonename = sub {
718 my ($zone) = @_;
719
720 return $zonemap->{$zone} if defined($zonemap->{$zone});
721 $zonemap->{$zone} = 'z' . $zoneid++;
722
723 return $zonemap->{$zone};
724 };
725
726 foreach my $z (sort keys %$zoneinfo) {
727 $zoneinfo->{$z}->{id} = &$lookup_zonename($z);
728 $zoneinfo->{$z}->{zonevar} = uc($z);
729 $zoneinfo->{$z}->{zoneref} = '$' . $zoneinfo->{$z}->{zonevar};
730 }
731
732 my $out;
733
734 # dump params file
735 $out = "# PVE zones\n";
736 foreach my $z (sort keys %$zoneinfo) {
737 $out .= "$zoneinfo->{$z}->{zonevar}=$zoneinfo->{$z}->{id}\n";
738 }
739 PVE::Tools::file_set_contents("$targetdir/params", $out);
740
741 # dump zone file
742
743 my $format = "%-30s %-10s %-15s\n";
744 $out = sprintf($format, '#ZONE', 'TYPE', 'OPTIONS');
745
746 foreach my $z (sort keys %$zoneinfo) {
747 my $zid = $zoneinfo->{$z}->{zoneref};
748 if ($zoneinfo->{$z}->{type} eq 'firewall') {
749 $out .= sprintf($format, $zid, $zoneinfo->{$z}->{type}, '');
750 } elsif ($zoneinfo->{$z}->{type} eq 'bridge') {
751 $out .= sprintf($format, $zid, 'ipv4', '');
752 } elsif ($zoneinfo->{$z}->{type} eq 'bport') {
753 my $bridge_zone = $zoneinfo->{$z}->{bridge_zone} || die "internal error";
754 my $bzid = $zoneinfo->{$bridge_zone}->{zoneref} || die "internal error";
755 $out .= sprintf($format, "$zid:$bzid", 'bport', '');
756 } else {
757 die "internal error";
758 }
759 }
760
761 $out .= sprintf("#LAST LINE - ADD YOUR ENTRIES ABOVE THIS ONE - DO NOT REMOVE\n");
762
763 PVE::Tools::file_set_contents("$targetdir/zones", $out);
764
765 # dump interfaces
766
767 $format = "%-25s %-20s %-10s %-15s\n";
768 $out = sprintf($format, '#ZONE', 'INTERFACE', 'BROADCAST', 'OPTIONS');
769
770 my $maclist_format = "%-15s %-15s %-15s\n";
771 my $macs = sprintf($maclist_format, '#DISPOSITION', 'INTERFACE', 'MACZONE');
772
773 foreach my $z (sort keys %$zoneinfo) {
774 my $zid = $zoneinfo->{$z}->{zoneref};
775 if ($zoneinfo->{$z}->{type} eq 'firewall') {
776 # do nothing;
777 } elsif ($zoneinfo->{$z}->{type} eq 'bridge') {
778 my $bridge = $zoneinfo->{$z}->{bridge} || die "internal error";
779 $out .= sprintf($format, $zid, $bridge, 'detect', 'bridge,optional');
780 } elsif ($zoneinfo->{$z}->{type} eq 'bport') {
781 my $ifaces = $zoneinfo->{$z}->{ifaces};
782 foreach my $iface (sort keys %$ifaces) {
783 my $bridge_zone = $zoneinfo->{$z}->{bridge_zone} || die "internal error";
784 my $bridge = $zoneinfo->{$bridge_zone}->{bridge} || die "internal error";
785 my $iftxt = "$bridge:$iface";
786
787 if ($maclist->{$iface}) {
788 $out .= sprintf($format, $zid, $iftxt, '-', 'maclist');
789 $macs .= sprintf($maclist_format, 'ACCEPT', $iface, $maclist->{$iface});
790 } else {
791 $out .= sprintf($format, $zid, $iftxt, '-', '');
792 }
793 }
794 } else {
795 die "internal error";
796 }
797 }
798
799 $out .= sprintf("#LAST LINE - ADD YOUR ENTRIES ABOVE THIS ONE - DO NOT REMOVE\n");
800
801 PVE::Tools::file_set_contents("$targetdir/interfaces", $out);
802
803 # dump maclist
804 PVE::Tools::file_set_contents("$targetdir/maclist", $macs);
805
806 # dump policy
807
808 $format = "%-15s %-15s %-15s %s\n";
809 $out = sprintf($format, '#SOURCE', 'DEST', 'POLICY', 'LOG');
810 $out .= sprintf($format, 'fw', 'all', 'ACCEPT', '');
811
812 # we need to disable intra-zone traffic on bridges. Else traffic
813 # from untracked interfaces simply pass the firewall
814 foreach my $z (sort keys %$zoneinfo) {
815 my $zid = $zoneinfo->{$z}->{zoneref};
816 if ($zoneinfo->{$z}->{type} eq 'bridge') {
817 $out .= sprintf($format, $zid, $zid, 'REJECT', 'info');
818 }
819 }
820 $out .= sprintf($format, 'all', 'all', 'REJECT', 'info');
821
822 PVE::Tools::file_set_contents("$targetdir/policy", $out);
823
824 # dump rules
825 $out = '';
826
827 $out = sprintf($rule_format, '#ACTION', 'SOURCE', 'DEST', 'PROTO', 'DPORT', 'SPORT');
828 foreach my $vmid (sort keys %$rules) {
829 my $inrules = $rules->{$vmid}->{in};
830 my $outrules = $rules->{$vmid}->{out};
831
832 if (scalar(@$inrules)) {
833 $out .= "# IN to VM $vmid\n";
834 foreach my $rule (@$inrules) {
835 foreach my $netid (keys %{$netinfo->{$vmid}}) {
836 my $net = $netinfo->{$vmid}->{$netid};
837 next if $rule->{iface} && $rule->{iface} ne $netid;
838 $out .= &$generate_input_rule($zoneinfo, $rule, $net, $netid);
839 }
840 }
841 }
842
843 if (scalar(@$outrules)) {
844 $out .= "# OUT from VM $vmid\n";
845 foreach my $rule (@$outrules) {
846 foreach my $netid (keys %{$netinfo->{$vmid}}) {
847 my $net = $netinfo->{$vmid}->{$netid};
848 next if $rule->{iface} && $rule->{iface} ne $netid;
849 $out .= &$generate_output_rule($zoneinfo, $rule, $net, $netid);
850 }
851 }
852 }
853 }
854
855 PVE::Tools::file_set_contents("$targetdir/rules", $out);
856 };
857
858
859 sub parse_fw_rules {
860 my ($filename, $fh, $group) = @_;
861
862 my $section;
863 my $securitygroup;
864 my $securitygroupexist;
865
866 my $res = { in => [], out => [] };
867
868 my $macros = get_firewall_macros();
869 my $protocols = get_etc_protocols();
870
871 while (defined(my $line = <$fh>)) {
872 next if $line =~ m/^#/;
873 next if $line =~ m/^\s*$/;
874
875 if ($line =~ m/^\[(in|out)(:(\S+))?\]\s*$/i) {
876 $section = lc($1);
877 $securitygroup = lc($3) if $3;
878 $securitygroupexist = 1 if $securitygroup && $securitygroup eq $group;
879 next;
880 }
881 next if !$section;
882 next if $group && $securitygroup ne $group;
883
884 my ($action, $iface, $source, $dest, $proto, $dport, $sport) =
885 split(/\s+/, $line);
886
887 if (!$action) {
888 warn "skip incomplete line\n";
889 next;
890 }
891
892 my $service;
893 if ($action =~ m/^(ACCEPT|DROP|REJECT|GROUP-(\S+))$/) {
894 # OK
895 } elsif ($action =~ m/^(\S+)\((ACCEPT|DROP|REJECT)\)$/) {
896 ($service, $action) = ($1, $2);
897 if (!$macros->{$service}) {
898 warn "unknown service '$service'\n";
899 next;
900 }
901 } else {
902 warn "unknown action '$action'\n";
903 next;
904 }
905
906 $iface = undef if $iface && $iface eq '-';
907 if ($iface && $iface !~ m/^(net0|net1|net2|net3|net4|net5)$/) {
908 warn "unknown interface '$iface'\n";
909 next;
910 }
911
912 $proto = undef if $proto && $proto eq '-';
913 if ($proto && !(defined($protocols->{byname}->{$proto}) ||
914 defined($protocols->{byid}->{$proto}))) {
915 warn "unknown protokol '$proto'\n";
916 next;
917 }
918
919 $source = undef if $source && $source eq '-';
920 $dest = undef if $dest && $dest eq '-';
921
922 $dport = undef if $dport && $dport eq '-';
923 $sport = undef if $sport && $sport eq '-';
924 my $nbdport = undef;
925 my $nbsport = undef;
926 my $nbsource = undef;
927 my $nbdest = undef;
928
929 eval {
930 $nbsource = parse_address_list($source) if $source;
931 $nbdest = parse_address_list($dest) if $dest;
932 $nbdport = parse_port_name_number_or_range($dport) if $dport;
933 $nbsport = parse_port_name_number_or_range($sport) if $sport;
934 };
935 if (my $err = $@) {
936 warn $err;
937 next;
938
939 }
940
941
942 my $rule = {
943 action => $action,
944 service => $service,
945 iface => $iface,
946 source => $source,
947 dest => $dest,
948 nbsource => $nbsource,
949 nbdest => $nbdest,
950 proto => $proto,
951 dport => $dport,
952 sport => $sport,
953 nbdport => $nbdport,
954 nbsport => $nbsport,
955
956 };
957
958 push @{$res->{$section}}, $rule;
959 }
960
961 die "security group $group don't exist" if $group && !$securitygroupexist;
962 return $res;
963 }
964
965 sub run_locked {
966 my ($code, @param) = @_;
967
968 my $timeout = 10;
969
970 my $res = lock_file($pve_fw_lock_filename, $timeout, $code, @param);
971
972 die $@ if $@;
973
974 return $res;
975 }
976
977 sub read_local_vm_config {
978
979 my $openvz = {};
980
981 my $qemu = {};
982
983 my $list = PVE::QemuServer::config_list();
984
985 foreach my $vmid (keys %$list) {
986 #next if !($vmid eq '100' || $vmid eq '102');
987 my $cfspath = PVE::QemuServer::cfs_config_path($vmid);
988 if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
989 $qemu->{$vmid} = $conf;
990 }
991 }
992
993 my $vmdata = { openvz => $openvz, qemu => $qemu };
994
995 return $vmdata;
996 };
997
998 sub read_vm_firewall_rules {
999 my ($vmdata) = @_;
1000 my $rules = {};
1001 foreach my $vmid (keys %{$vmdata->{qemu}}, keys %{$vmdata->{openvz}}) {
1002 my $filename = "/etc/pve/firewall/$vmid.fw";
1003 my $fh = IO::File->new($filename, O_RDONLY);
1004 next if !$fh;
1005
1006 $rules->{$vmid} = parse_fw_rules($filename, $fh);
1007 }
1008
1009 return $rules;
1010 }
1011
1012 sub compile {
1013 my $vmdata = read_local_vm_config();
1014 my $rules = read_vm_firewall_rules($vmdata);
1015
1016 # print Dumper($vmdata);
1017
1018 die "implement me";
1019 }
1020
1021 sub compile_and_start {
1022 my ($restart) = @_;
1023
1024 compile();
1025
1026 die "implement me";
1027 }
1028
1029 1;