start VM firewall API
[pve-firewall.git] / src / 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::INotify;
8 use PVE::Cluster;
9 use PVE::ProcFSTools;
10 use PVE::Tools;
11 use File::Basename;
12 use File::Path;
13 use IO::File;
14 use Net::IP;
15 use PVE::Tools qw(run_command lock_file);
16
17 # dynamically include PVE::QemuServer and PVE::OpenVZ 
18 # to avoid dependency problems
19 my $have_qemu_server;
20 eval {
21     require PVE::QemuServer;
22     $have_qemu_server = 1;
23 };
24
25 my $have_pve_manager;
26 eval {
27     require PVE::OpenVZ;
28     $have_pve_manager = 1;
29 };
30
31 use Data::Dumper;
32
33 my $nodename = PVE::INotify::nodename();
34
35 my $pve_fw_lock_filename = "/var/lock/pvefw.lck";
36 my $pve_fw_status_filename = "/var/lib/pve-firewall/pvefw.status";
37
38 my $default_log_level = 'info';
39
40 my $log_level_hash = {
41     debug => 7,
42     info => 6,
43     notice => 5,
44     warning => 4,
45     err => 3,
46     crit => 2,
47     alert => 1,
48     emerg => 0,
49 };
50
51 # imported/converted from: /usr/share/shorewall/macro.*
52 my $pve_fw_macros = {
53     'Amanda' => [
54         { action => 'PARAM', proto => 'udp', dport => '10080' },
55         { action => 'PARAM', proto => 'tcp', dport => '10080' },
56     ],
57     'Auth' => [
58         { action => 'PARAM', proto => 'tcp', dport => '113' },
59     ],
60     'BGP' => [
61         { action => 'PARAM', proto => 'tcp', dport => '179' },
62     ],
63     'BitTorrent' => [
64         { action => 'PARAM', proto => 'tcp', dport => '6881:6889' },
65         { action => 'PARAM', proto => 'udp', dport => '6881' },
66     ],
67     'BitTorrent32' => [
68         { action => 'PARAM', proto => 'tcp', dport => '6881:6999' },
69         { action => 'PARAM', proto => 'udp', dport => '6881' },
70     ],
71     'CVS' => [
72         { action => 'PARAM', proto => 'tcp', dport => '2401' },
73     ],
74     'Citrix' => [
75         { action => 'PARAM', proto => 'tcp', dport => '1494' },
76         { action => 'PARAM', proto => 'udp', dport => '1604' },
77         { action => 'PARAM', proto => 'tcp', dport => '2598' },
78     ],
79     'DAAP' => [
80         { action => 'PARAM', proto => 'tcp', dport => '3689' },
81         { action => 'PARAM', proto => 'udp', dport => '3689' },
82     ],
83     'DCC' => [
84         { action => 'PARAM', proto => 'tcp', dport => '6277' },
85     ],
86     'DHCPfwd' => [
87         { action => 'PARAM', proto => 'udp', dport => '67:68', sport => '67:68' },
88         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '67:68', sport => '67:68' },
89     ],
90     'DNS' => [
91         { action => 'PARAM', proto => 'udp', dport => '53' },
92         { action => 'PARAM', proto => 'tcp', dport => '53' },
93     ],
94     'Distcc' => [
95         { action => 'PARAM', proto => 'tcp', dport => '3632' },
96     ],
97     'Edonkey' => [
98         { action => 'PARAM', proto => 'tcp', dport => '4662' },
99         { action => 'PARAM', proto => 'udp', dport => '4665' },
100     ],
101     'FTP' => [
102         { action => 'PARAM', proto => 'tcp', dport => '21' },
103     ],
104     'Finger' => [
105         { action => 'PARAM', proto => 'tcp', dport => '79' },
106     ],
107     'GNUnet' => [
108         { action => 'PARAM', proto => 'tcp', dport => '2086' },
109         { action => 'PARAM', proto => 'udp', dport => '2086' },
110         { action => 'PARAM', proto => 'tcp', dport => '1080' },
111         { action => 'PARAM', proto => 'udp', dport => '1080' },
112     ],
113     'GRE' => [
114         { action => 'PARAM', proto => '47' },
115         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '47' },
116     ],
117     'Git' => [
118         { action => 'PARAM', proto => 'tcp', dport => '9418' },
119     ],
120     'Gnutella' => [
121         { action => 'PARAM', proto => 'tcp', dport => '6346' },
122         { action => 'PARAM', proto => 'udp', dport => '6346' },
123     ],
124     'HKP' => [
125         { action => 'PARAM', proto => 'tcp', dport => '11371' },
126     ],
127     'HTTP' => [
128         { action => 'PARAM', proto => 'tcp', dport => '80' },
129     ],
130     'HTTPS' => [
131         { action => 'PARAM', proto => 'tcp', dport => '443' },
132     ],
133     'ICPV2' => [
134         { action => 'PARAM', proto => 'udp', dport => '3130' },
135     ],
136     'ICQ' => [
137         { action => 'PARAM', proto => 'tcp', dport => '5190' },
138     ],
139     'IMAP' => [
140         { action => 'PARAM', proto => 'tcp', dport => '143' },
141     ],
142     'IMAPS' => [
143         { action => 'PARAM', proto => 'tcp', dport => '993' },
144     ],
145     'IPIP' => [
146         { action => 'PARAM', proto => '94' },
147         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '94' },
148     ],
149     'IPP' => [
150         { action => 'PARAM', proto => 'tcp', dport => '631' },
151     ],
152     'IPPbrd' => [
153         { action => 'PARAM', proto => 'udp', dport => '631' },
154     ],
155     'IPPserver' => [
156         { action => 'PARAM', source => 'SOURCE', dest => 'DEST', proto => 'tcp', dport => '631' },
157         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '631' },
158     ],
159     'IPsec' => [
160         { action => 'PARAM', proto => 'udp', dport => '500', sport => '500' },
161         { action => 'PARAM', proto => '50' },
162         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '500', sport => '500' },
163         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '50' },
164     ],
165     'IPsecah' => [
166         { action => 'PARAM', proto => 'udp', dport => '500', sport => '500' },
167         { action => 'PARAM', proto => '51' },
168         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '500', sport => '500' },
169         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '51' },
170     ],
171     'IPsecnat' => [
172         { action => 'PARAM', proto => 'udp', dport => '500' },
173         { action => 'PARAM', proto => 'udp', dport => '4500' },
174         { action => 'PARAM', proto => '50' },
175         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '500' },
176         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '4500' },
177         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '50' },
178     ],
179     'IRC' => [
180         { action => 'PARAM', proto => 'tcp', dport => '6667' },
181     ],
182     'JabberPlain' => [
183         { action => 'PARAM', proto => 'tcp', dport => '5222' },
184     ],
185     'JabberSecure' => [
186         { action => 'PARAM', proto => 'tcp', dport => '5223' },
187     ],
188     'Jabberd' => [
189         { action => 'PARAM', proto => 'tcp', dport => '5269' },
190     ],
191     'Jetdirect' => [
192         { action => 'PARAM', proto => 'tcp', dport => '9100' },
193     ],
194     'L2TP' => [
195         { action => 'PARAM', proto => 'udp', dport => '1701' },
196         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '1701' },
197     ],
198     'LDAP' => [
199         { action => 'PARAM', proto => 'tcp', dport => '389' },
200     ],
201     'LDAPS' => [
202         { action => 'PARAM', proto => 'tcp', dport => '636' },
203     ],
204     'MSNP' => [
205         { action => 'PARAM', proto => 'tcp', dport => '1863' },
206     ],
207     'MSSQL' => [
208         { action => 'PARAM', proto => 'tcp', dport => '1433' },
209     ],
210     'Mail' => [
211         { action => 'PARAM', proto => 'tcp', dport => '25' },
212         { action => 'PARAM', proto => 'tcp', dport => '465' },
213         { action => 'PARAM', proto => 'tcp', dport => '587' },
214     ],
215     'Munin' => [
216         { action => 'PARAM', proto => 'tcp', dport => '4949' },
217     ],
218     'MySQL' => [
219         { action => 'PARAM', proto => 'tcp', dport => '3306' },
220     ],
221     'NNTP' => [
222         { action => 'PARAM', proto => 'tcp', dport => '119' },
223     ],
224     'NNTPS' => [
225         { action => 'PARAM', proto => 'tcp', dport => '563' },
226     ],
227     'NTP' => [
228         { action => 'PARAM', proto => 'udp', dport => '123' },
229     ],
230     'NTPbi' => [
231         { action => 'PARAM', proto => 'udp', dport => '123' },
232         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '123' },
233     ],
234     'NTPbrd' => [
235         { action => 'PARAM', proto => 'udp', dport => '123' },
236         { action => 'PARAM', proto => 'udp', dport => '1024:65535', sport => '123' },
237     ],
238     'OSPF' => [
239         { action => 'PARAM', proto => '89' },
240     ],
241     'OpenVPN' => [
242         { action => 'PARAM', proto => 'udp', dport => '1194' },
243     ],
244     'PCA' => [
245         { action => 'PARAM', proto => 'udp', dport => '5632' },
246         { action => 'PARAM', proto => 'tcp', dport => '5631' },
247     ],
248     'POP3' => [
249         { action => 'PARAM', proto => 'tcp', dport => '110' },
250     ],
251     'POP3S' => [
252         { action => 'PARAM', proto => 'tcp', dport => '995' },
253     ],
254     'PPtP' => [
255         { action => 'PARAM', proto => '47' },
256         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '47' },
257         { action => 'PARAM', proto => 'tcp', dport => '1723' },
258     ],
259     'Ping' => [
260         { action => 'PARAM', proto => 'icmp', dport => 'echo-request' },
261     ],
262     'PostgreSQL' => [
263         { action => 'PARAM', proto => 'tcp', dport => '5432' },
264     ],
265     'Printer' => [
266         { action => 'PARAM', proto => 'tcp', dport => '515' },
267     ],
268     'RDP' => [
269         { action => 'PARAM', proto => 'tcp', dport => '3389' },
270     ],
271     'RIPbi' => [
272         { action => 'PARAM', proto => 'udp', dport => '520' },
273         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '520' },
274     ],
275     'RNDC' => [
276         { action => 'PARAM', proto => 'tcp', dport => '953' },
277     ],
278     'Razor' => [
279         { action => 'ACCEPT', proto => 'tcp', dport => '2703' },
280     ],
281     'Rdate' => [
282         { action => 'PARAM', proto => 'tcp', dport => '37' },
283     ],
284     'Rsync' => [
285         { action => 'PARAM', proto => 'tcp', dport => '873' },
286     ],
287     'SANE' => [
288         { action => 'PARAM', proto => 'tcp', dport => '6566' },
289     ],
290     'SMB' => [
291         { action => 'PARAM', proto => 'udp', dport => '135,445' },
292         { action => 'PARAM', proto => 'udp', dport => '137:139' },
293         { action => 'PARAM', proto => 'udp', dport => '1024:65535', sport => '137' },
294         { action => 'PARAM', proto => 'tcp', dport => '135,139,445' },
295     ],
296     'SMBBI' => [
297         { action => 'PARAM', proto => 'udp', dport => '135,445' },
298         { action => 'PARAM', proto => 'udp', dport => '137:139' },
299         { action => 'PARAM', proto => 'udp', dport => '1024:65535', sport => '137' },
300         { action => 'PARAM', proto => 'tcp', dport => '135,139,445' },
301         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '135,445' },
302         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '137:139' },
303         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '1024:65535', sport => '137' },
304         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'tcp', dport => '135,139,445' },
305     ],
306     'SMBswat' => [
307         { action => 'PARAM', proto => 'tcp', dport => '901' },
308     ],
309     'SMTP' => [
310         { action => 'PARAM', proto => 'tcp', dport => '25' },
311     ],
312     'SMTPS' => [
313         { action => 'PARAM', proto => 'tcp', dport => '465' },
314     ],
315     'SNMP' => [
316         { action => 'PARAM', proto => 'udp', dport => '161:162' },
317         { action => 'PARAM', proto => 'tcp', dport => '161' },
318     ],
319     'SPAMD' => [
320         { action => 'PARAM', proto => 'tcp', dport => '783' },
321     ],
322     'SSH' => [
323         { action => 'PARAM', proto => 'tcp', dport => '22' },
324     ],
325     'SVN' => [
326         { action => 'PARAM', proto => 'tcp', dport => '3690' },
327     ],
328     'SixXS' => [
329         { action => 'PARAM', proto => 'tcp', dport => '3874' },
330         { action => 'PARAM', proto => 'udp', dport => '3740' },
331         { action => 'PARAM', proto => '41' },
332         { action => 'PARAM', proto => 'udp', dport => '5072,8374' },
333     ],
334     'Squid' => [
335         { action => 'PARAM', proto => 'tcp', dport => '3128' },
336     ],
337     'Submission' => [
338         { action => 'PARAM', proto => 'tcp', dport => '587' },
339     ],
340     'Syslog' => [
341         { action => 'PARAM', proto => 'udp', dport => '514' },
342         { action => 'PARAM', proto => 'tcp', dport => '514' },
343     ],
344     'TFTP' => [
345         { action => 'PARAM', proto => 'udp', dport => '69' },
346     ],
347     'Telnet' => [
348         { action => 'PARAM', proto => 'tcp', dport => '23' },
349     ],
350     'Telnets' => [
351         { action => 'PARAM', proto => 'tcp', dport => '992' },
352     ],
353     'Time' => [
354         { action => 'PARAM', proto => 'tcp', dport => '37' },
355     ],
356     'Trcrt' => [
357         { action => 'PARAM', proto => 'udp', dport => '33434:33524' },
358         { action => 'PARAM', proto => 'icmp', dport => 'echo-request' },
359     ],
360     'VNC' => [
361         { action => 'PARAM', proto => 'tcp', dport => '5900:5909' },
362     ],
363     'VNCL' => [
364         { action => 'PARAM', proto => 'tcp', dport => '5500' },
365     ],
366     'Web' => [
367         { action => 'PARAM', proto => 'tcp', dport => '80' },
368         { action => 'PARAM', proto => 'tcp', dport => '443' },
369     ],
370     'Webcache' => [
371         { action => 'PARAM', proto => 'tcp', dport => '8080' },
372     ],
373     'Webmin' => [
374         { action => 'PARAM', proto => 'tcp', dport => '10000' },
375     ],
376     'Whois' => [
377         { action => 'PARAM', proto => 'tcp', dport => '43' },
378     ],
379 };
380
381 my $pve_fw_parsed_macros;
382 my $pve_fw_preferred_macro_names = {};
383
384 my $pve_std_chains = {
385     'PVEFW-SET-ACCEPT-MARK' => [
386         "-j MARK --set-mark 1",
387     ],
388     'PVEFW-DropBroadcast' => [
389         # same as shorewall 'Broadcast'
390         # simply DROP BROADCAST/MULTICAST/ANYCAST
391         # we can use this to reduce logging
392         { action => 'DROP', dsttype => 'BROADCAST' },
393         { action => 'DROP', dsttype => 'MULTICAST' },
394         { action => 'DROP', dsttype => 'ANYCAST' },
395         { action => 'DROP', dest => '224.0.0.0/4' }, 
396     ],
397     'PVEFW-reject' => [
398         # same as shorewall 'reject'
399         { action => 'DROP', dsttype => 'BROADCAST' },
400         { action => 'DROP', source => '224.0.0.0/4' }, 
401         { action => 'DROP', proto => 'icmp' },
402         "-p tcp -j REJECT --reject-with tcp-reset",
403         "-p udp -j REJECT --reject-with icmp-port-unreachable",
404         "-p icmp -j REJECT --reject-with icmp-host-unreachable",
405         "-j REJECT --reject-with icmp-host-prohibited",
406     ],
407     'PVEFW-Drop' => [
408         # same as shorewall 'Drop', which is equal to DROP, 
409         # but REJECT/DROP some packages to reduce logging,
410         # and ACCEPT critical ICMP types
411         { action => 'PVEFW-reject',  proto => 'tcp', dport => '43' }, # REJECT 'auth'
412         # we are not interested in BROADCAST/MULTICAST/ANYCAST
413         { action => 'PVEFW-DropBroadcast' },
414         # ACCEPT critical ICMP types
415         { action => 'ACCEPT', proto => 'icmp', dport => 'fragmentation-needed' },
416         { action => 'ACCEPT', proto => 'icmp', dport => 'time-exceeded' },
417         # Drop packets with INVALID state
418         "-m conntrack --ctstate INVALID -j DROP",
419         # Drop Microsoft SMB noise
420         { action => 'DROP', proto => 'udp', dport => '135,445', nbdport => 2 },
421         { action => 'DROP', proto => 'udp', dport => '137:139'},
422         { action => 'DROP', proto => 'udp', dport => '1024:65535', sport => 137 },
423         { action => 'DROP', proto => 'tcp', dport => '135,139,445', nbdport => 3 },
424         { action => 'DROP', proto => 'udp', dport => 1900 }, # UPnP
425         # Drop new/NotSyn traffic so that it doesn't get logged
426         "-p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -j DROP",
427         # Drop DNS replies
428         { action => 'DROP', proto => 'udp', sport => 53 },
429     ],
430     'PVEFW-Reject' => [
431         # same as shorewall 'Reject', which is equal to Reject, 
432         # but REJECT/DROP some packages to reduce logging,
433         # and ACCEPT critical ICMP types
434         { action => 'PVEFW-reject',  proto => 'tcp', dport => '43' }, # REJECT 'auth'
435         # we are not interested in BROADCAST/MULTICAST/ANYCAST
436         { action => 'PVEFW-DropBroadcast' },
437         # ACCEPT critical ICMP types
438         { action => 'ACCEPT', proto => 'icmp', dport => 'fragmentation-needed' },
439         { action => 'ACCEPT', proto => 'icmp', dport => 'time-exceeded' },
440         # Drop packets with INVALID state
441         "-m conntrack --ctstate INVALID -j DROP",
442         # Drop Microsoft SMB noise
443         { action => 'PVEFW-reject', proto => 'udp', dport => '135,445', nbdport => 2 },
444         { action => 'PVEFW-reject', proto => 'udp', dport => '137:139'},
445         { action => 'PVEFW-reject', proto => 'udp', dport => '1024:65535', sport => 137 },
446         { action => 'PVEFW-reject', proto => 'tcp', dport => '135,139,445', nbdport => 3 },
447         { action => 'DROP', proto => 'udp', dport => 1900 }, # UPnP
448         # Drop new/NotSyn traffic so that it doesn't get logged
449         "-p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -j DROP",
450         # Drop DNS replies
451         { action => 'DROP', proto => 'udp', sport => 53 },
452     ],
453     'PVEFW-tcpflags' => [
454         # same as shorewall tcpflags action.
455         # Packets arriving on this interface are checked for som illegal combinations of TCP flags
456         "-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG FIN,PSH,URG -g PVEFW-logflags",
457         "-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE -g PVEFW-logflags",
458         "-p tcp -m tcp --tcp-flags SYN,RST SYN,RST -g PVEFW-logflags",
459         "-p tcp -m tcp --tcp-flags FIN,SYN FIN,SYN -g PVEFW-logflags",
460         "-p tcp -m tcp --sport 0 --tcp-flags FIN,SYN,RST,ACK SYN -g PVEFW-logflags",
461     ],
462     'PVEFW-smurfs' => [
463         # same as shorewall smurfs action
464         # Filter packets for smurfs (packets with a broadcast address as the source).
465         "-s 0.0.0.0/32 -j RETURN",
466         "-m addrtype --src-type BROADCAST -g PVEFW-smurflog",
467         "-s 224.0.0.0/4 -g PVEFW-smurflog",
468     ],
469 };
470
471 # iptables -p icmp -h
472 my $icmp_type_names = {
473     any => 1,
474     'echo-reply' => 1,
475     'destination-unreachable' => 1,
476     'network-unreachable' => 1,
477     'host-unreachable' => 1,
478     'protocol-unreachable' => 1,
479     'port-unreachable' => 1,
480     'fragmentation-needed' => 1,
481     'source-route-failed' => 1,
482     'network-unknown' => 1,
483     'host-unknown' => 1,
484     'network-prohibited' => 1,
485     'host-prohibited' => 1,
486     'TOS-network-unreachable' => 1,
487     'TOS-host-unreachable' => 1,
488     'communication-prohibited' => 1,
489     'host-precedence-violation' => 1,
490     'precedence-cutoff' => 1,
491     'source-quench' => 1,
492     'redirect' => 1,
493     'network-redirect' => 1,
494     'host-redirect' => 1,
495     'TOS-network-redirect' => 1,
496     'TOS-host-redirect' => 1,
497     'echo-request' => 1,
498     'router-advertisement' => 1,
499     'router-solicitation' => 1,
500     'time-exceeded' => 1,
501     'ttl-zero-during-transit' => 1,
502     'ttl-zero-during-reassembly' => 1,
503     'parameter-problem' => 1,
504     'ip-header-bad' => 1,
505     'required-option-missing' => 1,
506     'timestamp-request' => 1,
507     'timestamp-reply' => 1,
508     'address-mask-request' => 1,
509     'address-mask-reply' => 1,
510 };
511
512 sub get_firewall_macros {
513
514     return $pve_fw_parsed_macros if $pve_fw_parsed_macros;
515
516     $pve_fw_parsed_macros = {};
517
518     foreach my $k (keys %$pve_fw_macros) {
519         my $name = lc($k);
520
521         my $macro =  $pve_fw_macros->{$k};
522         $pve_fw_preferred_macro_names->{$name} = $k;
523         $pve_fw_parsed_macros->{$name} = $macro;
524     }
525
526     return $pve_fw_parsed_macros;
527 }
528
529 my $etc_services;
530
531 sub get_etc_services {
532
533     return $etc_services if $etc_services;
534
535     my $filename = "/etc/services";
536
537     my $fh = IO::File->new($filename, O_RDONLY);
538     if (!$fh) {
539         warn "unable to read '$filename' - $!\n";
540         return {};
541     }
542
543     my $services = {};
544
545     while (my $line = <$fh>) {
546         chomp ($line);
547         next if $line =~m/^#/;
548         next if ($line =~m/^\s*$/);
549
550         if ($line =~ m!^(\S+)\s+(\S+)/(tcp|udp).*$!) {
551             $services->{byid}->{$2}->{name} = $1;
552             $services->{byid}->{$2}->{port} = $2;
553             $services->{byid}->{$2}->{$3} = 1;
554             $services->{byname}->{$1} = $services->{byid}->{$2};
555         }
556     }
557
558     close($fh);
559
560     $etc_services = $services;
561
562
563     return $etc_services;
564 }
565
566 my $etc_protocols;
567
568 sub get_etc_protocols {
569     return $etc_protocols if $etc_protocols;
570
571     my $filename = "/etc/protocols";
572
573     my $fh = IO::File->new($filename, O_RDONLY);
574     if (!$fh) {
575         warn "unable to read '$filename' - $!\n";
576         return {};
577     }
578
579     my $protocols = {};
580
581     while (my $line = <$fh>) {
582         chomp ($line);
583         next if $line =~m/^#/;
584         next if ($line =~m/^\s*$/);
585
586         if ($line =~ m!^(\S+)\s+(\d+)\s+.*$!) {
587             $protocols->{byid}->{$2}->{name} = $1;
588             $protocols->{byname}->{$1} = $protocols->{byid}->{$2};
589         }
590     }
591
592     close($fh);
593
594     $etc_protocols = $protocols;
595
596     return $etc_protocols;
597 }
598
599 sub parse_address_list {
600     my ($str) = @_;
601
602     my $nbaor = 0;
603     foreach my $aor (split(/,/, $str)) {
604         if (!Net::IP->new($aor)) {
605             my $err = Net::IP::Error();
606             die "invalid IP address: $err\n";
607         }else{
608             $nbaor++;
609         }
610     }
611     return $nbaor;
612 }
613
614 sub parse_port_name_number_or_range {
615     my ($str) = @_;
616
617     my $services = PVE::Firewall::get_etc_services();
618     my $nbports = 0;
619     foreach my $item (split(/,/, $str)) {
620         my $portlist = "";
621         my $oldpon = undef;
622         $nbports++;
623         foreach my $pon (split(':', $item, 2)) {
624             $pon = $services->{byname}->{$pon}->{port} if $services->{byname}->{$pon}->{port};
625             if ($pon =~ m/^\d+$/){
626                 die "invalid port '$pon'\n" if $pon < 0 && $pon > 65535;
627                 die "port '$pon' must be bigger than port '$oldpon' \n" if $oldpon && ($pon < $oldpon);
628                 $oldpon = $pon;
629             }else{
630                 die "invalid port $services->{byname}->{$pon}\n" if !$services->{byname}->{$pon};
631             }
632         }
633     }
634
635     return ($nbports);
636 }
637
638 # helper function for API
639 sub cleanup_fw_rule {
640     my ($rule, $digest, $pos) = @_;
641
642     my $r = {};
643
644     foreach my $k (keys %$rule) {
645         next if $k eq 'nbdport';
646         next if $k eq 'nbsport';
647         my $v = $rule->{$k};
648         next if !defined($v);
649         $r->{$k} = $v;
650         $r->{digest} = $digest;
651         $r->{pos} = $pos;
652     }
653
654     return $r;
655 }
656
657 my $bridge_firewall_enabled = 0;
658
659 sub enable_bridge_firewall {
660
661     return if $bridge_firewall_enabled; # only once
662
663     PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-iptables", "1");
664     PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-ip6tables", "1");
665
666     # make sure syncookies are enabled (which is default on newer 3.X kernels anyways)
667     PVE::ProcFSTools::write_proc_entry("/proc/sys/net/ipv4/tcp_syncookies", "1");
668
669     $bridge_firewall_enabled = 1;
670 }
671
672 my $rule_format = "%-15s %-30s %-30s %-15s %-15s %-15s\n";
673
674 sub iptables {
675     my ($cmd) = @_;
676
677     run_command("/sbin/iptables $cmd", outfunc => sub {}, errfunc => sub {});
678 }
679
680 sub iptables_restore_cmdlist {
681     my ($cmdlist) = @_;
682
683     run_command("/sbin/iptables-restore -n", input => $cmdlist);
684 }
685
686 sub iptables_get_chains {
687
688     my $res = {};
689
690     # check what chains we want to track
691     my $is_pvefw_chain = sub {
692         my $name = shift;
693
694         return 1 if $name =~ m/^PVEFW-\S+$/;
695
696         return 1 if $name =~ m/^tap\d+i\d+-(:?IN|OUT)$/;
697
698         return 1 if $name =~ m/^veth\d+.\d+-(:?IN|OUT)$/; # fixme: dev name is configurable
699
700         return 1 if $name =~ m/^venet0-\d+-(:?IN|OUT)$/;
701
702         return 1 if $name =~ m/^vmbr\d+-(:?FW|IN|OUT)$/;
703         return 1 if $name =~ m/^GROUP-(:?[^\s\-]+)-(:?IN|OUT)$/;
704
705         return undef;
706     };
707
708     my $table = '';
709
710     my $parser = sub {
711         my $line = shift;
712
713         return if $line =~ m/^#/;
714         return if $line =~ m/^\s*$/;
715
716         if ($line =~ m/^\*(\S+)$/) {
717             $table = $1;
718             return;
719         }
720
721         return if $table ne 'filter';
722
723         if ($line =~ m/^:(\S+)\s/) {
724             my $chain = $1;
725             return if !&$is_pvefw_chain($chain);
726             $res->{$chain} = "unknown";
727         } elsif ($line =~ m/^-A\s+(\S+)\s.*--comment\s+\"PVESIG:(\S+)\"/) {
728             my ($chain, $sig) = ($1, $2);
729             return if !&$is_pvefw_chain($chain);
730             $res->{$chain} = $sig;
731         } else {
732             # simply ignore the rest
733             return;
734         }
735     };
736
737     run_command("/sbin/iptables-save", outfunc => $parser);
738
739     return $res;
740 }
741
742 sub iptables_chain_exist {
743     my ($chain) = @_;
744
745     eval{
746         iptables("-n --list $chain");
747     };
748     return undef if $@;
749
750     return 1;
751 }
752
753 sub iptables_rule_exist {
754     my ($rule) = @_;
755
756     eval{
757         iptables("-C $rule");
758     };
759     return undef if $@;
760
761     return 1;
762 }
763
764 sub ruleset_generate_cmdstr {
765     my ($ruleset, $chain, $rule, $actions, $goto) = @_;
766
767     return if $rule->{disable};
768
769     my @cmd = ();
770
771     push @cmd, "-i $rule->{iface_in}" if $rule->{iface_in};
772     push @cmd, "-o $rule->{iface_out}" if $rule->{iface_out};
773
774     push @cmd, "-m iprange --src-range" if $rule->{nbsource} && $rule->{nbsource} > 1;
775     push @cmd, "-s $rule->{source}" if $rule->{source};
776     push @cmd, "-m iprange --dst-range" if $rule->{nbdest} && $rule->{nbdest} > 1;
777     push @cmd, "-d $rule->{dest}" if $rule->{dest};
778
779     if ($rule->{proto}) {
780         push @cmd, "-p $rule->{proto}";
781
782         my $multiport = 0;
783         $multiport++ if $rule->{nbdport} && ($rule->{nbdport} > 1);
784         $multiport++ if $rule->{nbsport} && ($rule->{nbsport} > 1);
785
786         push @cmd, "--match multiport" if $multiport;
787
788         die "multiport: option '--sports' cannot be used together with '--dports'\n" 
789             if ($multiport == 2) && ($rule->{dport} ne $rule->{sport});
790
791         if ($rule->{dport}) {
792             if ($rule->{proto} && $rule->{proto} eq 'icmp') {
793                 # Note: we use dport to store --icmp-type
794                 die "unknown icmp-type '$rule->{dport}'\n" if !defined($icmp_type_names->{$rule->{dport}});
795                 push @cmd, "-m icmp --icmp-type $rule->{dport}";
796             } else {
797                 if ($rule->{nbdport} && $rule->{nbdport} > 1) {
798                     if ($multiport == 2) {
799                         push @cmd,  "--ports $rule->{dport}";
800                     } else {
801                         push @cmd, "--dports $rule->{dport}";
802                     }
803                 } else {
804                     push @cmd, "--dport $rule->{dport}";
805                 }
806             }
807         }
808
809         if ($rule->{sport}) {
810             if ($rule->{nbsport} && $rule->{nbsport} > 1) {
811                 push @cmd, "--sports $rule->{sport}" if $multiport != 2;
812             } else {
813                 push @cmd, "--sport $rule->{sport}";
814             }
815         }
816     } elsif ($rule->{dport} || $rule->{sport}) {
817         warn "ignoring destination port '$rule->{dport}' - no protocol specified\n" if $rule->{dport};
818         warn "ignoring source port '$rule->{sport}' - no protocol specified\n" if $rule->{sport};
819     }
820
821     push @cmd, "-m addrtype --dst-type $rule->{dsttype}" if $rule->{dsttype};
822
823     if (my $action = $rule->{action}) {
824         $action = $actions->{$action} if defined($actions->{$action}); 
825         $goto = 1 if !defined($goto) && $action eq 'PVEFW-SET-ACCEPT-MARK';
826         push @cmd, $goto ? "-g $action" : "-j $action";
827     }
828
829     return scalar(@cmd) ? join(' ', @cmd) : undef;
830 }
831
832 sub ruleset_generate_rule {
833     my ($ruleset, $chain, $rule, $actions, $goto) = @_;
834
835     if (my $cmdstr = ruleset_generate_cmdstr($ruleset, $chain, $rule, $actions, $goto)) {
836         ruleset_addrule($ruleset, $chain, $cmdstr);
837     }
838 }
839 sub ruleset_generate_rule_insert {
840     my ($ruleset, $chain, $rule, $actions, $goto) = @_;
841
842     if (my $cmdstr = ruleset_generate_cmdstr($ruleset, $chain, $rule, $actions, $goto)) {
843         ruleset_insertrule($ruleset, $chain, $cmdstr);
844     }
845 }
846
847 sub ruleset_create_chain {
848     my ($ruleset, $chain) = @_;
849
850     die "Invalid chain name '$chain' (28 char max)\n" if length($chain) > 28;
851     die "chain name may not contain collons\n" if $chain =~ m/:/; # because of log format
852
853     die "chain '$chain' already exists\n" if $ruleset->{$chain};
854
855     $ruleset->{$chain} = [];
856 }
857
858 sub ruleset_chain_exist {
859     my ($ruleset, $chain) = @_;
860
861     return $ruleset->{$chain} ? 1 : undef;
862 }
863
864 sub ruleset_addrule {
865    my ($ruleset, $chain, $rule) = @_;
866
867    die "no such chain '$chain'\n" if !$ruleset->{$chain};
868
869    push @{$ruleset->{$chain}}, "-A $chain $rule";
870 }
871
872 sub ruleset_insertrule {
873    my ($ruleset, $chain, $rule) = @_;
874
875    die "no such chain '$chain'\n" if !$ruleset->{$chain};
876
877    unshift @{$ruleset->{$chain}}, "-A $chain $rule";
878 }
879
880 sub get_log_rule_base {
881     my ($chain, $vmid, $msg, $loglevel) = @_;
882     
883     die "internal error - no log level" if !defined($loglevel);
884
885     $vmid = 0 if !defined($vmid);
886
887     # Note: we use special format for prefix to pass further 
888     # info to log daemon (VMID, LOGVELEL and CHAIN)
889
890     return "-j NFLOG --nflog-prefix \":$vmid:$loglevel:$chain: $msg\"";
891 }
892
893 sub ruleset_addlog {
894     my ($ruleset, $chain, $vmid, $msg, $loglevel, $rule) = @_;
895
896     return if !defined($loglevel);
897
898     my $logrule = get_log_rule_base($chain, $vmid, $msg, $loglevel);
899
900     $logrule = "$rule $logrule" if defined($rule);
901
902     ruleset_addrule($ruleset, $chain, $logrule)
903 }
904
905 sub generate_bridge_chains {
906     my ($ruleset, $hostfw_conf, $bridge, $routing_table) = @_;
907
908     my $options = $hostfw_conf->{options} || {};
909
910     die "error: detected direct route to bridge '$bridge'\n"
911         if !$options->{allow_bridge_route} && $routing_table->{$bridge};
912
913     if (!ruleset_chain_exist($ruleset, "$bridge-FW")) {
914         ruleset_create_chain($ruleset, "$bridge-FW");
915         ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o $bridge -m physdev --physdev-is-out -j $bridge-FW");
916         ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i $bridge -m physdev --physdev-is-in -j $bridge-FW");
917     }
918
919     if (!ruleset_chain_exist($ruleset, "$bridge-OUT")) {
920         ruleset_create_chain($ruleset, "$bridge-OUT");
921         ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-in -j $bridge-OUT");
922         ruleset_insertrule($ruleset, "PVEFW-INPUT", "-i $bridge -m physdev --physdev-is-in -j $bridge-OUT");
923     }
924
925     if (!ruleset_chain_exist($ruleset, "$bridge-IN")) {
926         ruleset_create_chain($ruleset, "$bridge-IN");
927         ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-out -j $bridge-IN");
928         ruleset_addrule($ruleset, "$bridge-FW", "-m mark --mark 1 -j ACCEPT");
929         # accept traffic to unmanaged bridge ports
930         ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-out -j ACCEPT ");
931     }
932 }
933
934 sub ruleset_add_chain_policy {
935     my ($ruleset, $chain, $vmid, $policy, $loglevel, $accept_action) = @_;
936
937     if ($policy eq 'ACCEPT') {
938
939         ruleset_generate_rule($ruleset, $chain, { action => 'ACCEPT' },
940                               { ACCEPT =>  $accept_action});
941
942     } elsif ($policy eq 'DROP') {
943
944         ruleset_addrule($ruleset, $chain, "-j PVEFW-Drop");
945
946         ruleset_addlog($ruleset, $chain, $vmid, "policy $policy: ", $loglevel);
947
948         ruleset_addrule($ruleset, $chain, "-j DROP");
949     } elsif ($policy eq 'REJECT') {
950         ruleset_addrule($ruleset, $chain, "-j PVEFW-Reject");
951
952         ruleset_addlog($ruleset, $chain, $vmid, "policy $policy: ", $loglevel);
953
954         ruleset_addrule($ruleset, $chain, "-g PVEFW-reject");
955     } else {
956         # should not happen
957         die "internal error: unknown policy '$policy'";
958     }
959 }
960
961 sub ruleset_create_vm_chain {
962     my ($ruleset, $chain, $options, $macaddr, $direction) = @_;
963
964     ruleset_create_chain($ruleset, $chain);
965
966     if (!(defined($options->{nosmurfs}) && $options->{nosmurfs} == 0)) {
967         ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID,NEW -j PVEFW-smurfs");
968     }
969
970     if (!(defined($options->{dhcp}) && $options->{dhcp} == 0)) {
971         ruleset_addrule($ruleset, $chain, "-p udp -m udp --dport 67:68 -j ACCEPT");
972     }
973
974     if ($options->{tcpflags}) {
975         ruleset_addrule($ruleset, $chain, "-p tcp -j PVEFW-tcpflags");
976     }
977
978     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID -j DROP");
979     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
980
981     if ($direction eq 'OUT') {
982         if (defined($macaddr) && !(defined($options->{macfilter}) && $options->{macfilter} == 0)) {
983             ruleset_addrule($ruleset, $chain, "-m mac ! --mac-source $macaddr -j DROP");
984         }
985         ruleset_addrule($ruleset, $chain, "-j MARK --set-mark 0"); # clear mark
986     }
987 }
988
989 sub ruleset_generate_vm_rules {
990     my ($ruleset, $rules, $groups_conf, $chain, $netid, $direction) = @_;
991
992     my $lc_direction = lc($direction);
993
994     foreach my $rule (@$rules) {
995         next if $rule->{iface} && $rule->{iface} ne $netid;
996         next if $rule->{disable};
997         if ($rule->{type} eq 'group') {
998             my $group_chain = "GROUP-$rule->{action}-$direction"; 
999             if(!ruleset_chain_exist($ruleset, $group_chain)){
1000                 generate_group_rules($ruleset, $groups_conf, $rule->{action});
1001             }
1002             ruleset_addrule($ruleset, $chain, "-j $group_chain");
1003             ruleset_addrule($ruleset, $chain, "-m mark --mark 1 -j RETURN")
1004                 if $direction eq 'OUT';
1005         } else {
1006             next if $rule->{type} ne $lc_direction;
1007             if ($direction eq 'OUT') {
1008                 ruleset_generate_rule($ruleset, $chain, $rule, 
1009                                       { ACCEPT => "PVEFW-SET-ACCEPT-MARK", REJECT => "PVEFW-reject" });
1010             } else {
1011                 ruleset_generate_rule($ruleset, $chain, $rule, { REJECT => "PVEFW-reject" });
1012             }
1013         }
1014     }
1015 }
1016
1017 sub generate_venet_rules_direction {
1018     my ($ruleset, $groups_conf, $vmfw_conf, $vmid, $ip, $direction) = @_;
1019
1020     parse_address_list($ip); # make sure we have a valid $ip list
1021
1022     my $lc_direction = lc($direction);
1023
1024     my $rules = $vmfw_conf->{rules};
1025
1026     my $options = $vmfw_conf->{options};
1027     my $loglevel = get_option_log_level($options, "log_level_${lc_direction}");
1028
1029     my $chain = "venet0-$vmid-$direction";
1030
1031     ruleset_create_vm_chain($ruleset, $chain, $options, undef, $direction);
1032
1033     ruleset_generate_vm_rules($ruleset, $rules, $groups_conf, $chain, 'venet', $direction);
1034
1035     # implement policy
1036     my $policy;
1037
1038     if ($direction eq 'OUT') {
1039         $policy = $options->{policy_out} || 'ACCEPT'; # allow everything by default
1040     } else {
1041         $policy = $options->{policy_in} || 'DROP'; # allow nothing by default
1042     }
1043
1044     my $accept_action = $direction eq 'OUT' ? "PVEFW-SET-ACCEPT-MARK" : "ACCEPT";
1045     ruleset_add_chain_policy($ruleset, $chain, $vmid, $policy, $loglevel, $accept_action);
1046
1047     # plug into FORWARD, INPUT and OUTPUT chain
1048     if ($direction eq 'OUT') {
1049         ruleset_generate_rule_insert($ruleset, "PVEFW-FORWARD", {
1050             action => $chain,
1051             source => $ip,
1052             iface_in => 'venet0'});
1053
1054         ruleset_generate_rule_insert($ruleset, "PVEFW-INPUT", {
1055             action => $chain,
1056             source => $ip,
1057             iface_in => 'venet0'});
1058     } else {
1059         ruleset_generate_rule($ruleset, "PVEFW-FORWARD", {
1060             action => $chain,
1061             dest => $ip,
1062             iface_out => 'venet0'});
1063
1064         ruleset_generate_rule($ruleset, "PVEFW-OUTPUT", {
1065             action => $chain,
1066             dest => $ip,
1067             iface_out => 'venet0'});
1068     }
1069 }
1070
1071 sub generate_tap_rules_direction {
1072     my ($ruleset, $groups_conf, $iface, $netid, $macaddr, $vmfw_conf, $vmid, $bridge, $direction) = @_;
1073
1074     my $lc_direction = lc($direction);
1075
1076     my $rules = $vmfw_conf->{rules};
1077
1078     my $options = $vmfw_conf->{options};
1079     my $loglevel = get_option_log_level($options, "log_level_${lc_direction}");
1080
1081     my $tapchain = "$iface-$direction";
1082
1083     ruleset_create_vm_chain($ruleset, $tapchain, $options, $macaddr, $direction);
1084
1085     ruleset_generate_vm_rules($ruleset, $rules, $groups_conf, $tapchain, $netid, $direction);
1086
1087     # implement policy
1088     my $policy;
1089
1090     if ($direction eq 'OUT') {
1091         $policy = $options->{policy_out} || 'ACCEPT'; # allow everything by default
1092     } else {
1093         $policy = $options->{policy_in} || 'DROP'; # allow nothing by default
1094     }
1095
1096     my $accept_action = $direction eq 'OUT' ? "PVEFW-SET-ACCEPT-MARK" : "ACCEPT";
1097     ruleset_add_chain_policy($ruleset, $tapchain, $vmid, $policy, $loglevel, $accept_action);
1098
1099     # plug the tap chain to bridge chain
1100     if ($direction eq 'IN') {
1101         ruleset_insertrule($ruleset, "$bridge-IN",
1102                            "-m physdev --physdev-is-bridged --physdev-out $iface -j $tapchain");
1103     } else {
1104         ruleset_insertrule($ruleset, "$bridge-OUT",
1105                            "-m physdev --physdev-in $iface -j $tapchain");
1106     }
1107 }
1108
1109 sub enable_host_firewall {
1110     my ($ruleset, $hostfw_conf, $groups_conf) = @_;
1111
1112     # fixme: allow security groups
1113
1114     my $options = $hostfw_conf->{options};
1115     my $rules = $hostfw_conf->{rules};
1116
1117     # host inbound firewall
1118     my $chain = "PVEFW-HOST-IN";
1119     ruleset_create_chain($ruleset, $chain);
1120
1121     my $loglevel = get_option_log_level($options, "log_level_in");
1122
1123     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID -j DROP");
1124     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
1125     ruleset_addrule($ruleset, $chain, "-i lo -j ACCEPT");
1126     ruleset_addrule($ruleset, $chain, "-m addrtype --dst-type MULTICAST -j ACCEPT");
1127     ruleset_addrule($ruleset, $chain, "-p udp -m conntrack --ctstate NEW --dport 5404:5405 -j ACCEPT");
1128     ruleset_addrule($ruleset, $chain, "-p udp -m udp --dport 9000 -j ACCEPT");  #corosync
1129
1130     # we use RETURN because we need to check also tap rules
1131     my $accept_action = 'RETURN';
1132
1133     foreach my $rule (@$rules) {
1134         next if $rule->{type} ne 'in';
1135         ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" });
1136     }
1137
1138     # implement input policy
1139     my $policy = $options->{policy_in} || 'DROP'; # allow nothing by default
1140     ruleset_add_chain_policy($ruleset, $chain, 0, $policy, $loglevel, $accept_action);
1141
1142     # host outbound firewall
1143     $chain = "PVEFW-HOST-OUT";
1144     ruleset_create_chain($ruleset, $chain);
1145
1146     $loglevel = get_option_log_level($options, "log_level_out");
1147
1148     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID -j DROP");
1149     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
1150     ruleset_addrule($ruleset, $chain, "-o lo -j ACCEPT");
1151     ruleset_addrule($ruleset, $chain, "-m addrtype --dst-type MULTICAST -j ACCEPT");
1152     ruleset_addrule($ruleset, $chain, "-p udp -m conntrack --ctstate NEW --dport 5404:5405 -j ACCEPT");
1153     ruleset_addrule($ruleset, $chain, "-p udp -m udp --dport 9000 -j ACCEPT"); #corosync
1154
1155     # we use RETURN because we may want to check other thigs later
1156     $accept_action = 'RETURN';
1157
1158     foreach my $rule (@$rules) {
1159         next if $rule->{type} ne 'out';
1160         ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" });
1161     }
1162
1163     # implement output policy
1164     $policy = $options->{policy_out} || 'ACCEPT'; # allow everything by default
1165     ruleset_add_chain_policy($ruleset, $chain, 0, $policy, $loglevel, $accept_action);
1166
1167     ruleset_addrule($ruleset, "PVEFW-OUTPUT", "-j PVEFW-HOST-OUT");
1168     ruleset_addrule($ruleset, "PVEFW-INPUT", "-j PVEFW-HOST-IN");
1169 }
1170
1171 sub generate_group_rules {
1172     my ($ruleset, $groups_conf, $group) = @_;
1173
1174     die "no such security group '$group'\n" if !$groups_conf->{$group};
1175
1176     my $rules = $groups_conf->{rules}->{$group};
1177
1178     my $chain = "GROUP-${group}-IN";
1179
1180     ruleset_create_chain($ruleset, $chain);
1181
1182     foreach my $rule (@$rules) {
1183         next if $rule->{type} ne 'in';
1184         ruleset_generate_rule($ruleset, $chain, $rule, { REJECT => "PVEFW-reject" });
1185     }
1186
1187     $chain = "GROUP-${group}-OUT";
1188
1189     ruleset_create_chain($ruleset, $chain);
1190     ruleset_addrule($ruleset, $chain, "-j MARK --set-mark 0"); # clear mark
1191
1192     foreach my $rule (@$rules) {
1193         next if $rule->{type} ne 'out';
1194         # we use PVEFW-SET-ACCEPT-MARK (Instead of ACCEPT) because we need to
1195         # check also other tap rules later
1196         ruleset_generate_rule($ruleset, $chain, $rule, 
1197                               { ACCEPT => 'PVEFW-SET-ACCEPT-MARK', REJECT => "PVEFW-reject" });
1198     }
1199 }
1200
1201 my $MAX_NETS = 32;
1202 my $valid_netdev_names = {};
1203 for (my $i = 0; $i < $MAX_NETS; $i++)  {
1204     $valid_netdev_names->{"net$i"} = 1;
1205 }
1206
1207 sub parse_fw_rule {
1208     my ($line, $need_iface, $allow_groups) = @_;
1209
1210     my $macros = get_firewall_macros();
1211     my $protocols = get_etc_protocols();
1212
1213     my ($type, $action, $iface, $source, $dest, $proto, $dport, $sport);
1214
1215     # we can add single line comments to the end of the rule
1216     my $comment = $1 if $line =~ s/#\s*(.*?)\s*$//;
1217
1218     # we can disable a rule when prefixed with '|'
1219     my $disable = 1 if  $line =~ s/^\|//;
1220
1221     my @data = split(/\s+/, $line);
1222     my $expected_elements = $need_iface ? 8 : 7;
1223
1224     die "wrong number of rule elements\n" if scalar(@data) > $expected_elements;
1225
1226     if ($need_iface) {
1227         ($type, $action, $iface, $source, $dest, $proto, $dport, $sport) = @data
1228     } else {
1229         ($type, $action, $source, $dest, $proto, $dport, $sport) =  @data;
1230     }
1231
1232     die "incomplete rule\n" if ! ($type && $action);
1233
1234     my $macro;
1235     my $macro_name;
1236
1237     $type = lc($type);
1238
1239     if ($type eq  'in' || $type eq 'out') {
1240         if ($action =~ m/^(ACCEPT|DROP|REJECT)$/) {
1241             # OK
1242         } elsif ($action =~ m/^(\S+)\((ACCEPT|DROP|REJECT)\)$/) {
1243             ($macro_name, $action) = ($1, $2);
1244             my $lc_macro_name = lc($macro_name);
1245             my $preferred_name = $pve_fw_preferred_macro_names->{$lc_macro_name};
1246             $macro_name = $preferred_name if $preferred_name;
1247             $macro = $macros->{$lc_macro_name};
1248             die "unknown macro '$macro_name'\n" if !$macro;
1249         } else {
1250             die "unknown action '$action'\n";
1251         }
1252     } elsif ($type eq 'group') {
1253         die "wrong number of rule elements\n" if scalar(@data) != 3;
1254         die "groups disabled\n" if !$allow_groups;
1255
1256         die "invalid characters in group name\n" if $action !~ m/^[A-Za-z0-9_\-]+$/;    
1257     } else {
1258         die "unknown rule type '$type'\n";
1259     }
1260
1261     if ($need_iface) {
1262         $iface = undef if $iface && $iface eq '-';
1263     }
1264
1265     $proto = undef if $proto && $proto eq '-';
1266     die "unknown protokol '$proto'\n" if $proto &&
1267         !(defined($protocols->{byname}->{$proto}) ||
1268           defined($protocols->{byid}->{$proto}));
1269
1270     $source = undef if $source && $source eq '-';
1271     $dest = undef if $dest && $dest eq '-';
1272
1273     $dport = undef if $dport && $dport eq '-';
1274     $sport = undef if $sport && $sport eq '-';
1275
1276     my $nbsource = undef;
1277     my $nbdest = undef;
1278
1279     $nbsource = parse_address_list($source) if $source;
1280     $nbdest = parse_address_list($dest) if $dest;
1281
1282     my $rules = [];
1283
1284     my $param = {
1285         type => $type,
1286         disable => $disable,
1287         comment => $comment,
1288         action => $action,
1289         iface => $iface,
1290         source => $source,
1291         dest => $dest,
1292         nbsource => $nbsource,
1293         nbdest => $nbdest,
1294         proto => $proto,
1295         dport => $dport,
1296         sport => $sport,
1297     };
1298
1299     if ($macro) {
1300         foreach my $templ (@$macro) {
1301             my $rule = {};
1302             my $param_used = {};
1303             foreach my $k (keys %$templ) {
1304                 my $v = $templ->{$k};
1305                 if ($v eq 'PARAM') {
1306                     $v = $param->{$k};
1307                     $param_used->{$k} = 1;
1308                 } elsif ($v eq 'DEST') {
1309                     $v = $param->{dest};
1310                     $param_used->{dest} = 1;
1311                 } elsif ($v eq 'SOURCE') {
1312                     $v = $param->{source};
1313                     $param_used->{source} = 1;
1314                 }
1315
1316                 die "missing parameter '$k' in macro '$macro_name'\n" if !defined($v);
1317                 $rule->{$k} = $v;
1318             }
1319             foreach my $k (keys %$param) {
1320                 next if !defined($param->{$k});
1321                 next if $param_used->{$k};
1322                 if (defined($rule->{$k})) {
1323                     die "parameter '$k' already define in macro (value = '$rule->{$k}')\n"
1324                         if $rule->{$k} ne $param->{$k};
1325                 } else {
1326                     $rule->{$k} = $param->{$k};
1327                 }
1328             }
1329             push @$rules, $rule;
1330         }
1331     } else {
1332         push @$rules, $param;
1333     }
1334
1335     foreach my $rule (@$rules) {
1336         $rule->{nbdport} = parse_port_name_number_or_range($rule->{dport})
1337             if defined($rule->{dport});
1338         $rule->{nbsport} = parse_port_name_number_or_range($rule->{sport})
1339             if defined($rule->{sport});
1340     }
1341
1342     return $rules;
1343 }
1344
1345 sub parse_vmfw_option {
1346     my ($line) = @_;
1347
1348     my ($opt, $value);
1349
1350     my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
1351
1352     if ($line =~ m/^(enable|dhcp|macfilter|nosmurfs|tcpflags):\s*(0|1)\s*$/i) {
1353         $opt = lc($1);
1354         $value = int($2);
1355     } elsif ($line =~ m/^(log_level_in|log_level_out):\s*(($loglevels)\s*)?$/i) {
1356         $opt = lc($1);
1357         $value = $2 ? lc($3) : '';
1358     } elsif ($line =~ m/^(policy_(in|out)):\s*(ACCEPT|DROP|REJECT)\s*$/i) {
1359         $opt = lc($1);
1360         $value = uc($3);
1361     } else {
1362         chomp $line;
1363         die "can't parse option '$line'\n"
1364     }
1365
1366     return ($opt, $value);
1367 }
1368
1369 sub parse_hostfw_option {
1370     my ($line) = @_;
1371
1372     my ($opt, $value);
1373
1374     my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
1375
1376     if ($line =~ m/^(enable|dhcp|nosmurfs|tcpflags|allow_bridge_route):\s*(0|1)\s*$/i) {
1377         $opt = lc($1);
1378         $value = int($2);
1379     } elsif ($line =~ m/^(log_level_in|log_level_out|tcp_flags_log_level|smurf_log_level):\s*(($loglevels)\s*)?$/i) {
1380         $opt = lc($1);
1381         $value = $2 ? lc($3) : '';
1382     } elsif ($line =~ m/^(policy_(in|out)):\s*(ACCEPT|DROP|REJECT)\s*$/i) {
1383         $opt = lc($1);
1384         $value = uc($3);
1385     } elsif ($line =~ m/^(nf_conntrack_max):\s*(\d+)\s*$/i) {
1386         $opt = lc($1);
1387         $value = int($2);
1388     } else {
1389         chomp $line;
1390         die "can't parse option '$line'\n"
1391     }
1392
1393     return ($opt, $value);
1394 }
1395
1396 sub parse_vm_fw_rules {
1397     my ($filename, $fh) = @_;
1398
1399     my $res = { rules => [], options => {}};
1400
1401     my $section;
1402
1403     my $digest = Digest::SHA->new('sha1');
1404
1405     while (defined(my $line = <$fh>)) {
1406         $digest->add($line);
1407
1408         next if $line =~ m/^#/;
1409         next if $line =~ m/^\s*$/;
1410
1411         my $linenr = $fh->input_line_number();
1412         my $prefix = "$filename (line $linenr)";
1413
1414         if ($line =~ m/^\[(\S+)\]\s*$/i) {
1415             $section = lc($1);
1416             warn "$prefix: ignore unknown section '$section'\n" if !$res->{$section};
1417             next;
1418         }
1419         if (!$section) {
1420             warn "$prefix: skip line - no section";
1421             next;
1422         }
1423
1424         next if !$res->{$section}; # skip undefined section
1425
1426         if ($section eq 'options') {
1427             eval {
1428                 my ($opt, $value) = parse_vmfw_option($line);
1429                 $res->{options}->{$opt} = $value;
1430             };
1431             warn "$prefix: $@" if $@;
1432             next;
1433         }
1434
1435         my $rules;
1436         eval { $rules = parse_fw_rule($line, 1, 1); };
1437         if (my $err = $@) {
1438             warn "$prefix: $err";
1439             next;
1440         }
1441
1442         push @{$res->{$section}}, @$rules;
1443     }
1444
1445     $res->{digest} = $digest->b64digest;
1446
1447     return $res;
1448 }
1449
1450 sub parse_host_fw_rules {
1451     my ($filename, $fh) = @_;
1452
1453     my $res = { rules => [], options => {}};
1454
1455     my $section;
1456
1457     my $digest = Digest::SHA->new('sha1');
1458
1459     while (defined(my $line = <$fh>)) {
1460         $digest->add($line);
1461
1462         next if $line =~ m/^#/;
1463         next if $line =~ m/^\s*$/;
1464
1465         my $linenr = $fh->input_line_number();
1466         my $prefix = "$filename (line $linenr)";
1467
1468         if ($line =~ m/^\[(\S+)\]\s*$/i) {
1469             $section = lc($1);
1470             warn "$prefix: ignore unknown section '$section'\n" if !$res->{$section};
1471             next;
1472         }
1473         if (!$section) {
1474             warn "$prefix: skip line - no section";
1475             next;
1476         }
1477
1478         next if !$res->{$section}; # skip undefined section
1479
1480         if ($section eq 'options') {
1481             eval {
1482                 my ($opt, $value) = parse_hostfw_option($line);
1483                 $res->{options}->{$opt} = $value;
1484             };
1485             warn "$prefix: $@" if $@;
1486             next;
1487         }
1488
1489         my $rules;
1490         eval { $rules = parse_fw_rule($line, 1, 1); };
1491         if (my $err = $@) {
1492             warn "$prefix: $err";
1493             next;
1494         }
1495
1496         push @{$res->{$section}}, @$rules;
1497     }
1498
1499     $res->{digest} = $digest->b64digest;
1500
1501     return $res;
1502 }
1503
1504 sub parse_group_fw_rules {
1505     my ($filename, $fh) = @_;
1506
1507     my $section;
1508     my $group;
1509
1510     my $res = { rules => {} };
1511
1512     my $digest = Digest::SHA->new('sha1');
1513
1514     while (defined(my $line = <$fh>)) {
1515         $digest->add($line);
1516
1517         next if $line =~ m/^#/;
1518         next if $line =~ m/^\s*$/;
1519
1520         my $linenr = $fh->input_line_number();
1521         my $prefix = "$filename (line $linenr)";
1522
1523         if ($line =~ m/^\[group\s+(\S+)\]\s*$/i) {
1524             $section = 'rules';
1525             $group = lc($1);
1526             next;
1527         }
1528         if (!$section || !$group) {
1529             warn "$prefix: skip line - no section";
1530             next;
1531         }
1532
1533         my $rules;
1534         eval { $rules = parse_fw_rule($line, 0, 0); };
1535         if (my $err = $@) {
1536             warn "$prefix: $err";
1537             next;
1538         }
1539
1540         push @{$res->{$section}->{$group}}, @$rules;
1541     }
1542
1543     $res->{digest} = $digest->b64digest;
1544
1545     return $res;
1546 }
1547
1548 sub run_locked {
1549     my ($code, @param) = @_;
1550
1551     my $timeout = 10;
1552
1553     my $res = lock_file($pve_fw_lock_filename, $timeout, $code, @param);
1554
1555     die $@ if $@;
1556
1557     return $res;
1558 }
1559
1560 sub read_local_vm_config {
1561
1562     my $openvz = {};
1563     my $qemu = {};
1564
1565     my $vmdata = { openvz => $openvz, qemu => $qemu };
1566
1567     my $vmlist = PVE::Cluster::get_vmlist();
1568     return $vmdata if !$vmlist || !$vmlist->{ids};
1569     my $ids = $vmlist->{ids};
1570
1571     foreach my $vmid (keys %$ids) {
1572         next if !$vmid; # skip VE0
1573         my $d = $ids->{$vmid};
1574         next if !$d->{node} || $d->{node} ne $nodename;
1575         next if !$d->{type};
1576         if ($d->{type} eq 'openvz') {
1577             if ($have_pve_manager) {
1578                 my $cfspath = PVE::OpenVZ::cfs_config_path($vmid);
1579                 if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
1580                     $openvz->{$vmid} = $conf;
1581                 }
1582             }
1583         } elsif ($d->{type} eq 'qemu') {
1584             if ($have_qemu_server) {
1585                 my $cfspath = PVE::QemuServer::cfs_config_path($vmid);
1586                 if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
1587                     $qemu->{$vmid} = $conf;
1588                 }
1589             }
1590         }
1591     }
1592
1593     return $vmdata;
1594 };
1595
1596 sub load_vmfw_conf {
1597     my ($vmid) = @_;
1598
1599     my $vmfw_conf = {};
1600
1601     my $filename = "/etc/pve/firewall/$vmid.fw";
1602     if (my $fh = IO::File->new($filename, O_RDONLY)) {
1603         $vmfw_conf = parse_vm_fw_rules($filename, $fh);
1604     }
1605
1606     return $vmfw_conf;
1607 }
1608
1609 sub read_vm_firewall_configs {
1610     my ($vmdata) = @_;
1611     my $vmfw_configs = {};
1612
1613     foreach my $vmid (keys %{$vmdata->{qemu}}, keys %{$vmdata->{openvz}}) {
1614         my $vmfw_conf = load_vmfw_conf($vmid);
1615         next if !$vmfw_conf->{options}; # skip if file does not exists
1616         $vmfw_configs->{$vmid} = $vmfw_conf;
1617     }
1618
1619     return $vmfw_configs;
1620 }
1621
1622 sub get_option_log_level {
1623     my ($options, $k) = @_;
1624
1625     my $v = $options->{$k};
1626     $v = $default_log_level if !defined($v);
1627
1628     return undef if $v eq '' || $v eq 'nolog';
1629
1630     $v = $log_level_hash->{$v} if defined($log_level_hash->{$v});
1631
1632     return $v if ($v >= 0) && ($v <= 7);
1633
1634     warn "unknown log level ($k = '$v')\n";
1635
1636     return undef;
1637 }
1638
1639 sub generate_std_chains {
1640     my ($ruleset, $options) = @_;
1641     
1642     my $loglevel = get_option_log_level($options, 'smurf_log_level');
1643
1644     # same as shorewall smurflog.
1645     my $chain = 'PVEFW-smurflog';
1646
1647     push @{$pve_std_chains->{$chain}}, get_log_rule_base($chain, 0, "DROP: ", $loglevel) if $loglevel;
1648     push @{$pve_std_chains->{$chain}}, "-j DROP";
1649
1650     # same as shorewall logflags action.
1651     $loglevel = get_option_log_level($options, 'tcp_flags_log_level');
1652     $chain = 'PVEFW-logflags';
1653     # fixme: is this correctly logged by pvewf-logger? (ther is no --log-ip-options for NFLOG)
1654     push @{$pve_std_chains->{$chain}}, get_log_rule_base($chain, 0, "DROP: ", $loglevel) if $loglevel;
1655     push @{$pve_std_chains->{$chain}}, "-j DROP";
1656
1657     foreach my $chain (keys %$pve_std_chains) {
1658         ruleset_create_chain($ruleset, $chain);
1659         foreach my $rule (@{$pve_std_chains->{$chain}}) {
1660             if (ref($rule)) {
1661                 ruleset_generate_rule($ruleset, $chain, $rule);
1662             } else {
1663                 ruleset_addrule($ruleset, $chain, $rule);
1664             }
1665         }
1666     }
1667 }
1668
1669 sub save_pvefw_status {
1670     my ($status) = @_;
1671
1672     die "unknown status '$status' - internal error"
1673         if $status !~ m/^(stopped|active)$/;
1674
1675     mkdir dirname($pve_fw_status_filename);
1676     PVE::Tools::file_set_contents($pve_fw_status_filename, $status);
1677 }
1678
1679 sub read_pvefw_status {
1680
1681     my $status = 'unknown';
1682
1683     return 'stopped' if ! -f $pve_fw_status_filename;
1684
1685     eval {
1686         $status = PVE::Tools::file_get_contents($pve_fw_status_filename);
1687     };
1688     warn $@ if $@;
1689
1690     return $status;
1691 }
1692
1693 # fixme: move to pve-common PVE::ProcFSTools
1694 sub read_proc_net_route {
1695     my $filename = "/proc/net/route";
1696
1697     my $res = {};
1698
1699     my $fh = IO::File->new ($filename, "r");
1700     return $res if !$fh;
1701
1702     my $int_to_quad = sub {
1703         return join '.' => map { ($_[0] >> 8*(3-$_)) % 256 } (3, 2, 1, 0);
1704     };
1705
1706     while (defined(my $line = <$fh>)) {
1707         next if $line =~/^Iface\s+Destination/; # skip head
1708         my ($iface, $dest, $gateway, $metric, $mask, $mtu) = (split(/\s+/, $line))[0,1,2,6,7,8];
1709         push @{$res->{$iface}}, {
1710             dest => &$int_to_quad(hex($dest)),
1711             gateway => &$int_to_quad(hex($gateway)),
1712             mask => &$int_to_quad(hex($mask)),
1713             metric => $metric,
1714             mtu => $mtu,
1715         };
1716     }
1717
1718     return $res;
1719 }
1720
1721 sub load_security_groups {
1722
1723     my $groups_conf = {};
1724     my $filename = "/etc/pve/firewall/groups.fw";
1725     if (my $fh = IO::File->new($filename, O_RDONLY)) {
1726         $groups_conf = parse_group_fw_rules($filename, $fh);
1727     }
1728
1729     return $groups_conf;
1730 }
1731
1732 sub load_hostfw_conf {
1733
1734     my $hostfw_conf = {};
1735     my $filename = "/etc/pve/local/host.fw";
1736     if (my $fh = IO::File->new($filename, O_RDONLY)) {
1737         $hostfw_conf = parse_host_fw_rules($filename, $fh);
1738     }
1739     return $hostfw_conf;
1740 }
1741
1742 sub compile {
1743     my $vmdata = read_local_vm_config();
1744     my $vmfw_configs = read_vm_firewall_configs($vmdata);
1745
1746     my $routing_table = read_proc_net_route();
1747
1748     my $groups_conf = load_security_groups();
1749
1750     my $ruleset = {};
1751
1752     ruleset_create_chain($ruleset, "PVEFW-INPUT");
1753     ruleset_create_chain($ruleset, "PVEFW-OUTPUT");
1754
1755     ruleset_create_chain($ruleset, "PVEFW-FORWARD");
1756
1757     my $hostfw_conf = load_hostfw_conf();
1758     my $hostfw_options = $hostfw_conf->{options} || {};
1759
1760     generate_std_chains($ruleset, $hostfw_options);
1761
1762     my $hostfw_enable = !(defined($hostfw_options->{enable}) && ($hostfw_options->{enable} == 0));
1763
1764     enable_host_firewall($ruleset, $hostfw_conf, $groups_conf) if $hostfw_enable;
1765
1766     # generate firewall rules for QEMU VMs
1767     foreach my $vmid (keys %{$vmdata->{qemu}}) {
1768         my $conf = $vmdata->{qemu}->{$vmid};
1769         my $vmfw_conf = $vmfw_configs->{$vmid};
1770         next if !$vmfw_conf;
1771         next if defined($vmfw_conf->{options}->{enable}) && ($vmfw_conf->{options}->{enable} == 0);
1772
1773         foreach my $netid (keys %$conf) {
1774             next if $netid !~ m/^net(\d+)$/;
1775             my $net = PVE::QemuServer::parse_net($conf->{$netid});
1776             next if !$net;
1777             my $iface = "tap${vmid}i$1";
1778
1779             my $bridge = $net->{bridge};
1780             next if !$bridge; # fixme: ?
1781
1782             $bridge .= "v$net->{tag}" if $net->{tag};
1783
1784             generate_bridge_chains($ruleset, $hostfw_conf, $bridge, $routing_table);
1785
1786             my $macaddr = $net->{macaddr};
1787             generate_tap_rules_direction($ruleset, $groups_conf, $iface, $netid, $macaddr, 
1788                                          $vmfw_conf, $vmid, $bridge, 'IN');
1789             generate_tap_rules_direction($ruleset, $groups_conf, $iface, $netid, $macaddr, 
1790                                          $vmfw_conf, $vmid, $bridge, 'OUT');
1791         }
1792     }
1793
1794     # generate firewall rules for OpenVZ containers
1795     foreach my $vmid (keys %{$vmdata->{openvz}}) {
1796         my $conf = $vmdata->{openvz}->{$vmid};
1797
1798         my $vmfw_conf = $vmfw_configs->{$vmid};
1799         next if !$vmfw_conf;
1800         next if defined($vmfw_conf->{options}->{enable}) && ($vmfw_conf->{options}->{enable} == 0);
1801
1802         if ($conf->{ip_address} && $conf->{ip_address}->{value}) {
1803             my $ip = $conf->{ip_address}->{value};
1804             generate_venet_rules_direction($ruleset, $groups_conf, $vmfw_conf, $vmid, $ip, 'IN');
1805             generate_venet_rules_direction($ruleset, $groups_conf, $vmfw_conf, $vmid, $ip, 'OUT');
1806         }
1807
1808         if ($conf->{netif} && $conf->{netif}->{value}) {
1809             my $netif = PVE::OpenVZ::parse_netif($conf->{netif}->{value});
1810             foreach my $netid (keys %$netif) {
1811                 my $d = $netif->{$netid};
1812                 my $bridge = $d->{bridge};
1813                 if (!$bridge) {
1814                     warn "no bridge device for CT $vmid iface '$netid'\n";
1815                     next; # fixme?
1816                 }
1817                 
1818                 generate_bridge_chains($ruleset, $hostfw_conf, $bridge, $routing_table);
1819
1820                 my $macaddr = $d->{mac};
1821                 my $iface = $d->{host_ifname};
1822                 generate_tap_rules_direction($ruleset, $groups_conf, $iface, $netid, $macaddr, 
1823                                              $vmfw_conf, $vmid, $bridge, 'IN');
1824                 generate_tap_rules_direction($ruleset, $groups_conf, $iface, $netid, $macaddr, 
1825                                              $vmfw_conf, $vmid, $bridge, 'OUT');
1826             }
1827         }
1828     }
1829
1830     # fixme: this is an optimization? if so, we should also drop INVALID packages?
1831     ruleset_insertrule($ruleset, "PVEFW-FORWARD", "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
1832
1833     # fixme: what log level should we use here?
1834     my $loglevel = get_option_log_level($hostfw_options, "log_level_out");
1835
1836     # fixme: should we really block inter-bridge traffic?
1837
1838     # always allow traffic from containers?
1839     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i venet0 -j RETURN");
1840
1841     # disable interbridge routing
1842     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o vmbr+ -j PVEFW-Drop"); 
1843     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i vmbr+ -j PVEFW-Drop");
1844     ruleset_addlog($ruleset, "PVEFW-FORWARD", 0, "DROP: ", $loglevel, "-o vmbr+");  
1845     ruleset_addlog($ruleset, "PVEFW-FORWARD", 0, "DROP: ", $loglevel, "-i vmbr+");  
1846     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o vmbr+ -j DROP");  
1847     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i vmbr+ -j DROP");
1848
1849     return wantarray ? ($ruleset, $hostfw_conf) : $ruleset;
1850 }
1851
1852 sub get_ruleset_status {
1853     my ($ruleset, $verbose) = @_;
1854
1855     my $active_chains = iptables_get_chains();
1856
1857     my $statushash = {};
1858
1859     foreach my $chain (sort keys %$ruleset) {
1860         my $digest = Digest::SHA->new('sha1');
1861         foreach my $cmd (@{$ruleset->{$chain}}) {
1862              $digest->add("$cmd\n");
1863         }
1864         my $sig = $digest->b64digest;
1865         $statushash->{$chain}->{sig} = $sig;
1866
1867         my $oldsig = $active_chains->{$chain};
1868         if (!defined($oldsig)) {
1869             $statushash->{$chain}->{action} = 'create';
1870         } else {
1871             if ($oldsig eq $sig) {
1872                 $statushash->{$chain}->{action} = 'exists';
1873             } else {
1874                 $statushash->{$chain}->{action} = 'update';
1875             }
1876         }
1877         print "$statushash->{$chain}->{action} $chain ($sig)\n" if $verbose;
1878         foreach my $cmd (@{$ruleset->{$chain}}) {
1879             print "\t$cmd\n" if $verbose;
1880         }
1881     }
1882
1883     foreach my $chain (sort keys %$active_chains) {
1884         if (!defined($ruleset->{$chain})) {
1885             my $sig = $active_chains->{$chain};
1886             $statushash->{$chain}->{action} = 'delete';
1887             $statushash->{$chain}->{sig} = $sig;
1888             print "delete $chain ($sig)\n" if $verbose;
1889         }
1890     }
1891
1892     return $statushash;
1893 }
1894
1895 sub print_ruleset {
1896     my ($ruleset) = @_;
1897
1898     get_ruleset_status($ruleset, 1);
1899 }
1900
1901 sub print_sig_rule {
1902     my ($chain, $sig) = @_;
1903
1904     # We just use this to store a SHA1 checksum used to detect changes
1905     return "-A $chain -m comment --comment \"PVESIG:$sig\"\n";
1906 }
1907
1908 sub get_rulset_cmdlist {
1909     my ($ruleset, $verbose) = @_;
1910
1911     my $cmdlist = "*filter\n"; # we pass this to iptables-restore;
1912
1913     my $statushash = get_ruleset_status($ruleset, $verbose);
1914
1915     # create missing chains first
1916     foreach my $chain (sort keys %$ruleset) {
1917         my $stat = $statushash->{$chain};
1918         die "internal error" if !$stat;
1919         next if $stat->{action} ne 'create';
1920
1921         $cmdlist .= ":$chain - [0:0]\n";
1922     }
1923
1924     my $rule = "INPUT -j PVEFW-INPUT";
1925     if (!PVE::Firewall::iptables_rule_exist($rule)) {
1926         $cmdlist .= "-A $rule\n";
1927     }
1928     $rule = "OUTPUT -j PVEFW-OUTPUT";
1929     if (!PVE::Firewall::iptables_rule_exist($rule)) {
1930         $cmdlist .= "-A $rule\n";
1931     }
1932
1933     $rule = "FORWARD -j PVEFW-FORWARD";
1934     if (!PVE::Firewall::iptables_rule_exist($rule)) {
1935         $cmdlist .= "-A $rule\n";
1936     }
1937
1938     foreach my $chain (sort keys %$ruleset) {
1939         my $stat = $statushash->{$chain};
1940         die "internal error" if !$stat;
1941
1942         if ($stat->{action} eq 'update' || $stat->{action} eq 'create') {
1943             $cmdlist .= "-F $chain\n";
1944             foreach my $cmd (@{$ruleset->{$chain}}) {
1945                 $cmdlist .= "$cmd\n";
1946             }
1947             $cmdlist .= print_sig_rule($chain, $stat->{sig});
1948         } elsif ($stat->{action} eq 'delete') {
1949             die "internal error"; # this should not happen
1950         } elsif ($stat->{action} eq 'exists') {
1951             # do nothing
1952         } else {
1953             die "internal error - unknown status '$stat->{action}'";
1954         }
1955     }
1956
1957     foreach my $chain (keys %$statushash) {
1958         next if $statushash->{$chain}->{action} ne 'delete';
1959         $cmdlist .= "-F $chain\n";
1960     }
1961     foreach my $chain (keys %$statushash) {
1962         next if $statushash->{$chain}->{action} ne 'delete';
1963         next if $chain eq 'PVEFW-INPUT';
1964         next if $chain eq 'PVEFW-OUTPUT';
1965         next if $chain eq 'PVEFW-FORWARD';
1966         $cmdlist .= "-X $chain\n";
1967     }
1968
1969     $cmdlist .= "COMMIT\n";
1970
1971     return $cmdlist;
1972 }
1973
1974 sub apply_ruleset {
1975     my ($ruleset, $hostfw_conf, $verbose) = @_;
1976
1977     enable_bridge_firewall();
1978
1979     update_nf_conntrack_max($hostfw_conf);
1980
1981     my $cmdlist = get_rulset_cmdlist($ruleset, $verbose);
1982
1983     print $cmdlist if $verbose;
1984
1985     iptables_restore_cmdlist($cmdlist);
1986
1987     # test: re-read status and check if everything is up to date
1988     my $statushash = get_ruleset_status($ruleset);
1989
1990     my $errors;
1991     foreach my $chain (sort keys %$ruleset) {
1992         my $stat = $statushash->{$chain};
1993         if ($stat->{action} ne 'exists') {
1994             warn "unable to update chain '$chain'\n";
1995             $errors = 1;
1996         }
1997     }
1998
1999     die "unable to apply firewall changes\n" if $errors;
2000 }
2001
2002 sub update_nf_conntrack_max {
2003     my ($hostfw_conf) = @_;
2004
2005     my $max = 65536; # reasonable default
2006
2007     my $options = $hostfw_conf->{options} || {};
2008
2009     if (defined($options->{nf_conntrack_max}) && ($options->{nf_conntrack_max} > $max)) {
2010         $max = $options->{nf_conntrack_max};
2011         $max = int(($max+ 8191)/8192)*8192; # round to multiples of 8192
2012     }
2013
2014     my $filename_nf_conntrack_max = "/proc/sys/net/nf_conntrack_max";
2015     my $filename_hashsize = "/sys/module/nf_conntrack/parameters/hashsize";
2016
2017     my $current = int(PVE::Tools::file_read_firstline($filename_nf_conntrack_max) || $max);
2018
2019     if ($current != $max) {
2020         my $hashsize = int($max/4);
2021         PVE::ProcFSTools::write_proc_entry($filename_hashsize, $hashsize);
2022         PVE::ProcFSTools::write_proc_entry($filename_nf_conntrack_max, $max);
2023     }
2024 }
2025
2026 sub update {
2027     my ($start, $verbose) = @_;
2028
2029     my $code = sub {
2030         my $status = read_pvefw_status();
2031
2032         my ($ruleset, $hostfw_conf) = PVE::Firewall::compile();
2033
2034         if ($start || $status eq 'active') {
2035
2036             save_pvefw_status('active') if ($status ne 'active');
2037
2038             apply_ruleset($ruleset, $hostfw_conf, $verbose);
2039         } else {
2040             print "Firewall not active (status = $status)\n" if $verbose;
2041         }
2042     };
2043
2044     run_locked($code);
2045 }
2046
2047
2048 1;