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