basic bridge iptables implementation
[pve-firewall.git] / PVE / Firewall.pm
CommitLineData
b6360c3f
DM
1package PVE::Firewall;
2
3use warnings;
4use strict;
5use Data::Dumper;
f789653a 6use PVE::Tools;
b6360c3f 7use PVE::QemuServer;
5e1267a5
DM
8use File::Path;
9use IO::File;
ecbea048 10use Net::IP;
3a616aa0 11use PVE::Tools qw(run_command);
ecbea048 12
5e1267a5 13use Data::Dumper;
b6360c3f 14
9aab3127 15my $macros;
3a616aa0
AD
16my @ruleset = ();
17
9aab3127
DM
18sub get_shorewall_macros {
19
20 return $macros if $macros;
21
22 foreach my $path (</usr/share/shorewall/macro.*>) {
23 if ($path =~ m|/macro\.(\S+)$|) {
24 $macros->{$1} = 1;
25 }
26 }
27 return $macros;
28}
29
fcba0beb
DM
30my $etc_services;
31
32sub get_etc_services {
33
34 return $etc_services if $etc_services;
35
36 my $filename = "/etc/services";
37
38 my $fh = IO::File->new($filename, O_RDONLY);
39 if (!$fh) {
40 warn "unable to read '$filename' - $!\n";
41 return {};
42 }
43
44 my $services = {};
45
46 while (my $line = <$fh>) {
47 chomp ($line);
48 next if $line =~m/^#/;
49 next if ($line =~m/^\s*$/);
50
51 if ($line =~ m!^(\S+)\s+(\S+)/(tcp|udp).*$!) {
52 $services->{byid}->{$2}->{name} = $1;
53 $services->{byid}->{$2}->{$3} = 1;
54 $services->{byname}->{$1} = $services->{byid}->{$2};
55 }
56 }
57
58 close($fh);
59
60 $etc_services = $services;
61
3a616aa0 62
fcba0beb
DM
63 return $etc_services;
64}
65
66my $etc_protocols;
67
68sub get_etc_protocols {
69 return $etc_protocols if $etc_protocols;
70
71 my $filename = "/etc/protocols";
72
73 my $fh = IO::File->new($filename, O_RDONLY);
74 if (!$fh) {
75 warn "unable to read '$filename' - $!\n";
76 return {};
77 }
78
79 my $protocols = {};
80
81 while (my $line = <$fh>) {
82 chomp ($line);
83 next if $line =~m/^#/;
84 next if ($line =~m/^\s*$/);
85
86 if ($line =~ m!^(\S+)\s+(\d+)\s+.*$!) {
87 $protocols->{byid}->{$2}->{name} = $1;
88 $protocols->{byname}->{$1} = $protocols->{byid}->{$2};
89 }
90 }
91
92 close($fh);
93
94 $etc_protocols = $protocols;
95
96 return $etc_protocols;
97}
98
ecbea048
DM
99sub parse_address_list {
100 my ($str) = @_;
101
102 foreach my $aor (split(/,/, $str)) {
103 if (!Net::IP->new($aor)) {
104 my $err = Net::IP::Error();
105 die "invalid IP address: $err\n";
106 }
107 }
108}
dddd9413 109
fcba0beb
DM
110sub parse_port_name_number_or_range {
111 my ($str) = @_;
112
113 my $services = PVE::Firewall::get_etc_services();
114
115 foreach my $item (split(/,/, $str)) {
116 foreach my $pon (split(':', $item, 2)) {
117 next if $pon =~ m/^\d+$/ && $pon > 0 && $pon < 65536;
118 next if defined($services->{byname}->{$pon});
119 die "invalid port '$pon'\n";
120 }
121 }
122
123}
124
8cebfa6f 125my $rule_format = "%-15s %-30s %-30s %-15s %-15s %-15s\n";
dddd9413 126
3a616aa0
AD
127sub iptables {
128 my ($cmd) = @_;
129
130 run_command("/sbin/iptables $cmd", outfunc => sub {}, errfunc => sub {});
131}
132
133sub iptables_restore {
134
135 unshift (@ruleset, '*filter');
136 push (@ruleset, 'COMMIT');
137
138 my $cmdlist = join("\n", @ruleset);
139
140 run_command("echo '$cmdlist' | /sbin/iptables-restore -n", outfunc => sub {});
141}
142
143sub iptables_addrule {
144 my ($rule) = @_;
145
146 push (@ruleset, $rule);
147}
148
149sub iptables_chain_exist {
150 my ($chain) = @_;
151
152 eval{
153 iptables("-n --list $chain");
154 };
155 return undef if $@;
156
157 return 1;
158}
159
160sub iptables_rule_exist {
161 my ($rule) = @_;
162
163 eval{
164 iptables("-C $rule");
165 };
166 return undef if $@;
167
168 return 1;
169}
170
171sub iptables_generate_rule {
172 my ($chain, $rule) = @_;
173
174 my $cmd = "-A $chain";
175
176 $cmd .= " -s $rule->{source}" if $rule->{source};
177 $cmd .= " -d $rule->{dest}" if $rule->{destination};
178 $cmd .= " -p $rule->{proto}" if $rule->{proto};
179 $cmd .= " --dport $rule->{dport}" if $rule->{dport};
180 $cmd .= " --sport $rule->{sport}" if $rule->{sport};
181 $cmd .= " -j $rule->{action}" if $rule->{action};
182
183 iptables_addrule($cmd);
184
185}
186
187sub generate_bridge_rules {
188 my ($bridge) = @_;
189
190 if(!iptables_chain_exist("BRIDGEFW-OUT")){
191 iptables_addrule(":BRIDGEFW-OUT - [0:0]");
192 }
193
194 if(!iptables_chain_exist("BRIDGEFW-IN")){
195 iptables_addrule(":BRIDGEFW-IN - [0:0]");
196 }
197
198 if(!iptables_chain_exist("proxmoxfw-FORWARD")){
199 iptables_addrule(":proxmoxfw-FORWARD - [0:0]");
200 iptables_addrule("-I FORWARD -j proxmoxfw-FORWARD");
201 iptables_addrule("-A proxmoxfw-FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT");
202 iptables_addrule("-A proxmoxfw-FORWARD -m physdev --physdev-is-in --physdev-is-bridged -j BRIDGEFW-OUT");
203 iptables_addrule("-A proxmoxfw-FORWARD -m physdev --physdev-is-out --physdev-is-bridged -j BRIDGEFW-IN");
204
205 }
206
207 generate_proxmoxfwinput();
208
209 if(!iptables_chain_exist("$bridge-IN")){
210 iptables_addrule(":$bridge-IN - [0:0]");
211 iptables_addrule("-A proxmoxfw-FORWARD -i $bridge -j DROP"); #disable interbridge routing
212 iptables_addrule("-A BRIDGEFW-IN -j $bridge-IN");
213 iptables_addrule("-A $bridge-IN -j ACCEPT");
214
215 }
216
217 if(!iptables_chain_exist("$bridge-OUT")){
218 iptables_addrule(":$bridge-OUT - [0:0]");
219 iptables_addrule("-A proxmoxfw-FORWARD -o $bridge -j DROP"); # disable interbridge routing
220 iptables_addrule("-A BRIDGEFW-OUT -j $bridge-OUT");
221
222 }
223
224}
225
226
227sub generate_tap_rules_direction {
228 my ($iface, $netid, $rules, $bridge, $direction) = @_;
229
230 my $tapchain = "$iface-$direction";
231
232 iptables_addrule(":$tapchain - [0:0]");
233
234 iptables_addrule("-A $tapchain -m state --state INVALID -j DROP");
235 iptables_addrule("-A $tapchain -m state --state RELATED,ESTABLISHED -j ACCEPT");
236
237 if (scalar(@$rules)) {
238 foreach my $rule (@$rules) {
239 next if $rule->{iface} && $rule->{iface} ne $netid;
240 if($rule->{action} =~ m/^(GROUP-(\S+))$/){
241 $rule->{action} .= "-$direction";
242 #generate empty group rule if don't exist
243 if(!iptables_chain_exist($rule->{action})){
244 generate_group_rules($2);
245 }
246 }
247 #we go to vmbr-IN if accept in out rules
248 $rule->{action} = "$bridge-IN" if $rule->{action} eq 'ACCEPT' && $direction eq 'OUT';
249 iptables_generate_rule($tapchain, $rule);
250 }
251 }
252
253 iptables_addrule("-A $tapchain -j LOG --log-prefix \"$tapchain-dropped: \" --log-level 4");
254 iptables_addrule("-A $tapchain -j DROP");
255
256 #plug the tap chain to bridge chain
257 my $physdevdirection = $direction eq 'IN' ? "out":"in";
258 my $rule = "$bridge-$direction -m physdev --physdev-$physdevdirection $iface --physdev-is-bridged -j $tapchain";
259
260 if(!iptables_rule_exist($rule)){
261 iptables_addrule("-I $rule");
262 }
263
264 if($direction eq 'OUT'){
265 #add tap->host rules
266 my $rule = "proxmoxfw-INPUT -m physdev --physdev-$physdevdirection $iface -j $tapchain";
267
268 if(!iptables_rule_exist($rule)){
269 iptables_addrule("-A $rule");
270 }
271 }
272}
273
274sub generate_tap_rules {
275 my ($net, $netid, $vmid) = @_;
276
277 my $filename = "/etc/pve/firewall/$vmid.fw";
278 my $fh = IO::File->new($filename, O_RDONLY);
279 return if !$fh;
280
281 #generate bridge rules
282 my $bridge = $net->{bridge};
283 my $tag = $net->{tag};
284 $bridge .= "v$tag" if $tag;
285
286 #generate tap chain
287 my $rules = parse_fw_rules($filename, $fh);
288
289 my $inrules = $rules->{in};
290 my $outrules = $rules->{out};
291
292 my $iface = "tap".$vmid."i".$1 if $netid =~ m/net(\d+)/;
293
294 generate_bridge_rules($bridge);
295 generate_tap_rules_direction($iface, $netid, $inrules, $bridge, 'IN');
296 generate_tap_rules_direction($iface, $netid, $outrules, $bridge, 'OUT');
297 iptables_restore();
298}
299
300sub flush_tap_rules {
301 my ($net, $netid, $vmid) = @_;
302
303 my $bridge = $net->{bridge};
304 my $iface = "tap".$vmid."i".$1 if $netid =~ m/net(\d+)/;
305
306 flush_tap_rules_direction($iface, $bridge, 'IN');
307 flush_tap_rules_direction($iface, $bridge, 'OUT');
308 iptables_restore();
309}
310
311sub flush_tap_rules_direction {
312 my ($iface, $bridge, $direction) = @_;
313
314 my $tapchain = "$iface-$direction";
315
316 if(iptables_chain_exist($tapchain)){
317 iptables_addrule("-F $tapchain");
318
319 my $physdevdirection = $direction eq 'IN' ? "out":"in";
320 my $rule = "$bridge-$direction -m physdev --physdev-$physdevdirection $iface --physdev-is-bridged -j $tapchain";
321 if(iptables_rule_exist($rule)){
322 iptables_addrule("-D $rule");
323 }
324
325 if($direction eq 'OUT'){
326 my $rule = "proxmoxfw-INPUT -m physdev --physdev-$physdevdirection $iface -j $tapchain";
327
328 if(!iptables_rule_exist($rule)){
329 iptables_addrule("-D $rule");
330 }
331 }
332
333 iptables_addrule("-X $tapchain");
334 }
335}
336
dddd9413
DM
337my $generate_input_rule = sub {
338 my ($zoneinfo, $rule, $net, $netid) = @_;
339
dddd9413 340 my $zone = $net->{zone} || die "internal error";
a8195f4f 341 my $zid = $zoneinfo->{$zone}->{zoneref} || die "internal error";
dddd9413 342 my $tap = $net->{tap} || die "internal error";
9aab3127 343
8cebfa6f
DM
344 my $dest = "$zid:$tap";
345
346 if ($rule->{dest}) {
347 $dest .= ":$rule->{dest}";
348 }
349
9aab3127
DM
350 my $action = $rule->{service} ?
351 "$rule->{service}($rule->{action})" : $rule->{action};
352
b9b06789 353 my $sources = [];
8cebfa6f 354
b9b06789
DM
355 if (!$rule->{source}) {
356 push @$sources, 'all';
357 } elsif ($zoneinfo->{$zone}->{type} eq 'bport') {
8cebfa6f 358 my $bridge_zone = $zoneinfo->{$zone}->{bridge_zone} || die "internal error";
b9b06789
DM
359 my $zoneref = $zoneinfo->{$bridge_zone}->{zoneref} || die "internal error";
360
361 # using 'all' does not work, so we create one rule for
362 # each related zone on the same bridge
363 push @$sources, "${zoneref}:$rule->{source}";
364 foreach my $z (keys %$zoneinfo) {
365 next if $z eq $zone;
366 next if !$zoneinfo->{$z}->{bridge_zone};
367 next if $zoneinfo->{$z}->{bridge_zone} ne $bridge_zone;
368 $zoneref = $zoneinfo->{$z}->{zoneref} || die "internal error";
369 push @$sources, "${zoneref}:$rule->{source}";
8cebfa6f
DM
370 }
371 } else {
b9b06789
DM
372 push @$sources, "all:$rule->{source}";
373 }
374
375 my $out = '';
376
377 foreach my $source (@$sources) {
378 $out .= sprintf($rule_format, $action, $source, $dest, $rule->{proto} || '-',
379 $rule->{dport} || '-', $rule->{sport} || '-');
8cebfa6f
DM
380 }
381
b9b06789 382 return $out;
dddd9413
DM
383};
384
385my $generate_output_rule = sub {
386 my ($zoneinfo, $rule, $net, $netid) = @_;
387
dddd9413 388 my $zone = $net->{zone} || die "internal error";
a8195f4f 389 my $zid = $zoneinfo->{$zone}->{zoneref} || die "internal error";
dddd9413 390 my $tap = $net->{tap} || die "internal error";
9aab3127
DM
391
392 my $action = $rule->{service} ?
393 "$rule->{service}($rule->{action})" : $rule->{action};
dddd9413 394
8cebfa6f
DM
395 my $dest;
396
397 if (!$rule->{dest}) {
9a4644fa 398 $dest = 'all';
8cebfa6f 399 } else {
9a4644fa 400 $dest = "all:$rule->{dest}";
8cebfa6f
DM
401 }
402
403 return sprintf($rule_format, $action, "$zid:$tap", $dest,
dddd9413
DM
404 $rule->{proto} || '-', $rule->{dport} || '-', $rule->{sport} || '-');
405};
406
b6360c3f
DM
407# we need complete VM configuration of all VMs (openvz/qemu)
408# in vmdata
409
5e1267a5 410my $compile_shorewall = sub {
dddd9413 411 my ($targetdir, $vmdata, $rules) = @_;
b6360c3f 412
80bfe1ff 413 # remove existing data ?
a8195f4f 414 foreach my $file (qw(params zones rules interfaces maclist policy)) {
80bfe1ff
DM
415 unlink "$targetdir/$file";
416 }
417
b6360c3f
DM
418 my $netinfo;
419
886aba9c
DM
420 my $zoneinfo = {
421 fw => { type => 'firewall' },
422 };
423
026a6466
DM
424 my $maclist = {};
425
35081236
DM
426 my $register_bridge;
427
428 $register_bridge = sub {
429 my ($bridge, $vlan) = @_;
f789653a 430
1b6a0a59 431 my $zone = $bridge;
f789653a
DM
432
433 return $zone if $zoneinfo->{$zone};
434
1b6a0a59 435 my $ext_zone = "${bridge}_ext";
8cebfa6f 436
f789653a
DM
437 $zoneinfo->{$zone} = {
438 type => 'bridge',
439 bridge => $bridge,
8cebfa6f
DM
440 bridge_ext_zone => $ext_zone,
441 };
442
443 # physical input devices
444 my $dir = "/sys/class/net/$bridge/brif";
445 my $physical = {};
446 PVE::Tools::dir_glob_foreach($dir, '((eth|bond).+)', sub {
447 my ($slave) = @_;
448 $physical->{$slave} = 1;
449 });
450
451 $zoneinfo->{$ext_zone} = {
452 type => 'bport',
453 bridge_zone => $zone,
454 ifaces => $physical,
f789653a 455 };
35081236
DM
456
457 return &$register_bridge("${bridge}v${vlan}") if defined($vlan);
458
459 return $zone;
460 };
461
462 my $register_bridge_port = sub {
463 my ($bridge, $vlan, $vmzone, $tap) = @_;
464
465 my $bridge_zone = &$register_bridge($bridge, $vlan);
1b6a0a59 466 my $zone = $bridge_zone . '_' . $vmzone;
35081236
DM
467
468 if (!$zoneinfo->{$zone}) {
469 $zoneinfo->{$zone} = {
470 type => 'bport',
471 bridge_zone => $bridge_zone,
472 ifaces => {},
473 };
474 }
475
476 $zoneinfo->{$zone}->{ifaces}->{$tap} = 1;
f789653a
DM
477
478 return $zone;
479 };
480
b6360c3f
DM
481 foreach my $vmid (keys %{$vmdata->{qemu}}) {
482 $netinfo->{$vmid} = {};
483 my $conf = $vmdata->{qemu}->{$vmid};
484 foreach my $opt (keys %$conf) {
485 next if $opt !~ m/^net(\d+)$/;
dddd9413 486 my $netnum = $1;
b6360c3f
DM
487 my $net = PVE::QemuServer::parse_net($conf->{$opt});
488 next if !$net;
886aba9c 489 die "implement me" if !$net->{bridge};
886aba9c 490
f789653a 491 my $vmzone = $conf->{zone} || "vm$vmid";
dddd9413 492 $net->{tap} = "tap${vmid}i${netnum}";
026a6466 493 $maclist->{$net->{tap}} = $net->{macaddr} || die "internal error";
dddd9413
DM
494 $net->{zone} = &$register_bridge_port($net->{bridge}, $net->{tag}, $vmzone, $net->{tap});
495 $netinfo->{$vmid}->{$opt} = $net;
886aba9c
DM
496 }
497 }
498
499 #print Dumper($netinfo);
500
f789653a 501 # NOTE: zone names have length limit, so we need to
886aba9c
DM
502 # translate them into shorter names
503
f789653a
DM
504 my $zoneid = 0;
505 my $zonemap = { fw => 'fw' };
506
507 my $lookup_zonename = sub {
508 my ($zone) = @_;
509
510 return $zonemap->{$zone} if defined($zonemap->{$zone});
511 $zonemap->{$zone} = 'z' . $zoneid++;
512
513 return $zonemap->{$zone};
514 };
515
dddd9413
DM
516 foreach my $z (sort keys %$zoneinfo) {
517 $zoneinfo->{$z}->{id} = &$lookup_zonename($z);
a8195f4f
DM
518 $zoneinfo->{$z}->{zonevar} = uc($z);
519 $zoneinfo->{$z}->{zoneref} = '$' . $zoneinfo->{$z}->{zonevar};
dddd9413
DM
520 }
521
f789653a 522 my $out;
886aba9c 523
a8195f4f
DM
524 # dump params file
525 $out = "# PVE zones\n";
526 foreach my $z (sort keys %$zoneinfo) {
527 $out .= "$zoneinfo->{$z}->{zonevar}=$zoneinfo->{$z}->{id}\n";
528 }
529 PVE::Tools::file_set_contents("$targetdir/params", $out);
530
531 # dump zone file
532
533 my $format = "%-30s %-10s %-15s\n";
534 $out = sprintf($format, '#ZONE', 'TYPE', 'OPTIONS');
f789653a 535
886aba9c 536 foreach my $z (sort keys %$zoneinfo) {
a8195f4f 537 my $zid = $zoneinfo->{$z}->{zoneref};
f789653a 538 if ($zoneinfo->{$z}->{type} eq 'firewall') {
a8195f4f 539 $out .= sprintf($format, $zid, $zoneinfo->{$z}->{type}, '');
f789653a 540 } elsif ($zoneinfo->{$z}->{type} eq 'bridge') {
a8195f4f 541 $out .= sprintf($format, $zid, 'ipv4', '');
f789653a
DM
542 } elsif ($zoneinfo->{$z}->{type} eq 'bport') {
543 my $bridge_zone = $zoneinfo->{$z}->{bridge_zone} || die "internal error";
a8195f4f
DM
544 my $bzid = $zoneinfo->{$bridge_zone}->{zoneref} || die "internal error";
545 $out .= sprintf($format, "$zid:$bzid", 'bport', '');
f789653a
DM
546 } else {
547 die "internal error";
548 }
886aba9c
DM
549 }
550
f789653a
DM
551 $out .= sprintf("#LAST LINE - ADD YOUR ENTRIES ABOVE THIS ONE - DO NOT REMOVE\n");
552
553 PVE::Tools::file_set_contents("$targetdir/zones", $out);
886aba9c 554
f789653a 555 # dump interfaces
886aba9c 556
a8195f4f
DM
557 $format = "%-25s %-20s %-10s %-15s\n";
558 $out = sprintf($format, '#ZONE', 'INTERFACE', 'BROADCAST', 'OPTIONS');
f789653a 559
026a6466
DM
560 my $maclist_format = "%-15s %-15s %-15s\n";
561 my $macs = sprintf($maclist_format, '#DISPOSITION', 'INTERFACE', 'MACZONE');
562
886aba9c 563 foreach my $z (sort keys %$zoneinfo) {
a8195f4f 564 my $zid = $zoneinfo->{$z}->{zoneref};
f789653a
DM
565 if ($zoneinfo->{$z}->{type} eq 'firewall') {
566 # do nothing;
567 } elsif ($zoneinfo->{$z}->{type} eq 'bridge') {
568 my $bridge = $zoneinfo->{$z}->{bridge} || die "internal error";
a8195f4f 569 $out .= sprintf($format, $zid, $bridge, 'detect', 'bridge,optional');
f789653a
DM
570 } elsif ($zoneinfo->{$z}->{type} eq 'bport') {
571 my $ifaces = $zoneinfo->{$z}->{ifaces};
572 foreach my $iface (sort keys %$ifaces) {
573 my $bridge_zone = $zoneinfo->{$z}->{bridge_zone} || die "internal error";
574 my $bridge = $zoneinfo->{$bridge_zone}->{bridge} || die "internal error";
575 my $iftxt = "$bridge:$iface";
8cebfa6f
DM
576
577 if ($maclist->{$iface}) {
578 $out .= sprintf($format, $zid, $iftxt, '-', 'maclist');
579 $macs .= sprintf($maclist_format, 'ACCEPT', $iface, $maclist->{$iface});
580 } else {
581 $out .= sprintf($format, $zid, $iftxt, '-', '');
582 }
f789653a
DM
583 }
584 } else {
585 die "internal error";
b6360c3f
DM
586 }
587 }
588
f789653a
DM
589 $out .= sprintf("#LAST LINE - ADD YOUR ENTRIES ABOVE THIS ONE - DO NOT REMOVE\n");
590
591 PVE::Tools::file_set_contents("$targetdir/interfaces", $out);
592
026a6466
DM
593 # dump maclist
594 PVE::Tools::file_set_contents("$targetdir/maclist", $macs);
595
f789653a
DM
596 # dump policy
597
598 $format = "%-15s %-15s %-15s %s\n";
599 $out = sprintf($format, '#SOURCE', 'DEST', 'POLICY', 'LOG');
8cebfa6f
DM
600 $out .= sprintf($format, 'fw', 'all', 'ACCEPT', '');
601
602 # we need to disable intra-zone traffic on bridges. Else traffic
603 # from untracked interfaces simply pass the firewall
604 foreach my $z (sort keys %$zoneinfo) {
605 my $zid = $zoneinfo->{$z}->{zoneref};
606 if ($zoneinfo->{$z}->{type} eq 'bridge') {
607 $out .= sprintf($format, $zid, $zid, 'REJECT', 'info');
608 }
609 }
f789653a 610 $out .= sprintf($format, 'all', 'all', 'REJECT', 'info');
886aba9c 611
f789653a 612 PVE::Tools::file_set_contents("$targetdir/policy", $out);
886aba9c 613
dddd9413
DM
614 # dump rules
615 $out = '';
616
617 $out = sprintf($rule_format, '#ACTION', 'SOURCE', 'DEST', 'PROTO', 'DPORT', 'SPORT');
618 foreach my $vmid (sort keys %$rules) {
78db5872
DM
619 my $inrules = $rules->{$vmid}->{in};
620 my $outrules = $rules->{$vmid}->{out};
621
622 if (scalar(@$inrules)) {
623 $out .= "# IN to VM $vmid\n";
dddd9413
DM
624 foreach my $rule (@$inrules) {
625 foreach my $netid (keys %{$netinfo->{$vmid}}) {
626 my $net = $netinfo->{$vmid}->{$netid};
9a4644fa 627 next if $rule->{iface} && $rule->{iface} ne $netid;
dddd9413
DM
628 $out .= &$generate_input_rule($zoneinfo, $rule, $net, $netid);
629 }
630 }
631 }
632
78db5872
DM
633 if (scalar(@$outrules)) {
634 $out .= "# OUT from VM $vmid\n";
dddd9413
DM
635 foreach my $rule (@$outrules) {
636 foreach my $netid (keys %{$netinfo->{$vmid}}) {
637 my $net = $netinfo->{$vmid}->{$netid};
9a4644fa 638 next if $rule->{iface} && $rule->{iface} ne $netid;
dddd9413
DM
639 $out .= &$generate_output_rule($zoneinfo, $rule, $net, $netid);
640 }
641 }
642 }
643 }
644
645 PVE::Tools::file_set_contents("$targetdir/rules", $out);
5e1267a5
DM
646};
647
648
649sub parse_fw_rules {
650 my ($filename, $fh) = @_;
651
652 my $section;
653
654 my $res = { in => [], out => [] };
655
fcba0beb
DM
656 my $macros = get_shorewall_macros();
657 my $protocols = get_etc_protocols();
658
5e1267a5
DM
659 while (defined(my $line = <$fh>)) {
660 next if $line =~ m/^#/;
661 next if $line =~ m/^\s*$/;
662
663 if ($line =~ m/^\[(in|out)\]\s*$/i) {
664 $section = lc($1);
665 next;
666 }
667 next if !$section;
668
669 my ($action, $iface, $source, $dest, $proto, $dport, $sport) =
670 split(/\s+/, $line);
671
672 if (!$action) {
673 warn "skip incomplete line\n";
674 next;
675 }
676
677 my $service;
3a616aa0 678 if ($action =~ m/^(ACCEPT|DROP|REJECT|GROUP-(\S+))$/) {
5e1267a5
DM
679 # OK
680 } elsif ($action =~ m/^(\S+)\((ACCEPT|DROP|REJECT)\)$/) {
681 ($service, $action) = ($1, $2);
682 if (!$macros->{$service}) {
683 warn "unknown service '$service'\n";
684 next;
685 }
686 } else {
687 warn "unknown action '$action'\n";
688 next;
689 }
690
691 $iface = undef if $iface && $iface eq '-';
692 if ($iface && $iface !~ m/^(net0|net1|net2|net3|net4|net5)$/) {
693 warn "unknown interface '$iface'\n";
694 next;
695 }
696
697 $proto = undef if $proto && $proto eq '-';
fcba0beb
DM
698 if ($proto && !(defined($protocols->{byname}->{$proto}) ||
699 defined($protocols->{byid}->{$proto}))) {
5e1267a5
DM
700 warn "unknown protokol '$proto'\n";
701 next;
702 }
703
704 $source = undef if $source && $source eq '-';
5e1267a5 705 $dest = undef if $dest && $dest eq '-';
5e1267a5
DM
706
707 $dport = undef if $dport && $dport eq '-';
708 $sport = undef if $sport && $sport eq '-';
709
ecbea048
DM
710 eval {
711 parse_address_list($source) if $source;
712 parse_address_list($dest) if $dest;
fcba0beb
DM
713 parse_port_name_number_or_range($dport) if $dport;
714 parse_port_name_number_or_range($sport) if $sport;
ecbea048
DM
715 };
716 if (my $err = $@) {
717 warn $err;
718 next;
719
720 }
721
722
5e1267a5
DM
723 my $rule = {
724 action => $action,
725 service => $service,
726 iface => $iface,
727 source => $source,
728 dest => $dest,
729 proto => $proto,
730 dport => $dport,
731 sport => $sport,
732 };
733
734 push @{$res->{$section}}, $rule;
735 }
736
737 return $res;
738}
739
740sub read_local_vm_config {
741
742 my $openvz = {};
743
744 my $qemu = {};
745
746 my $list = PVE::QemuServer::config_list();
747
748 foreach my $vmid (keys %$list) {
b9b06789 749 #next if !($vmid eq '100' || $vmid eq '102');
5e1267a5
DM
750 my $cfspath = PVE::QemuServer::cfs_config_path($vmid);
751 if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
752 $qemu->{$vmid} = $conf;
753 }
754 }
755
756 my $vmdata = { openvz => $openvz, qemu => $qemu };
757
758 return $vmdata;
759};
760
761sub read_vm_firewall_rules {
762 my ($vmdata) = @_;
763 my $rules = {};
764 foreach my $vmid (keys %{$vmdata->{qemu}}, keys %{$vmdata->{openvz}}) {
765 my $filename = "/etc/pve/firewall/$vmid.fw";
766 my $fh = IO::File->new($filename, O_RDONLY);
767 next if !$fh;
768
769 $rules->{$vmid} = parse_fw_rules($filename, $fh);
770 }
771
772 return $rules;
773}
774
775sub compile {
776
777 my $vmdata = read_local_vm_config();
778 my $rules = read_vm_firewall_rules($vmdata);
779
780 # print Dumper($vmdata);
781
782 my $swdir = '/etc/shorewall';
783 mkdir $swdir;
784
785 &$compile_shorewall($swdir, $vmdata, $rules);
b6360c3f 786
5e1267a5 787 PVE::Tools::run_command(['shorewall', 'compile']);
b6360c3f
DM
788}
789
5e1267a5
DM
790sub compile_and_start {
791 my ($restart) = @_;
886aba9c 792
5e1267a5 793 compile();
b6360c3f 794
5e1267a5 795 PVE::Tools::run_command(['shorewall', $restart ? 'restart' : 'start']);
b6360c3f
DM
796}
797
798
7991;