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