]> git.proxmox.com Git - pve-firewall.git/blame - src/pve-firewall
simplify code (error log is done inside Daemon.pm)
[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
cf10b505
DM
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
a3d58ffc 30my $cmdline = [$0, @ARGV];
e2beb7aa 31
a3d58ffc 32my %daemon_options = (restart_on_error => 5, stop_wait_time => 5);
e2beb7aa 33
8e471378 34my $daemon = __PACKAGE__->new('pve-firewall', $cmdline, %daemon_options);
e2beb7aa
DM
35
36my $rpcenv = PVE::RPCEnvironment->init('cli');
37
38$rpcenv->init_request();
39$rpcenv->set_language($ENV{LANG});
40$rpcenv->set_user('root@pam');
41
814de832
DM
42my $nodename = PVE::INotify::nodename();
43
a3d58ffc 44sub init {
e2beb7aa 45
a3d58ffc
DM
46 PVE::Cluster::cfs_update();
47
48 PVE::Firewall::init();
e2beb7aa
DM
49}
50
51my $restart_request = 0;
52my $next_update = 0;
53
54my $cycle = 0;
55my $updatetime = 10;
56
57my $initial_memory_usage;
58
a3d58ffc
DM
59sub shutdown {
60 my ($self) = @_;
e2beb7aa 61
a3d58ffc 62 syslog('info' , "server closing");
e2beb7aa 63
a3d58ffc
DM
64 # wait for children
65 1 while (waitpid(-1, POSIX::WNOHANG()) > 0);
e2beb7aa 66
a3d58ffc 67 syslog('info' , "clear firewall rules");
e2beb7aa 68
a3d58ffc
DM
69 eval { PVE::Firewall::remove_pvefw_chains(); };
70 warn $@ if $@;
e2beb7aa 71
a3d58ffc
DM
72 $self->exit_daemon(0);
73}
e2beb7aa 74
a3d58ffc
DM
75sub hup {
76 my ($self) = @_;
e2beb7aa 77
a3d58ffc
DM
78 $restart_request = 1;
79}
e2beb7aa 80
a3d58ffc
DM
81sub run {
82 my ($self) = @_;
e2beb7aa 83
a3d58ffc 84 local $SIG{'__WARN__'} = 'IGNORE'; # do not fill up logs
e2beb7aa 85
a3d58ffc 86 for (;;) { # forever
e2beb7aa 87
a3d58ffc 88 $next_update = time() + $updatetime;
e2beb7aa 89
a3d58ffc
DM
90 my ($ccsec, $cusec) = gettimeofday ();
91 eval {
92 PVE::Cluster::cfs_update();
93 PVE::Firewall::update();
e2beb7aa 94 };
e2beb7aa 95 my $err = $@;
a3d58ffc 96
e2beb7aa 97 if ($err) {
a3d58ffc 98 syslog('err', "status update error: $err");
e2beb7aa 99 }
e2beb7aa 100
a3d58ffc
DM
101 my ($ccsec_end, $cusec_end) = gettimeofday ();
102 my $cptime = ($ccsec_end-$ccsec) + ($cusec_end - $cusec)/1000000;
e2beb7aa 103
a3d58ffc
DM
104 syslog('info', sprintf("firewall update time (%.3f seconds)", $cptime))
105 if ($cptime > 5);
e2beb7aa 106
a3d58ffc 107 $cycle++;
e2beb7aa 108
a3d58ffc
DM
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();
e2beb7aa
DM
119 }
120 }
121
a3d58ffc
DM
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.");
e3087bc6 133$daemon->register_restart_command(__PACKAGE__, 1,
a3d58ffc
DM
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.");
e2beb7aa
DM
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',
55fad3b7
DM
155 enum => ['unknown', 'stopped', 'running'],
156 },
157 enable => {
158 description => "Firewall is enabled (in 'cluster.fw')",
159 type => 'boolean',
e2beb7aa
DM
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
a3d58ffc 175 my $status = $daemon->running() ? 'running' : 'stopped';
e2beb7aa
DM
176
177 my $res = { status => $status };
55fad3b7
DM
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') {
d4cda423 184
638c755a 185 my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile($cluster_conf, undef, undef, $verbose);
e2beb7aa 186
d4cda423
DM
187 $verbose = 0; # do not show iptables details
188 my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, $verbose);
55fad3b7 189 my ($test, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, $verbose);
17da5c0f
AD
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;
e2beb7aa
DM
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',
3324948a 204 method => 'GET',
16adff04 205 description => "Compile and print firewall rules. This is useful for testing.",
e2beb7aa
DM
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 {
e2beb7aa 218
d4cda423
DM
219 my $verbose = 1;
220
55fad3b7 221 my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, $verbose);
638c755a 222 my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile($cluster_conf, undef, undef, $verbose);
d4cda423 223
1cc9bd90 224 print "ipset cmdlist:\n";
d4cda423 225 my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, $verbose);
1cc9bd90
DM
226
227 print "\niptables cmdlist:\n";
d4cda423 228 my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, $verbose);
1cc9bd90
DM
229
230 print "\nip6tables cmdlist:\n";
17da5c0f 231 my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables");
55fad3b7 232
17da5c0f 233 if ($ipset_changes || $ruleset_changes || $ruleset_changesv6) {
e2beb7aa
DM
234 print "detected changes\n";
235 } else {
236 print "no changes\n";
237 }
55fad3b7
DM
238 if (!$cluster_conf->{options}->{enable}) {
239 print "firewall disabled\n";
240 }
241
e2beb7aa
DM
242 };
243
244 PVE::Firewall::run_locked($code);
245
246 return undef;
247 }});
248
e7fb6ff2
DM
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
814de832
DM
283__PACKAGE__->register_method ({
284 name => 'simulate',
285 path => 'simulate',
3324948a 286 method => 'GET',
c9902e5a 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.",
814de832
DM
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
815b4ebf
DM
348 local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
349
638c755a 350 my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile(undef, undef, undef, $param->{verbose});
814de832
DM
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 }});
e2beb7aa
DM
395
396my $cmddef = {
397 start => [ __PACKAGE__, 'start', []],
a3d58ffc 398 restart => [ __PACKAGE__, 'restart', []],
e2beb7aa
DM
399 stop => [ __PACKAGE__, 'stop', []],
400 compile => [ __PACKAGE__, 'compile', []],
814de832 401 simulate => [ __PACKAGE__, 'simulate', []],
e7fb6ff2 402 localnet => [ __PACKAGE__, 'localnet', []],
e2beb7aa
DM
403 status => [ __PACKAGE__, 'status', [], undef, sub {
404 my $res = shift;
55fad3b7
DM
405 my $status = ($res->{enable} ? "enabled" : "disabled") . '/' . $res->{status};
406
e2beb7aa 407 if ($res->{changes}) {
55fad3b7 408 print "Status: $status (pending changes)\n";
e2beb7aa 409 } else {
55fad3b7 410 print "Status: $status\n";
e2beb7aa
DM
411 }
412 }],
413 };
414
415my $cmd = shift;
416
417PVE::CLIHandler::handle_cmd($cmddef, $0, $cmd, \@ARGV, undef, $0);
418
419exit (0);
420
421__END__
422
423=head1 NAME
424
16adff04 425pve-firewall - PVE Firewall Daemon
e2beb7aa
DM
426
427=head1 SYNOPSIS
428
16adff04 429=include synopsis
e2beb7aa
DM
430
431=head1 DESCRIPTION
432
433This service updates iptables rules periodically.
434
16adff04 435=include pve_copyright