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