9a8561e0cf2018a3b1a0ed0243149a6f1d2eec93
[pve-firewall.git] / src / PVE / Firewall.pm
1 package PVE::Firewall;
2
3 use warnings;
4 use strict;
5 use POSIX;
6 use Data::Dumper;
7 use Digest::SHA;
8 use PVE::INotify;
9 use PVE::Exception qw(raise raise_param_exc);
10 use PVE::JSONSchema qw(get_standard_option);
11 use PVE::Cluster;
12 use PVE::ProcFSTools;
13 use PVE::Tools;
14 use File::Basename;
15 use File::Path;
16 use IO::File;
17 use Net::IP;
18 use PVE::Tools qw(run_command lock_file);
19 use Encode;
20
21 my $hostfw_conf_filename = "/etc/pve/local/host.fw";
22 my $clusterfw_conf_filename = "/etc/pve/firewall/cluster.fw";
23
24 # dynamically include PVE::QemuServer and PVE::OpenVZ
25 # to avoid dependency problems
26 my $have_qemu_server;
27 eval {
28     require PVE::QemuServer;
29     $have_qemu_server = 1;
30 };
31
32 my $have_pve_manager;
33 eval {
34     require PVE::OpenVZ;
35     $have_pve_manager = 1;
36 };
37
38 my $feature_ipset_nomatch = 0;
39 eval  {
40     my (undef, undef, $release) = POSIX::uname();
41     if ($release =~ m/^(\d+)\.(\d+)\.\d+-/) {
42         my ($major, $minor) = ($1, $2);
43         $feature_ipset_nomatch = 1 if ($major > 3) ||
44             ($major == 3 && $minor >= 7);
45     }
46
47 };
48
49 use Data::Dumper;
50
51 my $nodename = PVE::INotify::nodename();
52
53 my $pve_fw_lock_filename = "/var/lock/pvefw.lck";
54 my $pve_fw_status_filename = "/var/lib/pve-firewall/pvefw.status";
55
56 my $default_log_level = 'info';
57
58 my $log_level_hash = {
59     debug => 7,
60     info => 6,
61     notice => 5,
62     warning => 4,
63     err => 3,
64     crit => 2,
65     alert => 1,
66     emerg => 0,
67 };
68
69 # imported/converted from: /usr/share/shorewall/macro.*
70 my $pve_fw_macros = {
71     'Amanda' => [
72         "Amanda Backup",
73         { action => 'PARAM', proto => 'udp', dport => '10080' },
74         { action => 'PARAM', proto => 'tcp', dport => '10080' },
75     ],
76     'Auth' => [
77         "Auth (identd) traffic",
78         { action => 'PARAM', proto => 'tcp', dport => '113' },
79     ],
80     'BGP' => [
81         "Border Gateway Protocol traffic",
82         { action => 'PARAM', proto => 'tcp', dport => '179' },
83     ],
84     'BitTorrent' => [
85         "BitTorrent traffic for BitTorrent 3.1 and earlier",
86         { action => 'PARAM', proto => 'tcp', dport => '6881:6889' },
87         { action => 'PARAM', proto => 'udp', dport => '6881' },
88     ],
89     'BitTorrent32' => [
90         "BitTorrent traffic for BitTorrent 3.2 and later",
91         { action => 'PARAM', proto => 'tcp', dport => '6881:6999' },
92         { action => 'PARAM', proto => 'udp', dport => '6881' },
93     ],
94     'CVS' => [
95         "Concurrent Versions System pserver traffic",
96         { action => 'PARAM', proto => 'tcp', dport => '2401' },
97     ],
98     'Citrix' => [
99         "Citrix/ICA traffic (ICA, ICA Browser, CGP)",
100         { action => 'PARAM', proto => 'tcp', dport => '1494' },
101         { action => 'PARAM', proto => 'udp', dport => '1604' },
102         { action => 'PARAM', proto => 'tcp', dport => '2598' },
103     ],
104     'DAAP' => [
105         "Digital Audio Access Protocol traffic (iTunes, Rythmbox daemons)",
106         { action => 'PARAM', proto => 'tcp', dport => '3689' },
107         { action => 'PARAM', proto => 'udp', dport => '3689' },
108     ],
109     'DCC' => [
110         "Distributed Checksum Clearinghouse spam filtering mechanism",
111         { action => 'PARAM', proto => 'tcp', dport => '6277' },
112     ],
113     'DHCPfwd' => [
114         "Forwarded DHCP traffic (bidirectional)",
115         { action => 'PARAM', proto => 'udp', dport => '67:68', sport => '67:68' },
116         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '67:68', sport => '67:68' },
117     ],
118     'DNS' => [
119         "Domain Name System traffic (upd and tcp)",
120         { action => 'PARAM', proto => 'udp', dport => '53' },
121         { action => 'PARAM', proto => 'tcp', dport => '53' },
122     ],
123     'Distcc' => [
124         "Distributed Compiler service",
125         { action => 'PARAM', proto => 'tcp', dport => '3632' },
126     ],
127     'FTP' => [
128         "File Transfer Protocol",
129         { action => 'PARAM', proto => 'tcp', dport => '21' },
130     ],
131     'Finger' => [
132         "Finger protocol (RFC 742)",
133         { action => 'PARAM', proto => 'tcp', dport => '79' },
134     ],
135     'GNUnet' => [
136         "GNUnet secure peer-to-peer networking traffic",
137         { action => 'PARAM', proto => 'tcp', dport => '2086' },
138         { action => 'PARAM', proto => 'udp', dport => '2086' },
139         { action => 'PARAM', proto => 'tcp', dport => '1080' },
140         { action => 'PARAM', proto => 'udp', dport => '1080' },
141     ],
142     'GRE' => [
143         "Generic Routing Encapsulation tunneling protocol (bidirectional)",
144         { action => 'PARAM', proto => '47' },
145         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '47' },
146     ],
147     'Git' => [
148         "Git distributed revision control traffic",
149         { action => 'PARAM', proto => 'tcp', dport => '9418' },
150     ],
151     'HKP' => [
152         "OpenPGP HTTP keyserver protocol traffic",
153         { action => 'PARAM', proto => 'tcp', dport => '11371' },
154     ],
155     'HTTP' => [
156         "Hypertext Transfer Protocol (WWW)",
157         { action => 'PARAM', proto => 'tcp', dport => '80' },
158     ],
159     'HTTPS' => [
160         "Hypertext Transfer Protocol (WWW) over SSL",
161         { action => 'PARAM', proto => 'tcp', dport => '443' },
162     ],
163     'ICPV2' => [
164         "Internet Cache Protocol V2 (Squid) traffic",
165         { action => 'PARAM', proto => 'udp', dport => '3130' },
166     ],
167     'ICQ' => [
168         "AOL Instant Messenger traffic",
169         { action => 'PARAM', proto => 'tcp', dport => '5190' },
170     ],
171     'IMAP' => [
172         "Internet Message Access Protocol",
173         { action => 'PARAM', proto => 'tcp', dport => '143' },
174     ],
175     'IMAPS' => [
176         "Internet Message Access Protocol over SSL",
177         { action => 'PARAM', proto => 'tcp', dport => '993' },
178     ],
179     'IPIP' => [
180         "IPIP capsulation traffic (bidirectional)",
181         { action => 'PARAM', proto => '94' },
182         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '94' },
183     ],
184     'IPsec' => [
185         "IPsec traffic (bidirectional)",
186         { action => 'PARAM', proto => 'udp', dport => '500', sport => '500' },
187         { action => 'PARAM', proto => '50' },
188         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '500', sport => '500' },
189         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '50' },
190     ],
191     'IPsecah' => [
192         "IPsec authentication (AH) traffic (bidirectional)",
193         { action => 'PARAM', proto => 'udp', dport => '500', sport => '500' },
194         { action => 'PARAM', proto => '51' },
195         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '500', sport => '500' },
196         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '51' },
197     ],
198     'IPsecnat' => [
199         "IPsec traffic and Nat-Traversal (bidirectional)",
200         { action => 'PARAM', proto => 'udp', dport => '500' },
201         { action => 'PARAM', proto => 'udp', dport => '4500' },
202         { action => 'PARAM', proto => '50' },
203         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '500' },
204         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '4500' },
205         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '50' },
206     ],
207     'IRC' => [
208         "Internet Relay Chat traffic",
209         { action => 'PARAM', proto => 'tcp', dport => '6667' },
210     ],
211     'Jetdirect' => [
212         "HP Jetdirect printing",
213         { action => 'PARAM', proto => 'tcp', dport => '9100' },
214     ],
215     'L2TP' => [
216         "Layer 2 Tunneling Protocol traffic",
217         { action => 'PARAM', proto => 'udp', dport => '1701' },
218         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '1701' },
219     ],
220     'LDAP' => [
221         "Lightweight Directory Access Protocol traffic",
222         { action => 'PARAM', proto => 'tcp', dport => '389' },
223     ],
224     'LDAPS' => [
225         "Secure Lightweight Directory Access Protocol traffic",
226         { action => 'PARAM', proto => 'tcp', dport => '636' },
227     ],
228     'MSNP' => [
229         "Microsoft Notification Protocol",
230         { action => 'PARAM', proto => 'tcp', dport => '1863' },
231     ],
232     'MSSQL' => [
233         "Microsoft SQL Server",
234         { action => 'PARAM', proto => 'tcp', dport => '1433' },
235     ],
236     'Mail' => [
237         "Mail traffic (SMTP, SMTPS, Submission)",
238         { action => 'PARAM', proto => 'tcp', dport => '25' },
239         { action => 'PARAM', proto => 'tcp', dport => '465' },
240         { action => 'PARAM', proto => 'tcp', dport => '587' },
241     ],
242     'Munin' => [
243         "Munin networked resource monitoring traffic",
244         { action => 'PARAM', proto => 'tcp', dport => '4949' },
245     ],
246     'MySQL' => [
247         "MySQL server",
248         { action => 'PARAM', proto => 'tcp', dport => '3306' },
249     ],
250     'NNTP' => [
251         "NNTP traffic (Usenet).",
252         { action => 'PARAM', proto => 'tcp', dport => '119' },
253     ],
254     'NNTPS' => [
255         "Encrypted NNTP traffic (Usenet)",
256         { action => 'PARAM', proto => 'tcp', dport => '563' },
257     ],
258     'NTP' => [
259         "Network Time Protocol (ntpd)",
260         { action => 'PARAM', proto => 'udp', dport => '123' },
261     ],
262     'NTPbi' => [
263         "Bi-directional NTP (for NTP peers)",
264         { action => 'PARAM', proto => 'udp', dport => '123' },
265         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '123' },
266     ],
267     'OSPF' => [
268         "OSPF multicast traffic",
269         { action => 'PARAM', proto => '89' },
270     ],
271     'OpenVPN' => [
272         "OpenVPN traffic",
273         { action => 'PARAM', proto => 'udp', dport => '1194' },
274     ],
275     'PCA' => [
276         "Symantec PCAnywere (tm)",
277         { action => 'PARAM', proto => 'udp', dport => '5632' },
278         { action => 'PARAM', proto => 'tcp', dport => '5631' },
279     ],
280     'POP3' => [
281         "POP3 traffic",
282         { action => 'PARAM', proto => 'tcp', dport => '110' },
283     ],
284     'POP3S' => [
285         "Encrypted POP3 traffic",
286         { action => 'PARAM', proto => 'tcp', dport => '995' },
287     ],
288     'PPtP' => [
289         "Point-to-Point Tunneling Protocol",
290         { action => 'PARAM', proto => '47' },
291         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => '47' },
292         { action => 'PARAM', proto => 'tcp', dport => '1723' },
293     ],
294     'Ping' => [
295         "ICMP echo request",
296         { action => 'PARAM', proto => 'icmp', dport => 'echo-request' },
297     ],
298     'PostgreSQL' => [
299         "PostgreSQL server",
300         { action => 'PARAM', proto => 'tcp', dport => '5432' },
301     ],
302     'Printer' => [
303         "Line Printer protocol printing",
304         { action => 'PARAM', proto => 'tcp', dport => '515' },
305     ],
306     'RDP' => [
307         "Microsoft Remote Desktop Protocol traffic",
308         { action => 'PARAM', proto => 'tcp', dport => '3389' },
309     ],
310     'RIPbi' => [
311         "Routing Information Protocol (bidirectional)",
312         { action => 'PARAM', proto => 'udp', dport => '520' },
313         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '520' },
314     ],
315     'RNDC' => [
316         "BIND remote management protocol",
317         { action => 'PARAM', proto => 'tcp', dport => '953' },
318     ],
319     'Razor' => [
320         "Razor Antispam System",
321         { action => 'ACCEPT', proto => 'tcp', dport => '2703' },
322     ],
323     'Rdate' => [
324         "Remote time retrieval (rdate)",
325         { action => 'PARAM', proto => 'tcp', dport => '37' },
326     ],
327     'Rsync' => [
328         "Rsync server",
329         { action => 'PARAM', proto => 'tcp', dport => '873' },
330     ],
331     'SANE' => [
332         "SANE network scanning",
333         { action => 'PARAM', proto => 'tcp', dport => '6566' },
334     ],
335     'SMB' => [
336         "Microsoft SMB traffic",
337         { action => 'PARAM', proto => 'udp', dport => '135,445' },
338         { action => 'PARAM', proto => 'udp', dport => '137:139' },
339         { action => 'PARAM', proto => 'udp', dport => '1024:65535', sport => '137' },
340         { action => 'PARAM', proto => 'tcp', dport => '135,139,445' },
341     ],
342     'SMBBI' => [
343         "Microsoft SMB traffic (bidirectional)",
344         { action => 'PARAM', proto => 'udp', dport => '135,445' },
345         { action => 'PARAM', proto => 'udp', dport => '137:139' },
346         { action => 'PARAM', proto => 'udp', dport => '1024:65535', sport => '137' },
347         { action => 'PARAM', proto => 'tcp', dport => '135,139,445' },
348         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '135,445' },
349         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '137:139' },
350         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'udp', dport => '1024:65535', sport => '137' },
351         { action => 'PARAM', source => 'DEST', dest => 'SOURCE', proto => 'tcp', dport => '135,139,445' },
352     ],
353     'SMBswat' => [
354         "Samba Web Administration Tool",
355         { action => 'PARAM', proto => 'tcp', dport => '901' },
356     ],
357     'SMTP' => [
358         "Simple Mail Transfer Protocol",
359         { action => 'PARAM', proto => 'tcp', dport => '25' },
360     ],
361     'SMTPS' => [
362         "Encrypted Simple Mail Transfer Protocol",
363         { action => 'PARAM', proto => 'tcp', dport => '465' },
364     ],
365     'SNMP' => [
366         "Simple Network Management Protocol",
367         { action => 'PARAM', proto => 'udp', dport => '161:162' },
368         { action => 'PARAM', proto => 'tcp', dport => '161' },
369     ],
370     'SPAMD' => [
371         "Spam Assassin SPAMD traffic",
372         { action => 'PARAM', proto => 'tcp', dport => '783' },
373     ],
374     'SSH' => [
375         "Secure shell traffic",
376         { action => 'PARAM', proto => 'tcp', dport => '22' },
377     ],
378     'SVN' => [
379         "Subversion server (svnserve)",
380         { action => 'PARAM', proto => 'tcp', dport => '3690' },
381     ],
382     'SixXS' => [
383         "SixXS IPv6 Deployment and Tunnel Broker",
384         { action => 'PARAM', proto => 'tcp', dport => '3874' },
385         { action => 'PARAM', proto => 'udp', dport => '3740' },
386         { action => 'PARAM', proto => '41' },
387         { action => 'PARAM', proto => 'udp', dport => '5072,8374' },
388     ],
389     'Squid' => [
390         "Squid web proxy traffic",
391         { action => 'PARAM', proto => 'tcp', dport => '3128' },
392     ],
393     'Submission' => [
394         "Mail message submission traffic",
395         { action => 'PARAM', proto => 'tcp', dport => '587' },
396     ],
397     'Syslog' => [
398         "Syslog protocol (RFC 5424) traffic",
399         { action => 'PARAM', proto => 'udp', dport => '514' },
400         { action => 'PARAM', proto => 'tcp', dport => '514' },
401     ],
402     'TFTP' => [
403         "Trivial File Transfer Protocol traffic",
404         { action => 'PARAM', proto => 'udp', dport => '69' },
405     ],
406     'Telnet' => [
407         "Telnet traffic",
408         { action => 'PARAM', proto => 'tcp', dport => '23' },
409     ],
410     'Telnets' => [
411         "Telnet over SSL",
412         { action => 'PARAM', proto => 'tcp', dport => '992' },
413     ],
414     'Time' => [
415         "RFC 868 Time protocol",
416         { action => 'PARAM', proto => 'tcp', dport => '37' },
417     ],
418     'Trcrt' => [
419         "Traceroute (for up to 30 hops) traffic",
420         { action => 'PARAM', proto => 'udp', dport => '33434:33524' },
421         { action => 'PARAM', proto => 'icmp', dport => 'echo-request' },
422     ],
423     'VNC' => [
424         "VNC traffic for VNC display's 0 - 9",
425         { action => 'PARAM', proto => 'tcp', dport => '5900:5909' },
426     ],
427     'VNCL' => [
428         "VNC traffic from Vncservers to Vncviewers in listen mode",
429         { action => 'PARAM', proto => 'tcp', dport => '5500' },
430     ],
431     'Web' => [
432         "WWW traffic (HTTP and HTTPS)",
433         { action => 'PARAM', proto => 'tcp', dport => '80' },
434         { action => 'PARAM', proto => 'tcp', dport => '443' },
435     ],
436     'Webcache' => [
437         "Web Cache/Proxy traffic (port 8080)",
438         { action => 'PARAM', proto => 'tcp', dport => '8080' },
439     ],
440     'Webmin' => [
441         "Webmin traffic",
442         { action => 'PARAM', proto => 'tcp', dport => '10000' },
443     ],
444     'Whois' => [
445         "Whois (nicname, RFC 3912) traffic",
446         { action => 'PARAM', proto => 'tcp', dport => '43' },
447     ],
448 };
449
450 my $pve_fw_parsed_macros;
451 my $pve_fw_macro_descr;
452 my $pve_fw_preferred_macro_names = {};
453
454 my $pve_std_chains = {
455     'PVEFW-SET-ACCEPT-MARK' => [
456         "-j MARK --set-mark 1",
457     ],
458     'PVEFW-DropBroadcast' => [
459         # same as shorewall 'Broadcast'
460         # simply DROP BROADCAST/MULTICAST/ANYCAST
461         # we can use this to reduce logging
462         { action => 'DROP', dsttype => 'BROADCAST' },
463         { action => 'DROP', dsttype => 'MULTICAST' },
464         { action => 'DROP', dsttype => 'ANYCAST' },
465         { action => 'DROP', dest => '224.0.0.0/4' },
466     ],
467     'PVEFW-reject' => [
468         # same as shorewall 'reject'
469         { action => 'DROP', dsttype => 'BROADCAST' },
470         { action => 'DROP', source => '224.0.0.0/4' },
471         { action => 'DROP', proto => 'icmp' },
472         "-p tcp -j REJECT --reject-with tcp-reset",
473         "-p udp -j REJECT --reject-with icmp-port-unreachable",
474         "-p icmp -j REJECT --reject-with icmp-host-unreachable",
475         "-j REJECT --reject-with icmp-host-prohibited",
476     ],
477     'PVEFW-Drop' => [
478         # same as shorewall 'Drop', which is equal to DROP,
479         # but REJECT/DROP some packages to reduce logging,
480         # and ACCEPT critical ICMP types
481         { action => 'PVEFW-reject',  proto => 'tcp', dport => '43' }, # REJECT 'auth'
482         # we are not interested in BROADCAST/MULTICAST/ANYCAST
483         { action => 'PVEFW-DropBroadcast' },
484         # ACCEPT critical ICMP types
485         { action => 'ACCEPT', proto => 'icmp', dport => 'fragmentation-needed' },
486         { action => 'ACCEPT', proto => 'icmp', dport => 'time-exceeded' },
487         # Drop packets with INVALID state
488         "-m conntrack --ctstate INVALID -j DROP",
489         # Drop Microsoft SMB noise
490         { action => 'DROP', proto => 'udp', dport => '135,445', nbdport => 2 },
491         { action => 'DROP', proto => 'udp', dport => '137:139'},
492         { action => 'DROP', proto => 'udp', dport => '1024:65535', sport => 137 },
493         { action => 'DROP', proto => 'tcp', dport => '135,139,445', nbdport => 3 },
494         { action => 'DROP', proto => 'udp', dport => 1900 }, # UPnP
495         # Drop new/NotSyn traffic so that it doesn't get logged
496         "-p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -j DROP",
497         # Drop DNS replies
498         { action => 'DROP', proto => 'udp', sport => 53 },
499     ],
500     'PVEFW-Reject' => [
501         # same as shorewall 'Reject', which is equal to Reject,
502         # but REJECT/DROP some packages to reduce logging,
503         # and ACCEPT critical ICMP types
504         { action => 'PVEFW-reject',  proto => 'tcp', dport => '43' }, # REJECT 'auth'
505         # we are not interested in BROADCAST/MULTICAST/ANYCAST
506         { action => 'PVEFW-DropBroadcast' },
507         # ACCEPT critical ICMP types
508         { action => 'ACCEPT', proto => 'icmp', dport => 'fragmentation-needed' },
509         { action => 'ACCEPT', proto => 'icmp', dport => 'time-exceeded' },
510         # Drop packets with INVALID state
511         "-m conntrack --ctstate INVALID -j DROP",
512         # Drop Microsoft SMB noise
513         { action => 'PVEFW-reject', proto => 'udp', dport => '135,445', nbdport => 2 },
514         { action => 'PVEFW-reject', proto => 'udp', dport => '137:139'},
515         { action => 'PVEFW-reject', proto => 'udp', dport => '1024:65535', sport => 137 },
516         { action => 'PVEFW-reject', proto => 'tcp', dport => '135,139,445', nbdport => 3 },
517         { action => 'DROP', proto => 'udp', dport => 1900 }, # UPnP
518         # Drop new/NotSyn traffic so that it doesn't get logged
519         "-p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -j DROP",
520         # Drop DNS replies
521         { action => 'DROP', proto => 'udp', sport => 53 },
522     ],
523     'PVEFW-tcpflags' => [
524         # same as shorewall tcpflags action.
525         # Packets arriving on this interface are checked for som illegal combinations of TCP flags
526         "-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG FIN,PSH,URG -g PVEFW-logflags",
527         "-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE -g PVEFW-logflags",
528         "-p tcp -m tcp --tcp-flags SYN,RST SYN,RST -g PVEFW-logflags",
529         "-p tcp -m tcp --tcp-flags FIN,SYN FIN,SYN -g PVEFW-logflags",
530         "-p tcp -m tcp --sport 0 --tcp-flags FIN,SYN,RST,ACK SYN -g PVEFW-logflags",
531     ],
532     'PVEFW-smurfs' => [
533         # same as shorewall smurfs action
534         # Filter packets for smurfs (packets with a broadcast address as the source).
535         "-s 0.0.0.0/32 -j RETURN",
536         "-m addrtype --src-type BROADCAST -g PVEFW-smurflog",
537         "-s 224.0.0.0/4 -g PVEFW-smurflog",
538     ],
539 };
540
541 # iptables -p icmp -h
542 my $icmp_type_names = {
543     any => 1,
544     'echo-reply' => 1,
545     'destination-unreachable' => 1,
546     'network-unreachable' => 1,
547     'host-unreachable' => 1,
548     'protocol-unreachable' => 1,
549     'port-unreachable' => 1,
550     'fragmentation-needed' => 1,
551     'source-route-failed' => 1,
552     'network-unknown' => 1,
553     'host-unknown' => 1,
554     'network-prohibited' => 1,
555     'host-prohibited' => 1,
556     'TOS-network-unreachable' => 1,
557     'TOS-host-unreachable' => 1,
558     'communication-prohibited' => 1,
559     'host-precedence-violation' => 1,
560     'precedence-cutoff' => 1,
561     'source-quench' => 1,
562     'redirect' => 1,
563     'network-redirect' => 1,
564     'host-redirect' => 1,
565     'TOS-network-redirect' => 1,
566     'TOS-host-redirect' => 1,
567     'echo-request' => 1,
568     'router-advertisement' => 1,
569     'router-solicitation' => 1,
570     'time-exceeded' => 1,
571     'ttl-zero-during-transit' => 1,
572     'ttl-zero-during-reassembly' => 1,
573     'parameter-problem' => 1,
574     'ip-header-bad' => 1,
575     'required-option-missing' => 1,
576     'timestamp-request' => 1,
577     'timestamp-reply' => 1,
578     'address-mask-request' => 1,
579     'address-mask-reply' => 1,
580 };
581
582 sub init_firewall_macros {
583
584     $pve_fw_parsed_macros = {};
585
586     foreach my $k (keys %$pve_fw_macros) {
587         my $lc_name = lc($k);
588         my $macro = $pve_fw_macros->{$k};
589         if (!ref($macro->[0])) {
590             $pve_fw_macro_descr->{$k} = shift @$macro;
591         }
592         $pve_fw_preferred_macro_names->{$lc_name} = $k;
593         $pve_fw_parsed_macros->{$k} = $macro;
594     }
595 }
596
597 init_firewall_macros();
598
599 sub get_macros {
600     return wantarray ? ($pve_fw_parsed_macros, $pve_fw_macro_descr): $pve_fw_parsed_macros;
601 }
602
603 my $etc_services;
604
605 sub get_etc_services {
606
607     return $etc_services if $etc_services;
608
609     my $filename = "/etc/services";
610
611     my $fh = IO::File->new($filename, O_RDONLY);
612     if (!$fh) {
613         warn "unable to read '$filename' - $!\n";
614         return {};
615     }
616
617     my $services = {};
618
619     while (my $line = <$fh>) {
620         chomp ($line);
621         next if $line =~m/^#/;
622         next if ($line =~m/^\s*$/);
623
624         if ($line =~ m!^(\S+)\s+(\S+)/(tcp|udp).*$!) {
625             $services->{byid}->{$2}->{name} = $1;
626             $services->{byid}->{$2}->{port} = $2;
627             $services->{byid}->{$2}->{$3} = 1;
628             $services->{byname}->{$1} = $services->{byid}->{$2};
629         }
630     }
631
632     close($fh);
633
634     $etc_services = $services;
635
636
637     return $etc_services;
638 }
639
640 my $etc_protocols;
641
642 sub get_etc_protocols {
643     return $etc_protocols if $etc_protocols;
644
645     my $filename = "/etc/protocols";
646
647     my $fh = IO::File->new($filename, O_RDONLY);
648     if (!$fh) {
649         warn "unable to read '$filename' - $!\n";
650         return {};
651     }
652
653     my $protocols = {};
654
655     while (my $line = <$fh>) {
656         chomp ($line);
657         next if $line =~m/^#/;
658         next if ($line =~m/^\s*$/);
659
660         if ($line =~ m!^(\S+)\s+(\d+)\s+.*$!) {
661             $protocols->{byid}->{$2}->{name} = $1;
662             $protocols->{byname}->{$1} = $protocols->{byid}->{$2};
663         }
664     }
665
666     close($fh);
667
668     $etc_protocols = $protocols;
669
670     return $etc_protocols;
671 }
672
673 sub parse_address_list {
674     my ($str) = @_;
675
676     return if $str !~ m/^(\+)(\S+)$/; # ipset ref
677
678     my $count = 0;
679     my $iprange = 0;
680     foreach my $elem (split(/,/, $str)) {
681         $count++;
682         if (!Net::IP->new($elem)) {
683             my $err = Net::IP::Error();
684             die "invalid IP address: $err\n";
685         }
686         $iprange = 1 if $elem =~ m/-/;
687     }
688     
689     die "you can use a range in a list\n" if $iprange && $count > 1;
690 }
691
692 sub parse_port_name_number_or_range {
693     my ($str) = @_;
694
695     my $services = PVE::Firewall::get_etc_services();
696     my $count = 0;
697     foreach my $item (split(/,/, $str)) {
698         $count++;
699         if ($item =~ m/^(\d+):(\d+)$/) {
700             my ($port1, $port2) = ($1, $2);
701             die "invalid port '$port1'\n" if $port1 > 65535;
702             die "invalid port '$port2'\n" if $port2 > 65535;
703         } elsif ($item =~ m/^(\d+)$/) {
704             my $port = $1;
705             die "invalid port '$port'\n" if $port > 65535;
706         } else {
707             die "invalid port '$item'\n" if !$services->{byname}->{$item}; 
708         }
709     }
710
711     return $count;
712 }
713
714 PVE::JSONSchema::register_format('pve-fw-port-spec', \&pve_fw_verify_port_spec);
715 sub pve_fw_verify_port_spec {
716    my ($portstr) = @_;
717
718    parse_port_name_number_or_range($portstr);
719
720    return $portstr;
721 }
722
723 PVE::JSONSchema::register_format('pve-fw-v4addr-spec', \&pve_fw_verify_v4addr_spec);
724 sub pve_fw_verify_v4addr_spec {
725    my ($list) = @_;
726
727    parse_address_list($list);
728
729    return $list;
730 }
731
732 PVE::JSONSchema::register_format('pve-fw-protocol-spec', \&pve_fw_verify_protocol_spec);
733 sub pve_fw_verify_protocol_spec {
734    my ($proto) = @_;
735
736    my $protocols = get_etc_protocols();
737
738    die "unknown protocol '$proto'\n" if $proto &&
739        !(defined($protocols->{byname}->{$proto}) ||
740          defined($protocols->{byid}->{$proto}));
741
742    return $proto;
743 }
744
745
746 # helper function for API
747
748 my $rule_properties = {
749     pos => {
750         description => "Update rule at position <pos>.",
751         type => 'integer',
752         minimum => 0,
753         optional => 1,
754     },
755     digest => {
756         type => 'string',
757         optional => 1,
758         maxLength => 27,
759         minLength => 27,
760     },
761     type => {
762         type => 'string',
763         optional => 1,
764         enum => ['in', 'out', 'group'],
765     },
766     action => {
767         type => 'string',
768         optional => 1,
769         enum => ['ACCEPT', 'DROP', 'REJECT'],
770     },
771     macro => {
772         type => 'string',
773         optional => 1,
774         maxLength => 128,
775     },
776     iface => get_standard_option('pve-iface', { optional => 1 }),
777     source => {
778         type => 'string', format => 'pve-fw-v4addr-spec',
779         optional => 1,
780     },
781     dest => {
782         type => 'string', format => 'pve-fw-v4addr-spec',
783         optional => 1,
784     },
785     proto => {
786         type => 'string', format => 'pve-fw-protocol-spec',
787         optional => 1,
788     },
789     enable => {
790         type => 'boolean',
791         optional => 1,
792     },
793     sport => {
794         type => 'string', format => 'pve-fw-port-spec',
795         optional => 1,
796     },
797     dport => {
798         type => 'string', format => 'pve-fw-port-spec',
799         optional => 1,
800     },
801     comment => {
802         type => 'string',
803         optional => 1,
804     },
805 };
806
807 sub cleanup_fw_rule {
808     my ($rule, $digest, $pos) = @_;
809
810     my $r = {};
811
812     foreach my $k (keys %$rule) {
813         next if !$rule_properties->{$k};
814         my $v = $rule->{$k};
815         next if !defined($v);
816         $r->{$k} = $v;
817         $r->{digest} = $digest;
818         $r->{pos} = $pos;
819     }
820
821     return $r;
822 }
823
824 sub add_rule_properties {
825     my ($properties) = @_;
826
827     foreach my $k (keys %$rule_properties) {
828         my $h = $rule_properties->{$k};
829         # copy data, so that we can modify later without side effects
830         foreach my $opt (keys %$h) { $properties->{$k}->{$opt} = $h->{$opt}; }
831     }
832
833     return $properties;
834 }
835
836 sub delete_rule_properties {
837     my ($rule, $delete_str) = @_;
838     
839     foreach my $opt (PVE::Tools::split_list($delete_str)) {
840         raise_param_exc({ 'delete' => "no such property ('$opt')"})
841             if !defined($rule_properties->{$opt});
842         raise_param_exc({ 'delete' => "unable to delete required property '$opt'"})
843             if $opt eq 'type' || $opt eq 'action';
844         delete $rule->{$opt};
845     }
846
847     return $rule;
848 }
849
850 sub verify_rule {
851     my ($rule, $allow_groups) = @_;
852
853     my $type = $rule->{type};
854
855     raise_param_exc({ type => "missing property"}) if !$type;
856     raise_param_exc({ action => "missing property"}) if !$rule->{action};
857  
858     if ($type eq  'in' || $type eq 'out') {
859         raise_param_exc({ action => "unknown action '$rule->{action}'"})
860             if $rule->{action} !~ m/^(ACCEPT|DROP|REJECT)$/;
861     } elsif ($type eq 'group') {
862         raise_param_exc({ type => "security groups not allowed"}) 
863             if !$allow_groups;
864         raise_param_exc({ action => "invalid characters in security group name"}) 
865             if $rule->{action} !~ m/^[A-Za-z0-9_\-]+$/;
866     } else {
867         raise_param_exc({ type => "unknown rule type '$type'"});
868     }
869    
870     # fixme: verify $rule->{iface}?
871
872     if ($rule->{macro}) {
873         my $preferred_name = $pve_fw_preferred_macro_names->{lc($rule->{macro})};
874         raise_param_exc({ macro => "unknown macro '$rule->{macro}'"}) if !$preferred_name;
875         $rule->{macro} = $preferred_name;
876     }
877
878     if ($rule->{dport}) {
879         eval { parse_port_name_number_or_range($rule->{dport}); };
880         raise_param_exc({ dport => $@ }) if $@;
881     }
882
883     if ($rule->{sport}) {
884         eval { parse_port_name_number_or_range($rule->{sport}); };
885         raise_param_exc({ sport => $@ }) if $@;
886     }
887
888     if ($rule->{source}) {
889         eval { parse_address_list($rule->{source}); };
890         raise_param_exc({ source => $@ }) if $@;
891     }
892
893     if ($rule->{dest}) {
894         eval { parse_address_list($rule->{dest}); };
895         raise_param_exc({ dest => $@ }) if $@;
896     }
897
898     return $rule;
899 }
900
901 sub copy_rule_data {
902     my ($rule, $param) = @_;
903
904     foreach my $k (keys %$rule_properties) {
905         if (defined(my $v = $param->{$k})) {
906             if ($v eq '' || $v eq '-') {
907                 delete $rule->{$k};
908             } else {
909                 $rule->{$k} = $v;
910             }
911         } else {
912             delete $rule->{$k};
913         }
914     }
915
916     # verify rule now
917
918     return $rule;
919 }
920
921 # core functions
922 my $bridge_firewall_enabled = 0;
923
924 sub enable_bridge_firewall {
925
926     return if $bridge_firewall_enabled; # only once
927
928     PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-iptables", "1");
929     PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-ip6tables", "1");
930
931     # make sure syncookies are enabled (which is default on newer 3.X kernels anyways)
932     PVE::ProcFSTools::write_proc_entry("/proc/sys/net/ipv4/tcp_syncookies", "1");
933
934     $bridge_firewall_enabled = 1;
935 }
936
937 my $rule_format = "%-15s %-30s %-30s %-15s %-15s %-15s\n";
938
939 sub iptables_restore_cmdlist {
940     my ($cmdlist) = @_;
941
942     run_command("/sbin/iptables-restore -n", input => $cmdlist);
943 }
944
945 sub ipset_restore_cmdlist {
946     my ($cmdlist) = @_;
947
948     run_command("/usr/sbin/ipset restore", input => $cmdlist);
949 }
950
951 sub iptables_get_chains {
952
953     my $res = {};
954
955     # check what chains we want to track
956     my $is_pvefw_chain = sub {
957         my $name = shift;
958
959         return 1 if $name =~ m/^PVEFW-\S+$/;
960
961         return 1 if $name =~ m/^tap\d+i\d+-(:?IN|OUT)$/;
962
963         return 1 if $name =~ m/^veth\d+.\d+-(:?IN|OUT)$/; # fixme: dev name is configurable
964
965         return 1 if $name =~ m/^venet0-\d+-(:?IN|OUT)$/;
966
967         return 1 if $name =~ m/^vmbr\d+-(:?FW|IN|OUT|IPS)$/;
968         return 1 if $name =~ m/^GROUP-(:?[^\s\-]+)-(:?IN|OUT)$/;
969
970         return undef;
971     };
972
973     my $table = '';
974
975     my $hooks = {};
976
977     my $parser = sub {
978         my $line = shift;
979
980         return if $line =~ m/^#/;
981         return if $line =~ m/^\s*$/;
982
983         if ($line =~ m/^\*(\S+)$/) {
984             $table = $1;
985             return;
986         }
987
988         return if $table ne 'filter';
989
990         if ($line =~ m/^:(\S+)\s/) {
991             my $chain = $1;
992             return if !&$is_pvefw_chain($chain);
993             $res->{$chain} = "unknown";
994         } elsif ($line =~ m/^-A\s+(\S+)\s.*--comment\s+\"PVESIG:(\S+)\"/) {
995             my ($chain, $sig) = ($1, $2);
996             return if !&$is_pvefw_chain($chain);
997             $res->{$chain} = $sig;
998         } elsif ($line =~ m/^-A\s+(INPUT|OUTPUT|FORWARD)\s+-j\s+PVEFW-\1$/) {
999             $hooks->{$1} = 1;
1000         } else {
1001             # simply ignore the rest
1002             return;
1003         }
1004     };
1005
1006     run_command("/sbin/iptables-save", outfunc => $parser);
1007
1008     return wantarray ? ($res, $hooks) : $res;
1009 }
1010
1011 sub iptables_chain_digest {
1012     my ($rules) = @_;
1013     my $digest = Digest::SHA->new('sha1');
1014     foreach my $rule (@$rules) { # order is important
1015         $digest->add($rule);
1016     }
1017     return $digest->b64digest;
1018 }
1019
1020 sub ipset_chain_digest {
1021     my ($rules) = @_;
1022
1023     my $digest = Digest::SHA->new('sha1');
1024     foreach my $rule (sort @$rules) { # note: sorted
1025         $digest->add($rule);
1026     }
1027     return $digest->b64digest;
1028 }
1029
1030 sub ipset_get_chains {
1031
1032     my $res = {};
1033     my $chains = {};
1034
1035     my $parser = sub {
1036         my $line = shift;
1037
1038         return if $line =~ m/^#/;
1039         return if $line =~ m/^\s*$/;
1040         if ($line =~ m/^(?:\S+)\s(PVEFW-\S+)\s(?:\S+).*/) {
1041             my $chain = $1;
1042             $line =~ s/\s+$//; # delete trailing white space
1043             push @{$chains->{$chain}}, $line;
1044         } else {
1045             # simply ignore the rest
1046             return;
1047         }
1048     };
1049
1050     run_command("/usr/sbin/ipset save", outfunc => $parser);
1051
1052     # compute digest for each chain
1053     foreach my $chain (keys %$chains) {
1054         $res->{$chain} = ipset_chain_digest($chains->{$chain});
1055     }
1056
1057     return $res;
1058 }
1059
1060 sub ruleset_generate_cmdstr {
1061     my ($ruleset, $chain, $rule, $actions, $goto, $cluster_conf) = @_;
1062
1063     return if defined($rule->{enable}) && !$rule->{enable};
1064
1065     die "unable to emit macro - internal error" if $rule->{macro}; # should not happen
1066
1067     my $nbdport = defined($rule->{dport}) ? parse_port_name_number_or_range($rule->{dport}) : 0;
1068     my $nbsport = defined($rule->{sport}) ? parse_port_name_number_or_range($rule->{sport}) : 0;
1069
1070     my @cmd = ();
1071
1072     push @cmd, "-i $rule->{iface_in}" if $rule->{iface_in};
1073     push @cmd, "-o $rule->{iface_out}" if $rule->{iface_out};
1074
1075     my $source = $rule->{source};
1076     my $dest = $rule->{dest};
1077
1078     if ($source) {
1079         if ($source =~ m/^(\+)(\S+)$/) {
1080             die "no such ipset $2" if !$cluster_conf->{ipset}->{$2};
1081             push @cmd, "-m set --match-set PVEFW-$2 src";
1082
1083         } elsif ($source =~ m/\-/){
1084             push @cmd, "-m iprange --src-range $source";
1085
1086         } else {
1087             push @cmd, "-s $source";
1088         }
1089     }
1090
1091     if ($dest) {
1092         if ($dest =~ m/^(\+)(\S+)$/) {
1093             die "no such ipset $2" if !$cluster_conf->{ipset}->{$2};
1094             push @cmd, "-m set --match-set PVEFW-$2 dst";
1095
1096         } elsif ($dest =~ m/^(\d+)\.(\d+).(\d+).(\d+)\-(\d+)\.(\d+).(\d+).(\d+)$/){
1097             push @cmd, "-m iprange --dst-range $dest";
1098
1099         } else {
1100             push @cmd, "-s $dest";
1101         }
1102     }
1103
1104     if ($rule->{proto}) {
1105         push @cmd, "-p $rule->{proto}";
1106
1107         my $multiport = 0;
1108         $multiport++ if $nbdport > 1;
1109         $multiport++ if $nbsport > 1;
1110
1111         push @cmd, "--match multiport" if $multiport;
1112
1113         die "multiport: option '--sports' cannot be used together with '--dports'\n"
1114             if ($multiport == 2) && ($rule->{dport} ne $rule->{sport});
1115
1116         if ($rule->{dport}) {
1117             if ($rule->{proto} && $rule->{proto} eq 'icmp') {
1118                 # Note: we use dport to store --icmp-type
1119                 die "unknown icmp-type '$rule->{dport}'\n" if !defined($icmp_type_names->{$rule->{dport}});
1120                 push @cmd, "-m icmp --icmp-type $rule->{dport}";
1121             } else {
1122                 if ($nbdport > 1) {
1123                     if ($multiport == 2) {
1124                         push @cmd,  "--ports $rule->{dport}";
1125                     } else {
1126                         push @cmd, "--dports $rule->{dport}";
1127                     }
1128                 } else {
1129                     push @cmd, "--dport $rule->{dport}";
1130                 }
1131             }
1132         }
1133
1134         if ($rule->{sport}) {
1135             if ($nbsport > 1) {
1136                 push @cmd, "--sports $rule->{sport}" if $multiport != 2;
1137             } else {
1138                 push @cmd, "--sport $rule->{sport}";
1139             }
1140         }
1141     } elsif ($rule->{dport} || $rule->{sport}) {
1142         warn "ignoring destination port '$rule->{dport}' - no protocol specified\n" if $rule->{dport};
1143         warn "ignoring source port '$rule->{sport}' - no protocol specified\n" if $rule->{sport};
1144     }
1145
1146     push @cmd, "-m addrtype --dst-type $rule->{dsttype}" if $rule->{dsttype};
1147
1148     if (my $action = $rule->{action}) {
1149         $action = $actions->{$action} if defined($actions->{$action});
1150         $goto = 1 if !defined($goto) && $action eq 'PVEFW-SET-ACCEPT-MARK';
1151         push @cmd, $goto ? "-g $action" : "-j $action";
1152     }
1153
1154     return scalar(@cmd) ? join(' ', @cmd) : undef;
1155 }
1156
1157 my $apply_macro = sub {
1158     my ($macro_name, $param) = @_;
1159
1160     my $macro_rules = $pve_fw_parsed_macros->{$macro_name};
1161     die "unknown macro '$macro_name'\n" if !$macro_rules; # should not happen
1162
1163     my $rules = [];
1164
1165     foreach my $templ (@$macro_rules) {
1166         my $rule = {};
1167         my $param_used = {};
1168         foreach my $k (keys %$templ) {
1169             my $v = $templ->{$k};
1170             if ($v eq 'PARAM') {
1171                 $v = $param->{$k};
1172                 $param_used->{$k} = 1;
1173             } elsif ($v eq 'DEST') {
1174                 $v = $param->{dest};
1175                 $param_used->{dest} = 1;
1176             } elsif ($v eq 'SOURCE') {
1177                 $v = $param->{source};
1178                 $param_used->{source} = 1;
1179             }
1180
1181             die "missing parameter '$k' in macro '$macro_name'\n" if !defined($v);
1182             $rule->{$k} = $v;
1183         }
1184         foreach my $k (keys %$param) {
1185             next if $k eq 'macro';
1186             next if !defined($param->{$k});
1187             next if $param_used->{$k};
1188             if (defined($rule->{$k})) {
1189                 die "parameter '$k' already define in macro (value = '$rule->{$k}')\n"
1190                     if $rule->{$k} ne $param->{$k};
1191             } else {
1192                 $rule->{$k} = $param->{$k};
1193             }
1194         }
1195         push @$rules, $rule;
1196     }
1197
1198     return $rules;
1199 };
1200
1201 sub ruleset_generate_rule {
1202     my ($ruleset, $chain, $rule, $actions, $goto, $cluster_conf) = @_;
1203
1204     my $rules;
1205
1206     if ($rule->{macro}) {
1207         $rules = &$apply_macro($rule->{macro}, $rule);
1208     } else {
1209         $rules = [ $rule ];
1210     }
1211
1212     foreach my $tmp (@$rules) {
1213         if (my $cmdstr = ruleset_generate_cmdstr($ruleset, $chain, $tmp, $actions, $goto, $cluster_conf)) {
1214             ruleset_addrule($ruleset, $chain, $cmdstr);
1215         }
1216     }
1217 }
1218
1219 sub ruleset_generate_rule_insert {
1220     my ($ruleset, $chain, $rule, $actions, $goto) = @_;
1221
1222     die "implement me" if $rule->{macro}; # not implemented, because not needed so far
1223
1224     if (my $cmdstr = ruleset_generate_cmdstr($ruleset, $chain, $rule, $actions, $goto)) {
1225         ruleset_insertrule($ruleset, $chain, $cmdstr);
1226     }
1227 }
1228
1229 sub ruleset_create_chain {
1230     my ($ruleset, $chain) = @_;
1231
1232     die "Invalid chain name '$chain' (28 char max)\n" if length($chain) > 28;
1233     die "chain name may not contain collons\n" if $chain =~ m/:/; # because of log format
1234
1235     die "chain '$chain' already exists\n" if $ruleset->{$chain};
1236
1237     $ruleset->{$chain} = [];
1238 }
1239
1240 sub ruleset_chain_exist {
1241     my ($ruleset, $chain) = @_;
1242
1243     return $ruleset->{$chain} ? 1 : undef;
1244 }
1245
1246 sub ruleset_addrule {
1247    my ($ruleset, $chain, $rule) = @_;
1248
1249    die "no such chain '$chain'\n" if !$ruleset->{$chain};
1250
1251    push @{$ruleset->{$chain}}, "-A $chain $rule";
1252 }
1253
1254 sub ruleset_insertrule {
1255    my ($ruleset, $chain, $rule) = @_;
1256
1257    die "no such chain '$chain'\n" if !$ruleset->{$chain};
1258
1259    unshift @{$ruleset->{$chain}}, "-A $chain $rule";
1260 }
1261
1262 sub get_log_rule_base {
1263     my ($chain, $vmid, $msg, $loglevel) = @_;
1264
1265     die "internal error - no log level" if !defined($loglevel);
1266
1267     $vmid = 0 if !defined($vmid);
1268
1269     # Note: we use special format for prefix to pass further
1270     # info to log daemon (VMID, LOGVELEL and CHAIN)
1271
1272     return "-j NFLOG --nflog-prefix \":$vmid:$loglevel:$chain: $msg\"";
1273 }
1274
1275 sub ruleset_addlog {
1276     my ($ruleset, $chain, $vmid, $msg, $loglevel, $rule) = @_;
1277
1278     return if !defined($loglevel);
1279
1280     my $logrule = get_log_rule_base($chain, $vmid, $msg, $loglevel);
1281
1282     $logrule = "$rule $logrule" if defined($rule);
1283
1284     ruleset_addrule($ruleset, $chain, $logrule)
1285 }
1286
1287 sub generate_bridge_chains {
1288     my ($ruleset, $hostfw_conf, $bridge, $routing_table) = @_;
1289
1290     my $options = $hostfw_conf->{options} || {};
1291
1292     die "error: detected direct route to bridge '$bridge'\n"
1293         if !$options->{allow_bridge_route} && $routing_table->{$bridge};
1294
1295     if (!ruleset_chain_exist($ruleset, "$bridge-FW")) {
1296         ruleset_create_chain($ruleset, "$bridge-FW");
1297         ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o $bridge -m physdev --physdev-is-out -j $bridge-FW");
1298         ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i $bridge -m physdev --physdev-is-in -j $bridge-FW");
1299     }
1300
1301     if (!ruleset_chain_exist($ruleset, "$bridge-OUT")) {
1302         ruleset_create_chain($ruleset, "$bridge-OUT");
1303         ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-in -j $bridge-OUT");
1304         ruleset_insertrule($ruleset, "PVEFW-INPUT", "-i $bridge -m physdev --physdev-is-in -j $bridge-OUT");
1305     }
1306
1307     if (!ruleset_chain_exist($ruleset, "$bridge-IN")) {
1308         ruleset_create_chain($ruleset, "$bridge-IN");
1309         ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-out -j $bridge-IN");
1310         ruleset_addrule($ruleset, "$bridge-FW", "-m mark --mark 1 -j ACCEPT");
1311         # accept traffic to unmanaged bridge ports
1312         ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-out -j ACCEPT ");
1313     }
1314 }
1315
1316 sub ruleset_add_chain_policy {
1317     my ($ruleset, $chain, $vmid, $policy, $loglevel, $accept_action) = @_;
1318
1319     if ($policy eq 'ACCEPT') {
1320
1321         ruleset_generate_rule($ruleset, $chain, { action => 'ACCEPT' },
1322                               { ACCEPT =>  $accept_action});
1323
1324     } elsif ($policy eq 'DROP') {
1325
1326         ruleset_addrule($ruleset, $chain, "-j PVEFW-Drop");
1327
1328         ruleset_addlog($ruleset, $chain, $vmid, "policy $policy: ", $loglevel);
1329
1330         ruleset_addrule($ruleset, $chain, "-j DROP");
1331     } elsif ($policy eq 'REJECT') {
1332         ruleset_addrule($ruleset, $chain, "-j PVEFW-Reject");
1333
1334         ruleset_addlog($ruleset, $chain, $vmid, "policy $policy: ", $loglevel);
1335
1336         ruleset_addrule($ruleset, $chain, "-g PVEFW-reject");
1337     } else {
1338         # should not happen
1339         die "internal error: unknown policy '$policy'";
1340     }
1341 }
1342
1343 sub ruleset_create_vm_chain {
1344     my ($ruleset, $chain, $options, $macaddr, $direction) = @_;
1345
1346     ruleset_create_chain($ruleset, $chain);
1347     my $accept = generate_nfqueue($options);
1348
1349     if (!(defined($options->{nosmurfs}) && $options->{nosmurfs} == 0)) {
1350         ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID,NEW -j PVEFW-smurfs");
1351     }
1352
1353     if (!(defined($options->{dhcp}) && $options->{dhcp} == 0)) {
1354         if ($direction eq 'OUT') {
1355             ruleset_generate_rule($ruleset, $chain, { action => 'PVEFW-SET-ACCEPT-MARK',
1356                                                       proto => 'udp', sport => 68, dport => 67 });
1357         } else {
1358             ruleset_generate_rule($ruleset, $chain, { action => 'ACCEPT',
1359                                                       proto => 'udp', sport => 67, dport => 68 });
1360         }
1361     }
1362
1363     if ($options->{tcpflags}) {
1364         ruleset_addrule($ruleset, $chain, "-p tcp -j PVEFW-tcpflags");
1365     }
1366
1367     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID -j DROP");
1368     if ($direction eq 'OUT') {
1369         ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED -g PVEFW-SET-ACCEPT-MARK");
1370     } else {
1371         ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED -j $accept");
1372     }
1373
1374     if ($direction eq 'OUT') {
1375         if (defined($macaddr) && !(defined($options->{macfilter}) && $options->{macfilter} == 0)) {
1376             ruleset_addrule($ruleset, $chain, "-m mac ! --mac-source $macaddr -j DROP");
1377         }
1378         ruleset_addrule($ruleset, $chain, "-j MARK --set-mark 0"); # clear mark
1379     }
1380 }
1381
1382 sub ruleset_generate_vm_rules {
1383     my ($ruleset, $rules, $cluster_conf, $chain, $netid, $direction, $options) = @_;
1384
1385     my $lc_direction = lc($direction);
1386
1387     foreach my $rule (@$rules) {
1388         next if $rule->{iface} && $rule->{iface} ne $netid;
1389         next if !$rule->{enable};
1390         if ($rule->{type} eq 'group') {
1391             my $group_chain = "GROUP-$rule->{action}-$direction";
1392             if(!ruleset_chain_exist($ruleset, $group_chain)){
1393                 generate_group_rules($ruleset, $cluster_conf, $rule->{action});
1394             }
1395             ruleset_addrule($ruleset, $chain, "-j $group_chain");
1396             if ($direction eq 'OUT'){
1397                 ruleset_addrule($ruleset, $chain, "-m mark --mark 1 -j RETURN");
1398             }else{
1399                 my $accept = generate_nfqueue($options);
1400                 ruleset_addrule($ruleset, $chain, "-m mark --mark 1 -j $accept");
1401             }
1402
1403         } else {
1404             next if $rule->{type} ne $lc_direction;
1405             if ($direction eq 'OUT') {
1406                 ruleset_generate_rule($ruleset, $chain, $rule,
1407                                       { ACCEPT => "PVEFW-SET-ACCEPT-MARK", REJECT => "PVEFW-reject" }, undef, $cluster_conf);
1408             } else {
1409                 my $accept = generate_nfqueue($options);
1410                 ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept , REJECT => "PVEFW-reject" }, undef, $cluster_conf);
1411             }
1412         }
1413     }
1414 }
1415
1416 sub generate_nfqueue {
1417     my ($options) = @_;
1418
1419     my $action = "";
1420     if($options->{ips}){
1421         $action = "NFQUEUE";
1422         if($options->{ips_queues} && $options->{ips_queues} =~ m/^(\d+)(:(\d+))?$/) {
1423             if(defined($3) && defined($1)) {
1424                 $action .= " --queue-balance $1:$3";
1425             }elsif (defined($1)) {
1426                 $action .= " --queue-num $1";
1427             }
1428         }
1429         $action .= " --queue-bypass";
1430     }else{
1431         $action = "ACCEPT";
1432     }
1433
1434     return $action;
1435 }
1436
1437 sub ruleset_generate_vm_ipsrules {
1438     my ($ruleset, $options, $direction, $iface, $bridge) = @_;
1439
1440     if ($options->{ips} && $direction eq 'IN') {
1441         my $nfqueue = generate_nfqueue($options);
1442
1443         if (!ruleset_chain_exist($ruleset, "$bridge-IPS")) {
1444             ruleset_create_chain($ruleset, "PVEFW-IPS");
1445         }
1446
1447         if (!ruleset_chain_exist($ruleset, "$bridge-IPS")) {
1448             ruleset_create_chain($ruleset, "$bridge-IPS");
1449             ruleset_insertrule($ruleset, "PVEFW-IPS", "-o $bridge -m physdev --physdev-is-out -j $bridge-IPS");
1450         }
1451
1452         ruleset_addrule($ruleset, "$bridge-IPS", "-m physdev --physdev-out $iface --physdev-is-bridged -j $nfqueue");
1453     }
1454 }
1455
1456 sub generate_venet_rules_direction {
1457     my ($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, $direction) = @_;
1458
1459     parse_address_list($ip); # make sure we have a valid $ip list
1460
1461     my $lc_direction = lc($direction);
1462
1463     my $rules = $vmfw_conf->{rules};
1464
1465     my $options = $vmfw_conf->{options};
1466     my $loglevel = get_option_log_level($options, "log_level_${lc_direction}");
1467
1468     my $chain = "venet0-$vmid-$direction";
1469
1470     ruleset_create_vm_chain($ruleset, $chain, $options, undef, $direction);
1471
1472     ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $chain, 'venet', $direction);
1473
1474     # implement policy
1475     my $policy;
1476
1477     if ($direction eq 'OUT') {
1478         $policy = $options->{policy_out} || 'ACCEPT'; # allow everything by default
1479     } else {
1480         $policy = $options->{policy_in} || 'DROP'; # allow nothing by default
1481     }
1482
1483     my $accept = generate_nfqueue($options);
1484     my $accept_action = $direction eq 'OUT' ? "PVEFW-SET-ACCEPT-MARK" : $accept;
1485     ruleset_add_chain_policy($ruleset, $chain, $vmid, $policy, $loglevel, $accept_action);
1486
1487     # plug into FORWARD, INPUT and OUTPUT chain
1488     if ($direction eq 'OUT') {
1489         ruleset_generate_rule_insert($ruleset, "PVEFW-FORWARD", {
1490             action => $chain,
1491             source => $ip,
1492             iface_in => 'venet0'});
1493
1494         ruleset_generate_rule_insert($ruleset, "PVEFW-INPUT", {
1495             action => $chain,
1496             source => $ip,
1497             iface_in => 'venet0'});
1498     } else {
1499         ruleset_generate_rule($ruleset, "PVEFW-FORWARD", {
1500             action => $chain,
1501             dest => $ip,
1502             iface_out => 'venet0'});
1503
1504         ruleset_generate_rule($ruleset, "PVEFW-OUTPUT", {
1505             action => $chain,
1506             dest => $ip,
1507             iface_out => 'venet0'});
1508     }
1509 }
1510
1511 sub generate_tap_rules_direction {
1512     my ($ruleset, $cluster_conf, $iface, $netid, $macaddr, $vmfw_conf, $vmid, $bridge, $direction) = @_;
1513
1514     my $lc_direction = lc($direction);
1515
1516     my $rules = $vmfw_conf->{rules};
1517
1518     my $options = $vmfw_conf->{options};
1519     my $loglevel = get_option_log_level($options, "log_level_${lc_direction}");
1520
1521     my $tapchain = "$iface-$direction";
1522
1523     ruleset_create_vm_chain($ruleset, $tapchain, $options, $macaddr, $direction);
1524
1525     ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $tapchain, $netid, $direction, $options);
1526
1527     ruleset_generate_vm_ipsrules($ruleset, $options, $direction, $iface, $bridge);
1528
1529     # implement policy
1530     my $policy;
1531
1532     if ($direction eq 'OUT') {
1533         $policy = $options->{policy_out} || 'ACCEPT'; # allow everything by default
1534     } else {
1535         $policy = $options->{policy_in} || 'DROP'; # allow nothing by default
1536     }
1537
1538     my $accept = generate_nfqueue($options);
1539     my $accept_action = $direction eq 'OUT' ? "PVEFW-SET-ACCEPT-MARK" : $accept;
1540     ruleset_add_chain_policy($ruleset, $tapchain, $vmid, $policy, $loglevel, $accept_action);
1541
1542     # plug the tap chain to bridge chain
1543     if ($direction eq 'IN') {
1544         ruleset_insertrule($ruleset, "$bridge-IN",
1545                            "-m physdev --physdev-is-bridged --physdev-out $iface -j $tapchain");
1546     } else {
1547         ruleset_insertrule($ruleset, "$bridge-OUT",
1548                            "-m physdev --physdev-in $iface -j $tapchain");
1549     }
1550 }
1551
1552 sub enable_host_firewall {
1553     my ($ruleset, $hostfw_conf, $cluster_conf) = @_;
1554
1555     # fixme: allow security groups
1556
1557     my $options = $hostfw_conf->{options};
1558     my $rules = $hostfw_conf->{rules};
1559
1560     # host inbound firewall
1561     my $chain = "PVEFW-HOST-IN";
1562     ruleset_create_chain($ruleset, $chain);
1563
1564     my $loglevel = get_option_log_level($options, "log_level_in");
1565
1566     if (!(defined($options->{nosmurfs}) && $options->{nosmurfs} == 0)) {
1567         ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID,NEW -j PVEFW-smurfs");
1568     }
1569
1570     if ($options->{tcpflags}) {
1571         ruleset_addrule($ruleset, $chain, "-p tcp -j PVEFW-tcpflags");
1572     }
1573
1574     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID -j DROP");
1575     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
1576     ruleset_addrule($ruleset, $chain, "-i lo -j ACCEPT");
1577     ruleset_addrule($ruleset, $chain, "-m addrtype --dst-type MULTICAST -j ACCEPT");
1578     ruleset_addrule($ruleset, $chain, "-p udp -m conntrack --ctstate NEW --dport 5404:5405 -j ACCEPT");
1579     ruleset_addrule($ruleset, $chain, "-p udp -m udp --dport 9000 -j ACCEPT");  #corosync
1580
1581     # we use RETURN because we need to check also tap rules
1582     my $accept_action = 'RETURN';
1583
1584     foreach my $rule (@$rules) {
1585         next if $rule->{type} ne 'in';
1586         ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" }, undef, $cluster_conf);
1587     }
1588
1589     # implement input policy
1590     my $policy = $options->{policy_in} || 'DROP'; # allow nothing by default
1591     ruleset_add_chain_policy($ruleset, $chain, 0, $policy, $loglevel, $accept_action);
1592
1593     # host outbound firewall
1594     $chain = "PVEFW-HOST-OUT";
1595     ruleset_create_chain($ruleset, $chain);
1596
1597     $loglevel = get_option_log_level($options, "log_level_out");
1598
1599     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID -j DROP");
1600     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
1601     ruleset_addrule($ruleset, $chain, "-o lo -j ACCEPT");
1602     ruleset_addrule($ruleset, $chain, "-m addrtype --dst-type MULTICAST -j ACCEPT");
1603     ruleset_addrule($ruleset, $chain, "-p udp -m conntrack --ctstate NEW --dport 5404:5405 -j ACCEPT");
1604     ruleset_addrule($ruleset, $chain, "-p udp -m udp --dport 9000 -j ACCEPT"); #corosync
1605
1606     # we use RETURN because we may want to check other thigs later
1607     $accept_action = 'RETURN';
1608
1609     foreach my $rule (@$rules) {
1610         next if $rule->{type} ne 'out';
1611         ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" }, undef, $cluster_conf);
1612     }
1613
1614     # implement output policy
1615     $policy = $options->{policy_out} || 'ACCEPT'; # allow everything by default
1616     ruleset_add_chain_policy($ruleset, $chain, 0, $policy, $loglevel, $accept_action);
1617
1618     ruleset_addrule($ruleset, "PVEFW-OUTPUT", "-j PVEFW-HOST-OUT");
1619     ruleset_addrule($ruleset, "PVEFW-INPUT", "-j PVEFW-HOST-IN");
1620 }
1621
1622 sub generate_group_rules {
1623     my ($ruleset, $cluster_conf, $group) = @_;
1624     die "no such security group '$group'\n" if !$cluster_conf->{groups}->{$group};
1625
1626     my $rules = $cluster_conf->{groups}->{$group};
1627
1628     my $chain = "GROUP-${group}-IN";
1629
1630     ruleset_create_chain($ruleset, $chain);
1631     ruleset_addrule($ruleset, $chain, "-j MARK --set-mark 0"); # clear mark
1632
1633     foreach my $rule (@$rules) {
1634         next if $rule->{type} ne 'in';
1635         ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => "PVEFW-SET-ACCEPT-MARK", REJECT => "PVEFW-reject" }, undef, $cluster_conf);
1636     }
1637
1638     $chain = "GROUP-${group}-OUT";
1639
1640     ruleset_create_chain($ruleset, $chain);
1641     ruleset_addrule($ruleset, $chain, "-j MARK --set-mark 0"); # clear mark
1642
1643     foreach my $rule (@$rules) {
1644         next if $rule->{type} ne 'out';
1645         # we use PVEFW-SET-ACCEPT-MARK (Instead of ACCEPT) because we need to
1646         # check also other tap rules later
1647         ruleset_generate_rule($ruleset, $chain, $rule,
1648                               { ACCEPT => 'PVEFW-SET-ACCEPT-MARK', REJECT => "PVEFW-reject" }, undef, $cluster_conf);
1649     }
1650 }
1651
1652 my $MAX_NETS = 32;
1653 my $valid_netdev_names = {};
1654 for (my $i = 0; $i < $MAX_NETS; $i++)  {
1655     $valid_netdev_names->{"net$i"} = 1;
1656 }
1657
1658 sub parse_fw_rule {
1659     my ($line, $need_iface, $allow_groups) = @_;
1660
1661     my ($type, $action, $iface, $source, $dest, $proto, $dport, $sport);
1662
1663     # we can add single line comments to the end of the rule
1664     my $comment = decode('utf8', $1) if $line =~ s/#\s*(.*?)\s*$//;
1665
1666     # we can disable a rule when prefixed with '|'
1667     my $enable = 1;
1668
1669     $enable = 0 if $line =~ s/^\|//;
1670
1671     my @data = split(/\s+/, $line);
1672     my $expected_elements = $need_iface ? 8 : 7;
1673
1674     die "wrong number of rule elements\n" if scalar(@data) > $expected_elements;
1675
1676     if ($need_iface) {
1677         ($type, $action, $iface, $source, $dest, $proto, $dport, $sport) = @data
1678     } else {
1679         ($type, $action, $source, $dest, $proto, $dport, $sport) =  @data;
1680     }
1681
1682     die "incomplete rule\n" if ! ($type && $action);
1683
1684     my $macro;
1685
1686     $type = lc($type);
1687
1688     if ($type eq  'in' || $type eq 'out') {
1689         if ($action =~ m/^(ACCEPT|DROP|REJECT)$/) {
1690             # OK
1691         } elsif ($action =~ m/^(\S+)\((ACCEPT|DROP|REJECT)\)$/) {
1692             $action = $2;
1693             my $preferred_name = $pve_fw_preferred_macro_names->{lc($1)};
1694             die "unknown macro '$1'\n" if !$preferred_name;
1695             $macro = $preferred_name;
1696         } else {
1697             die "unknown action '$action'\n";
1698         }
1699     } elsif ($type eq 'group') {
1700         die "wrong number of rule elements\n" if scalar(@data) != 3;
1701         die "groups disabled\n" if !$allow_groups;
1702
1703         die "invalid characters in group name\n" if $action !~ m/^[A-Za-z0-9_\-]+$/;
1704     } else {
1705         die "unknown rule type '$type'\n";
1706     }
1707
1708     if ($need_iface) {
1709         $iface = undef if $iface && $iface eq '-';
1710     }
1711
1712     $proto = undef if $proto && $proto eq '-';
1713     pve_fw_verify_protocol_spec($proto) if $proto;
1714
1715     $source = undef if $source && $source eq '-';
1716     $dest = undef if $dest && $dest eq '-';
1717
1718     $dport = undef if $dport && $dport eq '-';
1719     $sport = undef if $sport && $sport eq '-';
1720
1721     parse_port_name_number_or_range($dport) if defined($dport);
1722     parse_port_name_number_or_range($sport) if defined($sport);
1723
1724     parse_address_list($source) if $source;
1725     parse_address_list($dest) if $dest;
1726
1727     return {
1728         type => $type,
1729         enable => $enable,
1730         comment => $comment,
1731         action => $action,
1732         macro => $macro,
1733         iface => $iface,
1734         source => $source,
1735         dest => $dest,
1736         proto => $proto,
1737         dport => $dport,
1738         sport => $sport,
1739     };
1740 }
1741
1742 sub parse_vmfw_option {
1743     my ($line) = @_;
1744
1745     my ($opt, $value);
1746
1747     my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
1748
1749     if ($line =~ m/^(enable|dhcp|macfilter|nosmurfs|tcpflags|ips):\s*(0|1)\s*$/i) {
1750         $opt = lc($1);
1751         $value = int($2);
1752     } elsif ($line =~ m/^(log_level_in|log_level_out):\s*(($loglevels)\s*)?$/i) {
1753         $opt = lc($1);
1754         $value = $2 ? lc($3) : '';
1755     } elsif ($line =~ m/^(policy_(in|out)):\s*(ACCEPT|DROP|REJECT)\s*$/i) {
1756         $opt = lc($1);
1757         $value = uc($3);
1758     } elsif ($line =~ m/^(ips_queues):\s*((\d+)(:(\d+))?)\s*$/i) {
1759         $opt = lc($1);
1760         $value = $2;
1761     } else {
1762         chomp $line;
1763         die "can't parse option '$line'\n"
1764     }
1765
1766     return ($opt, $value);
1767 }
1768
1769 sub parse_hostfw_option {
1770     my ($line) = @_;
1771
1772     my ($opt, $value);
1773
1774     my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
1775
1776     if ($line =~ m/^(enable|dhcp|nosmurfs|tcpflags|allow_bridge_route|optimize):\s*(0|1)\s*$/i) {
1777         $opt = lc($1);
1778         $value = int($2);
1779     } elsif ($line =~ m/^(log_level_in|log_level_out|tcp_flags_log_level|smurf_log_level):\s*(($loglevels)\s*)?$/i) {
1780         $opt = lc($1);
1781         $value = $2 ? lc($3) : '';
1782     } elsif ($line =~ m/^(policy_(in|out)):\s*(ACCEPT|DROP|REJECT)\s*$/i) {
1783         $opt = lc($1);
1784         $value = uc($3);
1785     } elsif ($line =~ m/^(nf_conntrack_max):\s*(\d+)\s*$/i) {
1786         $opt = lc($1);
1787         $value = int($2);
1788     } else {
1789         chomp $line;
1790         die "can't parse option '$line'\n"
1791     }
1792
1793     return ($opt, $value);
1794 }
1795
1796 sub parse_clusterfw_option {
1797     my ($line) = @_;
1798
1799     my ($opt, $value);
1800
1801     if ($line =~ m/^(enable):\s*(0|1)\s*$/i) {
1802         $opt = lc($1);
1803         $value = int($2);
1804     } else {
1805         chomp $line;
1806         die "can't parse option '$line'\n"
1807     }
1808
1809     return ($opt, $value);
1810 }
1811
1812 sub parse_vm_fw_rules {
1813     my ($filename, $fh) = @_;
1814
1815     my $res = { rules => [], options => {}};
1816
1817     my $section;
1818
1819     my $digest = Digest::SHA->new('sha1');
1820
1821     while (defined(my $line = <$fh>)) {
1822         $digest->add($line);
1823
1824         next if $line =~ m/^#/;
1825         next if $line =~ m/^\s*$/;
1826
1827         my $linenr = $fh->input_line_number();
1828         my $prefix = "$filename (line $linenr)";
1829
1830         if ($line =~ m/^\[(\S+)\]\s*$/i) {
1831             $section = lc($1);
1832             warn "$prefix: ignore unknown section '$section'\n" if !$res->{$section};
1833             next;
1834         }
1835         if (!$section) {
1836             warn "$prefix: skip line - no section";
1837             next;
1838         }
1839
1840         next if !$res->{$section}; # skip undefined section
1841
1842         if ($section eq 'options') {
1843             eval {
1844                 my ($opt, $value) = parse_vmfw_option($line);
1845                 $res->{options}->{$opt} = $value;
1846             };
1847             warn "$prefix: $@" if $@;
1848             next;
1849         }
1850
1851         my $rule;
1852         eval { $rule = parse_fw_rule($line, 1, 1); };
1853         if (my $err = $@) {
1854             warn "$prefix: $err";
1855             next;
1856         }
1857
1858         push @{$res->{$section}}, $rule;
1859     }
1860
1861     $res->{digest} = $digest->b64digest;
1862
1863     return $res;
1864 }
1865
1866 sub parse_host_fw_rules {
1867     my ($filename, $fh) = @_;
1868
1869     my $res = { rules => [], options => {}};
1870
1871     my $section;
1872
1873     my $digest = Digest::SHA->new('sha1');
1874
1875     while (defined(my $line = <$fh>)) {
1876         $digest->add($line);
1877
1878         next if $line =~ m/^#/;
1879         next if $line =~ m/^\s*$/;
1880
1881         my $linenr = $fh->input_line_number();
1882         my $prefix = "$filename (line $linenr)";
1883
1884         if ($line =~ m/^\[(\S+)\]\s*$/i) {
1885             $section = lc($1);
1886             warn "$prefix: ignore unknown section '$section'\n" if !$res->{$section};
1887             next;
1888         }
1889         if (!$section) {
1890             warn "$prefix: skip line - no section";
1891             next;
1892         }
1893
1894         next if !$res->{$section}; # skip undefined section
1895
1896         if ($section eq 'options') {
1897             eval {
1898                 my ($opt, $value) = parse_hostfw_option($line);
1899                 $res->{options}->{$opt} = $value;
1900             };
1901             warn "$prefix: $@" if $@;
1902             next;
1903         }
1904
1905         my $rule;
1906         eval { $rule = parse_fw_rule($line, 1, 1); };
1907         if (my $err = $@) {
1908             warn "$prefix: $err";
1909             next;
1910         }
1911
1912         push @{$res->{$section}}, $rule;
1913     }
1914
1915 $res->{digest} = $digest->b64digest;
1916
1917     return $res;
1918 }
1919
1920 sub parse_cluster_fw_rules {
1921     my ($filename, $fh) = @_;
1922
1923     my $section;
1924     my $group;
1925
1926     my $res = { rules => [], options => {}, groups => {}, ipset => {} };
1927
1928     my $digest = Digest::SHA->new('sha1');
1929
1930     while (defined(my $line = <$fh>)) {
1931         $digest->add($line);
1932
1933         next if $line =~ m/^#/;
1934         next if $line =~ m/^\s*$/;
1935
1936         my $linenr = $fh->input_line_number();
1937         my $prefix = "$filename (line $linenr)";
1938
1939         if ($line =~ m/^\[options\]$/i) {
1940             $section = 'options';
1941             next;
1942         }
1943
1944         if ($line =~ m/^\[group\s+(\S+)\]\s*$/i) {
1945             $section = 'groups';
1946             $group = lc($1);
1947             next;
1948         }
1949
1950         if ($line =~ m/^\[rules\]$/i) {
1951             $section = 'rules';
1952             next;
1953         }
1954
1955         if ($line =~ m/^\[ipset\s+(\S+)\]\s*$/i) {
1956             $section = 'ipset';
1957             $group = lc($1);
1958             next;
1959         }
1960
1961         if (!$section) {
1962             warn "$prefix: skip line - no section\n";
1963             next;
1964         }
1965
1966         if ($section eq 'options') {
1967             eval {
1968                 my ($opt, $value) = parse_clusterfw_option($line);
1969                 $res->{options}->{$opt} = $value;
1970             };
1971             warn "$prefix: $@" if $@;
1972         } elsif ($section eq 'rules') {
1973             my $rule;
1974             eval { $rule = parse_fw_rule($line, 1, 1); };
1975             if (my $err = $@) {
1976                 warn "$prefix: $err";
1977                 next;
1978             }
1979             push @{$res->{$section}}, $rule;
1980         } elsif ($section eq 'groups') {
1981             my $rule;
1982             eval { $rule = parse_fw_rule($line, 0, 0); };
1983             if (my $err = $@) {
1984                 warn "$prefix: $err";
1985                 next;
1986             }
1987             push @{$res->{$section}->{$group}}, $rule;
1988         } elsif ($section eq 'ipset') {
1989             chomp $line;
1990             $line =~ m/^(\!)?\s*((\d+)\.(\d+)\.(\d+)\.(\d+)(\/(\d+))?)/;
1991             my $nomatch = $1;
1992         my $ip = $2;
1993
1994             if(!$ip){
1995                 warn "$prefix: $line is not an valid ip address\n";
1996                 next;
1997             }
1998             if (!Net::IP->new($ip)) {
1999                 warn "$prefix: $line is not an valid ip address\n";
2000                 next;
2001             }
2002
2003             if ($nomatch) {
2004                 if ($feature_ipset_nomatch) {
2005                     push @{$res->{$section}->{$group}}, "$ip nomatch";
2006                 } else {
2007                     warn "$prefix: ignore $line - nomatch not supported by kernel\n";
2008                 }
2009             } else {
2010                 push @{$res->{$section}->{$group}}, $ip;
2011             }
2012         }
2013     }
2014
2015     $res->{digest} = $digest->b64digest;
2016     return $res;
2017 }
2018
2019 sub run_locked {
2020     my ($code, @param) = @_;
2021
2022     my $timeout = 10;
2023
2024     my $res = lock_file($pve_fw_lock_filename, $timeout, $code, @param);
2025
2026     die $@ if $@;
2027
2028     return $res;
2029 }
2030
2031 sub read_local_vm_config {
2032
2033     my $openvz = {};
2034     my $qemu = {};
2035
2036     my $vmdata = { openvz => $openvz, qemu => $qemu };
2037
2038     my $vmlist = PVE::Cluster::get_vmlist();
2039     return $vmdata if !$vmlist || !$vmlist->{ids};
2040     my $ids = $vmlist->{ids};
2041
2042     foreach my $vmid (keys %$ids) {
2043         next if !$vmid; # skip VE0
2044         my $d = $ids->{$vmid};
2045         next if !$d->{node} || $d->{node} ne $nodename;
2046         next if !$d->{type};
2047         if ($d->{type} eq 'openvz') {
2048             if ($have_pve_manager) {
2049                 my $cfspath = PVE::OpenVZ::cfs_config_path($vmid);
2050                 if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
2051                     $openvz->{$vmid} = $conf;
2052                 }
2053             }
2054         } elsif ($d->{type} eq 'qemu') {
2055             if ($have_qemu_server) {
2056                 my $cfspath = PVE::QemuServer::cfs_config_path($vmid);
2057                 if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
2058                     $qemu->{$vmid} = $conf;
2059                 }
2060             }
2061         }
2062     }
2063
2064     return $vmdata;
2065 };
2066
2067 sub load_vmfw_conf {
2068     my ($vmid) = @_;
2069
2070     my $vmfw_conf = {};
2071
2072     my $filename = "/etc/pve/firewall/$vmid.fw";
2073     if (my $fh = IO::File->new($filename, O_RDONLY)) {
2074         $vmfw_conf = parse_vm_fw_rules($filename, $fh);
2075     }
2076
2077     return $vmfw_conf;
2078 }
2079
2080 my $format_rules = sub {
2081     my ($rules, $need_iface) = @_;
2082
2083     my $raw = '';
2084
2085     foreach my $rule (@$rules) {
2086         if ($rule->{type} eq  'in' || $rule->{type} eq 'out') {
2087             $raw .= '|' if defined($rule->{enable}) && !$rule->{enable};
2088             $raw .= uc($rule->{type});
2089             if ($rule->{macro}) {
2090                 $raw .= " $rule->{macro}($rule->{action})";
2091             } else {
2092                 $raw .= " " . $rule->{action};
2093             }
2094             $raw .= " " . ($rule->{iface} || '-') if $need_iface;
2095             $raw .= " " . ($rule->{source} || '-');
2096             $raw .= " " . ($rule->{dest} || '-');
2097             $raw .= " " . ($rule->{proto} || '-');
2098             $raw .= " " . ($rule->{dport} || '-');
2099             $raw .= " " . ($rule->{sport} || '-');
2100             $raw .= " # " . encode('utf8', $rule->{comment})
2101                 if $rule->{comment} && $rule->{comment} !~ m/^\s*$/;
2102             $raw .= "\n";
2103         } else {
2104             die "implement me '$rule->{type}'";
2105         }
2106     }
2107
2108     return $raw;
2109 };
2110
2111 my $format_options = sub {
2112     my ($options) = @_;
2113
2114     my $raw = '';
2115
2116     $raw .= "[OPTIONS]\n\n";
2117     foreach my $opt (keys %$options) {
2118         $raw .= "$opt: $options->{$opt}\n";
2119     }
2120     $raw .= "\n";
2121
2122     return $raw;
2123 };
2124
2125 sub save_vmfw_conf {
2126     my ($vmid, $vmfw_conf) = @_;
2127
2128     my $raw = '';
2129
2130     my $options = $vmfw_conf->{options};
2131     $raw .= &$format_options($options) if scalar(keys %$options);
2132
2133     my $rules = $vmfw_conf->{rules};
2134     if (scalar(@$rules)) {
2135         $raw .= "[RULES]\n\n";
2136         $raw .= &$format_rules($rules, 1);
2137         $raw .= "\n";
2138     }
2139
2140     my $filename = "/etc/pve/firewall/$vmid.fw";
2141     PVE::Tools::file_set_contents($filename, $raw);
2142 }
2143
2144 sub read_vm_firewall_configs {
2145     my ($vmdata) = @_;
2146     my $vmfw_configs = {};
2147
2148     foreach my $vmid (keys %{$vmdata->{qemu}}, keys %{$vmdata->{openvz}}) {
2149         my $vmfw_conf = load_vmfw_conf($vmid);
2150         next if !$vmfw_conf->{options}; # skip if file does not exists
2151         $vmfw_configs->{$vmid} = $vmfw_conf;
2152     }
2153
2154     return $vmfw_configs;
2155 }
2156
2157 sub get_option_log_level {
2158     my ($options, $k) = @_;
2159
2160     my $v = $options->{$k};
2161     $v = $default_log_level if !defined($v);
2162
2163     return undef if $v eq '' || $v eq 'nolog';
2164
2165     $v = $log_level_hash->{$v} if defined($log_level_hash->{$v});
2166
2167     return $v if ($v >= 0) && ($v <= 7);
2168
2169     warn "unknown log level ($k = '$v')\n";
2170
2171     return undef;
2172 }
2173
2174 sub generate_std_chains {
2175     my ($ruleset, $options) = @_;
2176
2177     my $loglevel = get_option_log_level($options, 'smurf_log_level');
2178
2179     # same as shorewall smurflog.
2180     my $chain = 'PVEFW-smurflog';
2181
2182     push @{$pve_std_chains->{$chain}}, get_log_rule_base($chain, 0, "DROP: ", $loglevel) if $loglevel;
2183     push @{$pve_std_chains->{$chain}}, "-j DROP";
2184
2185     # same as shorewall logflags action.
2186     $loglevel = get_option_log_level($options, 'tcp_flags_log_level');
2187     $chain = 'PVEFW-logflags';
2188     # fixme: is this correctly logged by pvewf-logger? (ther is no --log-ip-options for NFLOG)
2189     push @{$pve_std_chains->{$chain}}, get_log_rule_base($chain, 0, "DROP: ", $loglevel) if $loglevel;
2190     push @{$pve_std_chains->{$chain}}, "-j DROP";
2191
2192     foreach my $chain (keys %$pve_std_chains) {
2193         ruleset_create_chain($ruleset, $chain);
2194         foreach my $rule (@{$pve_std_chains->{$chain}}) {
2195             if (ref($rule)) {
2196                 ruleset_generate_rule($ruleset, $chain, $rule);
2197             } else {
2198                 ruleset_addrule($ruleset, $chain, $rule);
2199             }
2200         }
2201     }
2202 }
2203
2204 sub generate_ipset_chains {
2205     my ($ipset_ruleset, $fw_conf) = @_;
2206
2207     foreach my $ipset (keys %{$fw_conf->{ipset}}) {
2208         generate_ipset($ipset_ruleset, "PVEFW-$ipset", $fw_conf->{ipset}->{$ipset});
2209     }
2210 }
2211
2212 sub generate_ipset {
2213     my ($ipset_ruleset, $name, $options) = @_;
2214
2215     my $hashsize = scalar(@$options);
2216     if ($hashsize <= 64) {
2217         $hashsize = 64;
2218     } else {
2219         $hashsize = round_powerof2($hashsize);
2220     }
2221
2222     push @{$ipset_ruleset->{$name}}, "create $name hash:net family inet hashsize $hashsize maxelem $hashsize";
2223
2224     foreach my $ip (@$options) {
2225         push @{$ipset_ruleset->{$name}}, "add $name $ip";
2226     }
2227 }
2228
2229 sub round_powerof2 {
2230     my ($int) = @_;
2231
2232     $int--;
2233     $int |= $int >> $_ foreach (1,2,4,8,16);
2234     return ++$int;
2235 }
2236
2237 sub save_pvefw_status {
2238     my ($status) = @_;
2239
2240     die "unknown status '$status' - internal error"
2241         if $status !~ m/^(stopped|active)$/;
2242
2243     mkdir dirname($pve_fw_status_filename);
2244     PVE::Tools::file_set_contents($pve_fw_status_filename, $status);
2245 }
2246
2247 sub read_pvefw_status {
2248
2249     my $status = 'unknown';
2250
2251     return 'stopped' if ! -f $pve_fw_status_filename;
2252
2253     eval {
2254         $status = PVE::Tools::file_get_contents($pve_fw_status_filename);
2255     };
2256     warn $@ if $@;
2257
2258     return $status;
2259 }
2260
2261 # fixme: move to pve-common PVE::ProcFSTools
2262 sub read_proc_net_route {
2263     my $filename = "/proc/net/route";
2264
2265     my $res = {};
2266
2267     my $fh = IO::File->new ($filename, "r");
2268     return $res if !$fh;
2269
2270     my $int_to_quad = sub {
2271         return join '.' => map { ($_[0] >> 8*(3-$_)) % 256 } (3, 2, 1, 0);
2272     };
2273
2274     while (defined(my $line = <$fh>)) {
2275         next if $line =~/^Iface\s+Destination/; # skip head
2276         my ($iface, $dest, $gateway, $metric, $mask, $mtu) = (split(/\s+/, $line))[0,1,2,6,7,8];
2277         push @{$res->{$iface}}, {
2278             dest => &$int_to_quad(hex($dest)),
2279             gateway => &$int_to_quad(hex($gateway)),
2280             mask => &$int_to_quad(hex($mask)),
2281             metric => $metric,
2282             mtu => $mtu,
2283         };
2284     }
2285
2286     return $res;
2287 }
2288
2289 sub load_clusterfw_conf {
2290
2291     my $cluster_conf = {};
2292      if (my $fh = IO::File->new($clusterfw_conf_filename, O_RDONLY)) {
2293         $cluster_conf = parse_cluster_fw_rules($clusterfw_conf_filename, $fh);
2294     }
2295
2296     return $cluster_conf;
2297 }
2298
2299 sub save_clusterfw_conf {
2300     my ($cluster_conf) = @_;
2301
2302     my $raw = '';
2303
2304     my $options = $cluster_conf->{options};
2305     $raw .= &$format_options($options) if scalar(keys %$options);
2306
2307     # fixme: save ipset
2308
2309     my $rules = $cluster_conf->{rules};
2310     if (scalar(@$rules)) {
2311         $raw .= "[RULES]\n\n";
2312         $raw .= &$format_rules($rules, 1);
2313         $raw .= "\n";
2314     }
2315
2316     foreach my $group (sort keys %{$cluster_conf->{groups}}) {
2317         my $rules = $cluster_conf->{groups}->{$group};
2318         $raw .= "[group $group]\n\n";
2319         $raw .= &$format_rules($rules, 0);
2320         $raw .= "\n";
2321     }
2322
2323     PVE::Tools::file_set_contents($clusterfw_conf_filename, $raw);
2324 }
2325
2326 sub load_hostfw_conf {
2327
2328     my $hostfw_conf = {};
2329     if (my $fh = IO::File->new($hostfw_conf_filename, O_RDONLY)) {
2330         $hostfw_conf = parse_host_fw_rules($hostfw_conf_filename, $fh);
2331     }
2332     return $hostfw_conf;
2333 }
2334
2335 sub save_hostfw_conf {
2336     my ($hostfw_conf) = @_;
2337
2338     my $raw = '';
2339
2340     my $options = $hostfw_conf->{options};
2341     $raw .= &$format_options($options) if scalar(keys %$options);
2342
2343     my $rules = $hostfw_conf->{rules};
2344     if (scalar(@$rules)) {
2345         $raw .= "[RULES]\n\n";
2346         $raw .= &$format_rules($rules, 1);
2347         $raw .= "\n";
2348     }
2349
2350     PVE::Tools::file_set_contents($hostfw_conf_filename, $raw);
2351 }
2352
2353 sub compile {
2354     my $vmdata = read_local_vm_config();
2355     my $vmfw_configs = read_vm_firewall_configs($vmdata);
2356
2357     my $routing_table = read_proc_net_route();
2358
2359     my $cluster_conf = load_clusterfw_conf();
2360
2361     my $ipset_ruleset = {};
2362     generate_ipset_chains($ipset_ruleset, $cluster_conf);
2363
2364     my $ruleset = {};
2365
2366     ruleset_create_chain($ruleset, "PVEFW-INPUT");
2367     ruleset_create_chain($ruleset, "PVEFW-OUTPUT");
2368
2369     ruleset_create_chain($ruleset, "PVEFW-FORWARD");
2370
2371     my $hostfw_conf = load_hostfw_conf();
2372     my $hostfw_options = $hostfw_conf->{options} || {};
2373
2374     generate_std_chains($ruleset, $hostfw_options);
2375
2376     my $hostfw_enable = !(defined($hostfw_options->{enable}) && ($hostfw_options->{enable} == 0));
2377
2378     enable_host_firewall($ruleset, $hostfw_conf, $cluster_conf) if $hostfw_enable;
2379
2380     # generate firewall rules for QEMU VMs
2381     foreach my $vmid (keys %{$vmdata->{qemu}}) {
2382         my $conf = $vmdata->{qemu}->{$vmid};
2383         my $vmfw_conf = $vmfw_configs->{$vmid};
2384         next if !$vmfw_conf;
2385         next if defined($vmfw_conf->{options}->{enable}) && ($vmfw_conf->{options}->{enable} == 0);
2386
2387         foreach my $netid (keys %$conf) {
2388             next if $netid !~ m/^net(\d+)$/;
2389             my $net = PVE::QemuServer::parse_net($conf->{$netid});
2390             next if !$net;
2391             my $iface = "tap${vmid}i$1";
2392
2393             my $bridge = $net->{bridge};
2394             next if !$bridge; # fixme: ?
2395
2396             $bridge .= "v$net->{tag}" if $net->{tag};
2397
2398             generate_bridge_chains($ruleset, $hostfw_conf, $bridge, $routing_table);
2399
2400             my $macaddr = $net->{macaddr};
2401             generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
2402                                          $vmfw_conf, $vmid, $bridge, 'IN');
2403             generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
2404                                          $vmfw_conf, $vmid, $bridge, 'OUT');
2405         }
2406     }
2407
2408     # generate firewall rules for OpenVZ containers
2409     foreach my $vmid (keys %{$vmdata->{openvz}}) {
2410         my $conf = $vmdata->{openvz}->{$vmid};
2411
2412         my $vmfw_conf = $vmfw_configs->{$vmid};
2413         next if !$vmfw_conf;
2414         next if defined($vmfw_conf->{options}->{enable}) && ($vmfw_conf->{options}->{enable} == 0);
2415
2416         if ($conf->{ip_address} && $conf->{ip_address}->{value}) {
2417             my $ip = $conf->{ip_address}->{value};
2418             generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, 'IN');
2419             generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, 'OUT');
2420         }
2421
2422         if ($conf->{netif} && $conf->{netif}->{value}) {
2423             my $netif = PVE::OpenVZ::parse_netif($conf->{netif}->{value});
2424             foreach my $netid (keys %$netif) {
2425                 my $d = $netif->{$netid};
2426                 my $bridge = $d->{bridge};
2427                 if (!$bridge) {
2428                     warn "no bridge device for CT $vmid iface '$netid'\n";
2429                     next; # fixme?
2430                 }
2431
2432                 generate_bridge_chains($ruleset, $hostfw_conf, $bridge, $routing_table);
2433
2434                 my $macaddr = $d->{mac};
2435                 my $iface = $d->{host_ifname};
2436                 generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
2437                                              $vmfw_conf, $vmid, $bridge, 'IN');
2438                 generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
2439                                              $vmfw_conf, $vmid, $bridge, 'OUT');
2440             }
2441         }
2442     }
2443
2444     if($hostfw_options->{optimize}){
2445
2446         my $accept = ruleset_chain_exist($ruleset, "PVEFW-IPS") ? "PVEFW-IPS" : "ACCEPT";
2447         ruleset_insertrule($ruleset, "PVEFW-FORWARD", "-m conntrack --ctstate RELATED,ESTABLISHED -j $accept");
2448         ruleset_insertrule($ruleset, "PVEFW-FORWARD", "-m conntrack --ctstate INVALID -j DROP");
2449     }
2450
2451     # fixme: what log level should we use here?
2452     my $loglevel = get_option_log_level($hostfw_options, "log_level_out");
2453
2454     # fixme: should we really block inter-bridge traffic?
2455
2456     # always allow traffic from containers?
2457     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i venet0 -j RETURN");
2458
2459     # disable interbridge routing
2460     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o vmbr+ -j PVEFW-Drop");
2461     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i vmbr+ -j PVEFW-Drop");
2462     ruleset_addlog($ruleset, "PVEFW-FORWARD", 0, "DROP: ", $loglevel, "-o vmbr+");
2463     ruleset_addlog($ruleset, "PVEFW-FORWARD", 0, "DROP: ", $loglevel, "-i vmbr+");
2464     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o vmbr+ -j DROP");
2465     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i vmbr+ -j DROP");
2466
2467     return wantarray ? ($ruleset, $hostfw_conf, $ipset_ruleset) : $ruleset;
2468 }
2469
2470 sub get_ruleset_status {
2471     my ($ruleset, $active_chains, $digest_fn, $verbose) = @_;
2472
2473     my $statushash = {};
2474
2475     foreach my $chain (sort keys %$ruleset) {
2476         my $sig = &$digest_fn($ruleset->{$chain});
2477
2478         $statushash->{$chain}->{sig} = $sig;
2479
2480         my $oldsig = $active_chains->{$chain};
2481         if (!defined($oldsig)) {
2482             $statushash->{$chain}->{action} = 'create';
2483         } else {
2484             if ($oldsig eq $sig) {
2485                 $statushash->{$chain}->{action} = 'exists';
2486             } else {
2487                 $statushash->{$chain}->{action} = 'update';
2488             }
2489         }
2490         print "$statushash->{$chain}->{action} $chain ($sig)\n" if $verbose;
2491         foreach my $cmd (@{$ruleset->{$chain}}) {
2492             print "\t$cmd\n" if $verbose;
2493         }
2494     }
2495
2496     foreach my $chain (sort keys %$active_chains) {
2497         if (!defined($ruleset->{$chain})) {
2498             my $sig = $active_chains->{$chain};
2499             $statushash->{$chain}->{action} = 'delete';
2500             $statushash->{$chain}->{sig} = $sig;
2501             print "delete $chain ($sig)\n" if $verbose;
2502         }
2503     }
2504
2505     return $statushash;
2506 }
2507
2508 sub print_sig_rule {
2509     my ($chain, $sig) = @_;
2510
2511     # We just use this to store a SHA1 checksum used to detect changes
2512     return "-A $chain -m comment --comment \"PVESIG:$sig\"\n";
2513 }
2514
2515 sub get_ruleset_cmdlist {
2516     my ($ruleset, $verbose) = @_;
2517
2518     my $cmdlist = "*filter\n"; # we pass this to iptables-restore;
2519
2520     my ($active_chains, $hooks) = iptables_get_chains();
2521     my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest, $verbose);
2522
2523     # create missing chains first
2524     foreach my $chain (sort keys %$ruleset) {
2525         my $stat = $statushash->{$chain};
2526         die "internal error" if !$stat;
2527         next if $stat->{action} ne 'create';
2528
2529         $cmdlist .= ":$chain - [0:0]\n";
2530     }
2531
2532     foreach my $h (qw(INPUT OUTPUT FORWARD)) {
2533         if (!$hooks->{$h}) {
2534             $cmdlist .= "-A $h -j PVEFW-$h\n";
2535         }
2536     }
2537
2538     foreach my $chain (sort keys %$ruleset) {
2539         my $stat = $statushash->{$chain};
2540         die "internal error" if !$stat;
2541
2542         if ($stat->{action} eq 'update' || $stat->{action} eq 'create') {
2543             $cmdlist .= "-F $chain\n";
2544             foreach my $cmd (@{$ruleset->{$chain}}) {
2545                 $cmdlist .= "$cmd\n";
2546             }
2547             $cmdlist .= print_sig_rule($chain, $stat->{sig});
2548         } elsif ($stat->{action} eq 'delete') {
2549             die "internal error"; # this should not happen
2550         } elsif ($stat->{action} eq 'exists') {
2551             # do nothing
2552         } else {
2553             die "internal error - unknown status '$stat->{action}'";
2554         }
2555     }
2556
2557     foreach my $chain (keys %$statushash) {
2558         next if $statushash->{$chain}->{action} ne 'delete';
2559         $cmdlist .= "-F $chain\n";
2560     }
2561     foreach my $chain (keys %$statushash) {
2562         next if $statushash->{$chain}->{action} ne 'delete';
2563         next if $chain eq 'PVEFW-INPUT';
2564         next if $chain eq 'PVEFW-OUTPUT';
2565         next if $chain eq 'PVEFW-FORWARD';
2566         $cmdlist .= "-X $chain\n";
2567     }
2568
2569     my $changes = $cmdlist ne "*filter\n" ? 1 : 0;
2570
2571     $cmdlist .= "COMMIT\n";
2572
2573     return wantarray ? ($cmdlist, $changes) : $cmdlist;
2574 }
2575
2576 sub get_ipset_cmdlist {
2577     my ($ruleset, $verbose) = @_;
2578
2579     my $cmdlist = "";
2580
2581     my $delete_cmdlist = "";
2582
2583     my $active_chains = ipset_get_chains();
2584     my $statushash = get_ruleset_status($ruleset, $active_chains, \&ipset_chain_digest, $verbose);
2585
2586     foreach my $chain (sort keys %$ruleset) {
2587         my $stat = $statushash->{$chain};
2588         die "internal error" if !$stat;
2589
2590         if ($stat->{action} eq 'create') {
2591             foreach my $cmd (@{$ruleset->{$chain}}) {
2592                 $cmdlist .= "$cmd\n";
2593             }
2594         }
2595
2596         if ($stat->{action} eq 'update') {
2597             my $chain_swap = $chain."_swap";
2598
2599             foreach my $cmd (@{$ruleset->{$chain}}) {
2600                 $cmd =~ s/$chain/$chain_swap/;
2601                 $cmdlist .= "$cmd\n";
2602             }
2603             $cmdlist .= "swap $chain_swap $chain\n";
2604             $cmdlist .= "flush $chain_swap\n";
2605             $cmdlist .= "destroy $chain_swap\n";
2606         }
2607
2608     }
2609
2610     foreach my $chain (keys %$statushash) {
2611         next if $statushash->{$chain}->{action} ne 'delete';
2612
2613         $delete_cmdlist .= "flush $chain\n";
2614         $delete_cmdlist .= "destroy $chain\n";
2615     }
2616
2617     my $changes = ($cmdlist || $delete_cmdlist) ? 1 : 0;
2618
2619     return ($cmdlist, $delete_cmdlist, $changes);
2620 }
2621
2622 sub apply_ruleset {
2623     my ($ruleset, $hostfw_conf, $ipset_ruleset, $verbose) = @_;
2624
2625     enable_bridge_firewall();
2626
2627     update_nf_conntrack_max($hostfw_conf);
2628
2629     my ($ipset_create_cmdlist, $ipset_delete_cmdlist, $ipset_changes) =
2630         get_ipset_cmdlist($ipset_ruleset, undef, $verbose);
2631
2632     my ($cmdlist, $changes) = get_ruleset_cmdlist($ruleset, $verbose);
2633
2634     if ($verbose) {
2635         if ($ipset_changes) {
2636             print "ipset changes:\n";
2637             print $ipset_create_cmdlist if $ipset_create_cmdlist;
2638             print $ipset_delete_cmdlist if $ipset_delete_cmdlist;
2639         }
2640
2641         if ($changes) {
2642             print "iptables changes:\n";
2643             print $cmdlist;
2644         }
2645     }
2646
2647     ipset_restore_cmdlist($ipset_create_cmdlist);
2648
2649     iptables_restore_cmdlist($cmdlist);
2650
2651     ipset_restore_cmdlist($ipset_delete_cmdlist) if $ipset_delete_cmdlist;
2652
2653     # test: re-read status and check if everything is up to date
2654     my $active_chains = iptables_get_chains();
2655     my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest, 0);
2656
2657     my $errors;
2658     foreach my $chain (sort keys %$ruleset) {
2659         my $stat = $statushash->{$chain};
2660         if ($stat->{action} ne 'exists') {
2661             warn "unable to update chain '$chain'\n";
2662             $errors = 1;
2663         }
2664     }
2665
2666     die "unable to apply firewall changes\n" if $errors;
2667 }
2668
2669 sub update_nf_conntrack_max {
2670     my ($hostfw_conf) = @_;
2671
2672     my $max = 65536; # reasonable default
2673
2674     my $options = $hostfw_conf->{options} || {};
2675
2676     if (defined($options->{nf_conntrack_max}) && ($options->{nf_conntrack_max} > $max)) {
2677         $max = $options->{nf_conntrack_max};
2678         $max = int(($max+ 8191)/8192)*8192; # round to multiples of 8192
2679     }
2680
2681     my $filename_nf_conntrack_max = "/proc/sys/net/nf_conntrack_max";
2682     my $filename_hashsize = "/sys/module/nf_conntrack/parameters/hashsize";
2683
2684     my $current = int(PVE::Tools::file_read_firstline($filename_nf_conntrack_max) || $max);
2685
2686     if ($current != $max) {
2687         my $hashsize = int($max/4);
2688         PVE::ProcFSTools::write_proc_entry($filename_hashsize, $hashsize);
2689         PVE::ProcFSTools::write_proc_entry($filename_nf_conntrack_max, $max);
2690     }
2691 }
2692
2693 sub remove_pvefw_chains {
2694
2695     my ($chash, $hooks) = iptables_get_chains();
2696     my $cmdlist = "*filter\n";
2697
2698     foreach my $h (qw(INPUT OUTPUT FORWARD)) {
2699         if ($hooks->{$h}) {
2700             $cmdlist .= "-D $h -j PVEFW-$h\n";
2701         }
2702     }
2703
2704     foreach my $chain (keys %$chash) {
2705         $cmdlist .= "-F $chain\n";
2706     }
2707
2708     foreach my $chain (keys %$chash) {
2709         $cmdlist .= "-X $chain\n";
2710     }
2711     $cmdlist .= "COMMIT\n";
2712
2713     iptables_restore_cmdlist($cmdlist);
2714 }
2715
2716 sub update {
2717     my ($start, $verbose) = @_;
2718
2719     my $code = sub {
2720         my $status = read_pvefw_status();
2721
2722         my ($ruleset, $hostfw_conf, $ipset_ruleset) = compile();
2723
2724         if ($start || $status eq 'active') {
2725
2726             save_pvefw_status('active') if ($status ne 'active');
2727
2728             apply_ruleset($ruleset, $hostfw_conf, $ipset_ruleset, $verbose);
2729         } else {
2730             print "Firewall not active (status = $status)\n" if $verbose;
2731         }
2732     };
2733
2734     run_locked($code);
2735 }
2736
2737
2738 1;