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