simplify code (error log is done inside Daemon.pm)
[pve-firewall.git] / src / pve-firewall
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use PVE::SafeSyslog;
6 use PVE::Daemon;
7
8 use Time::HiRes qw (gettimeofday);
9 use PVE::Tools qw(dir_glob_foreach file_read_firstline);
10 use PVE::ProcFSTools;
11 use PVE::INotify;
12 use PVE::Cluster qw(cfs_read_file);
13 use PVE::RPCEnvironment;
14 use PVE::CLIHandler;
15 use PVE::Firewall;
16 use PVE::FirewallSimulator;
17 use Data::Dumper;
18
19 use base qw(PVE::Daemon);
20
21 $SIG{'__WARN__'} = sub {
22     my $err = $@;
23     my $t = $_[0];
24     chomp $t;
25     print STDERR "$t\n";
26     syslog('warning', "%s", $t);
27     $@ = $err;
28 };
29
30 my $cmdline = [$0, @ARGV];
31
32 my %daemon_options = (restart_on_error => 5, stop_wait_time => 5);
33
34 my $daemon = __PACKAGE__->new('pve-firewall', $cmdline, %daemon_options);
35
36 my $rpcenv = PVE::RPCEnvironment->init('cli');
37
38 $rpcenv->init_request();
39 $rpcenv->set_language($ENV{LANG});
40 $rpcenv->set_user('root@pam');
41
42 my $nodename = PVE::INotify::nodename();
43
44 sub init {
45
46     PVE::Cluster::cfs_update();
47     
48     PVE::Firewall::init();
49 }
50
51 my $restart_request = 0;
52 my $next_update = 0;
53
54 my $cycle = 0; 
55 my $updatetime = 10;
56
57 my $initial_memory_usage;
58
59 sub shutdown {
60     my ($self) = @_;
61
62     syslog('info' , "server closing");
63
64     # wait for children
65     1 while (waitpid(-1, POSIX::WNOHANG()) > 0);
66         
67     syslog('info' , "clear firewall rules");
68
69     eval { PVE::Firewall::remove_pvefw_chains(); };
70     warn $@ if $@;
71
72     $self->exit_daemon(0);
73 }
74
75 sub hup {
76     my ($self) = @_;
77
78     $restart_request = 1;
79 }
80
81 sub run {
82     my ($self) = @_;
83
84     local $SIG{'__WARN__'} = 'IGNORE'; # do not fill up logs
85
86     for (;;) { # forever
87
88         $next_update = time() + $updatetime;
89
90         my ($ccsec, $cusec) = gettimeofday ();
91         eval {
92             PVE::Cluster::cfs_update();
93             PVE::Firewall::update();
94         };
95         my $err = $@;
96         
97         if ($err) {
98             syslog('err', "status update error: $err");
99         }
100
101         my ($ccsec_end, $cusec_end) = gettimeofday ();
102         my $cptime = ($ccsec_end-$ccsec) + ($cusec_end - $cusec)/1000000;
103
104         syslog('info', sprintf("firewall update time (%.3f seconds)", $cptime))
105             if ($cptime > 5);
106
107         $cycle++;
108
109         my $mem = PVE::ProcFSTools::read_memory_usage();
110         
111         if (!defined($initial_memory_usage) || ($cycle < 10)) {
112             $initial_memory_usage = $mem->{resident};
113         } else {
114             my $diff = $mem->{resident} - $initial_memory_usage;
115             if ($diff > 5*1024*1024) {
116                 syslog ('info', "restarting server after $cycle cycles to " .
117                         "reduce memory usage (free $mem->{resident} ($diff) bytes)");
118                 $self->restart_daemon();
119             }
120         }
121
122         my $wcount = 0;
123         while ((time() < $next_update) && 
124                ($wcount < $updatetime) && # protect against time wrap
125                !$restart_request) { $wcount++; sleep (1); };
126         
127         $self->restart_daemon() if $restart_request;
128     }
129 }
130
131 $daemon->register_start_command(__PACKAGE__,
132                                 "Start the Proxmox VE firewall service.");
133 $daemon->register_restart_command(__PACKAGE__, 1, 
134                                   "Restart the Proxmox VE firewall service.");
135 $daemon->register_stop_command(__PACKAGE__,
136                                "Stop firewall. This removes all Proxmox VE " .
137                                "related iptable rules. " .
138                                "The host is unprotected afterwards.");
139
140 __PACKAGE__->register_method ({
141     name => 'status',
142     path => 'status',
143     method => 'GET',
144     description => "Get firewall status.",
145     parameters => {
146         additionalProperties => 0,
147         properties => {},
148     },
149     returns => { 
150         type => 'object',
151         additionalProperties => 0,
152         properties => {
153             status => {
154                 type => 'string',
155                 enum => ['unknown', 'stopped', 'running'],
156             },
157             enable => {
158                 description => "Firewall is enabled (in 'cluster.fw')",
159                 type => 'boolean',
160             },
161             changes => {
162                 description => "Set when there are pending changes.",
163                 type => 'boolean',
164                 optional => 1,
165             }
166         },
167     },
168     code => sub {
169         my ($param) = @_;
170
171         local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
172
173         my $code = sub {
174
175             my $status = $daemon->running() ? 'running' : 'stopped';
176
177             my $res = { status => $status };
178
179             my $verbose = 1; # show syntax errors 
180             my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, $verbose); 
181             $res->{enable} = $cluster_conf->{options}->{enable} ? 1 : 0;
182
183             if ($status eq 'running') {
184                 
185                 my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile($cluster_conf, undef, undef, $verbose);
186
187                 $verbose = 0; # do not show iptables details
188                 my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, $verbose);
189                 my ($test, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, $verbose);
190                 my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables");
191
192                 $res->{changes} = ($ipset_changes || $ruleset_changes || $ruleset_changesv6) ? 1 : 0;
193             }
194
195             return $res;
196         };
197
198         return PVE::Firewall::run_locked($code);
199     }});
200
201 __PACKAGE__->register_method ({
202     name => 'compile',
203     path => 'compile',
204     method => 'GET',
205     description => "Compile and print firewall rules. This is useful for testing.",
206     parameters => {
207         additionalProperties => 0,
208         properties => {},
209     },
210     returns => { type => 'null' },
211
212     code => sub {
213         my ($param) = @_;
214
215         local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
216
217         my $code = sub {
218
219             my $verbose = 1;
220
221             my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, $verbose); 
222             my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile($cluster_conf, undef, undef, $verbose);
223
224             print "ipset cmdlist:\n";
225             my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, $verbose);
226
227             print "\niptables cmdlist:\n";
228             my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, $verbose);
229
230             print "\nip6tables cmdlist:\n";
231             my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables");
232
233             if ($ipset_changes || $ruleset_changes || $ruleset_changesv6) {
234                 print "detected changes\n";
235             } else {
236                 print "no changes\n";
237             }
238             if (!$cluster_conf->{options}->{enable}) {
239                 print "firewall disabled\n";
240             }
241
242         };
243
244         PVE::Firewall::run_locked($code);
245
246         return undef;
247     }});
248
249 __PACKAGE__->register_method ({
250     name => 'localnet',
251     path => 'localnet',
252     method => 'GET',
253     description => "Print information about local network.",
254     parameters => {
255         additionalProperties => 0,
256         properties => {},
257     },
258     returns => { type => 'null' },
259     code => sub {
260         my ($param) = @_;
261
262         local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
263
264         my $nodename = PVE::INotify::nodename();
265         print "local hostname: $nodename\n";
266
267         my $ip = PVE::Cluster::remote_node_ip($nodename);
268         print "local IP address: $ip\n";
269
270         my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
271         
272         my $localnet = PVE::Firewall::local_network() || '127.0.0.0/8';
273         print "network auto detect: $localnet\n";
274         if ($cluster_conf->{aliases}->{local_network}) {
275             print "using user defined local_network: $cluster_conf->{aliases}->{local_network}->{cidr}\n";
276         } else {
277             print "using detected local_network: $localnet\n";
278         }
279
280         return undef;
281     }});
282
283 __PACKAGE__->register_method ({
284     name => 'simulate',
285     path => 'simulate',
286     method => 'GET',
287     description => "Simulate firewall rules. This does not simulate kernel 'routing' table. Instead, this simply assumes that routing from source zone to destination zone is possible.",
288     parameters => {
289         additionalProperties => 0,
290         properties => {
291             verbose => {
292                 description => "Verbose output.",
293                 type => 'boolean',
294                 optional => 1,
295                 default => 0,
296             },
297             from => {
298                 description => "Source zone.",
299                 type => 'string',
300                 pattern => '(host|outside|vm\d+|ct\d+|vmbr\d+/\S+)',
301                 optional => 1,
302                 default => 'outside',
303             },
304             to => {
305                 description => "Destination zone.",
306                 type => 'string',
307                 pattern => '(host|outside|vm\d+|ct\d+|vmbr\d+/\S+)',
308                 optional => 1,
309                 default => 'host',
310             },
311             protocol => {
312                 description => "Protocol.",
313                 type => 'string',
314                 pattern => '(tcp|udp)',
315                 optional => 1,
316                 default => 'tcp',
317             },
318             dport => {
319                 description => "Destination port.",
320                 type => 'integer',
321                 minValue => 1,
322                 maxValue => 65535,
323                 optional => 1,
324             },
325             sport => {
326                 description => "Source port.",
327                 type => 'integer',
328                 minValue => 1,
329                 maxValue => 65535,
330                 optional => 1,
331             },
332             source => {
333                 description => "Source IP address.",
334                 type => 'string', format => 'ipv4',
335                 optional => 1,
336             },
337             dest => {
338                 description => "Destination IP address.",
339                 type => 'string', format => 'ipv4',
340                 optional => 1,
341             },
342         },
343     },
344     returns => { type => 'null' },
345     code => sub {
346         my ($param) = @_;
347
348         local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
349
350         my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile(undef, undef, undef, $param->{verbose});
351
352         PVE::FirewallSimulator::debug($param->{verbose} || 0);
353         
354         my $host_ip = PVE::Cluster::remote_node_ip($nodename);
355
356         PVE::FirewallSimulator::reset_trace();
357         print Dumper($ruleset) if $param->{verbose};
358
359         my $test = {
360             from => $param->{from},
361             to => $param->{to},
362             proto => $param->{protocol} || 'tcp',
363             source => $param->{source},
364             dest => $param->{dest},
365             dport => $param->{dport},
366             sport => $param->{sport},
367         };
368
369         if (!defined($test->{to})) {
370             $test->{to} = 'host';
371             PVE::FirewallSimulator::add_trace("Set Zone: to => '$test->{to}'\n"); 
372         } 
373         if (!defined($test->{from})) {
374             $test->{from} = 'outside',
375             PVE::FirewallSimulator::add_trace("Set Zone: from => '$test->{from}'\n"); 
376         }
377
378         my $vmdata = PVE::Firewall::read_local_vm_config();
379
380         print "Test packet:\n";
381
382         foreach my $k (qw(from to proto source dest dport sport)) {
383             printf("  %-8s: %s\n", $k, $test->{$k}) if defined($test->{$k});
384         }
385
386         $test->{action} = 'QUERY';
387
388         my $res = PVE::FirewallSimulator::simulate_firewall($ruleset, $ipset_ruleset, 
389                                                             $host_ip, $vmdata, $test);
390         
391         print "ACTION: $res\n";
392
393         return undef;
394     }});
395
396 my $cmddef = {
397     start => [ __PACKAGE__, 'start', []],
398     restart => [ __PACKAGE__, 'restart', []],
399     stop => [ __PACKAGE__, 'stop', []],
400     compile => [ __PACKAGE__, 'compile', []],
401     simulate => [ __PACKAGE__, 'simulate', []],
402     localnet => [ __PACKAGE__, 'localnet', []],
403     status => [ __PACKAGE__, 'status', [], undef, sub {
404         my $res = shift;
405         my $status = ($res->{enable} ? "enabled" : "disabled") . '/' . $res->{status};
406         
407         if ($res->{changes}) {
408             print "Status: $status (pending changes)\n";
409         } else {
410             print "Status: $status\n";
411         }
412     }],
413  };
414
415 my $cmd = shift;
416
417 PVE::CLIHandler::handle_cmd($cmddef, $0, $cmd, \@ARGV, undef, $0);
418
419 exit (0);
420
421 __END__
422
423 =head1 NAME
424                                           
425 pve-firewall - PVE Firewall Daemon
426
427 =head1 SYNOPSIS
428
429 =include synopsis
430
431 =head1 DESCRIPTION
432
433 This service updates iptables rules periodically.
434
435 =include pve_copyright