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