88d57f97a41aefbecb9caf3e3584ade4f37f6978
[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("Start the Proxmox VE firewall service.");
132 $daemon->register_restart_command(1, "Restart the Proxmox VE firewall service.");
133 $daemon->register_stop_command("Stop firewall. This removes all Proxmox VE " .
134 "related iptable rules. " .
135 "The host is unprotected afterwards.");
136
137 __PACKAGE__->register_method ({
138 name => 'status',
139 path => 'status',
140 method => 'GET',
141 description => "Get firewall status.",
142 parameters => {
143 additionalProperties => 0,
144 properties => {},
145 },
146 returns => {
147 type => 'object',
148 additionalProperties => 0,
149 properties => {
150 status => {
151 type => 'string',
152 enum => ['unknown', 'stopped', 'running'],
153 },
154 enable => {
155 description => "Firewall is enabled (in 'cluster.fw')",
156 type => 'boolean',
157 },
158 changes => {
159 description => "Set when there are pending changes.",
160 type => 'boolean',
161 optional => 1,
162 }
163 },
164 },
165 code => sub {
166 my ($param) = @_;
167
168 local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
169
170 my $code = sub {
171
172 my $status = $daemon->running() ? 'running' : 'stopped';
173
174 my $res = { status => $status };
175
176 my $verbose = 1; # show syntax errors
177 my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, $verbose);
178 $res->{enable} = $cluster_conf->{options}->{enable} ? 1 : 0;
179
180 if ($status eq 'running') {
181
182 my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile($cluster_conf, undef, undef, $verbose);
183
184 $verbose = 0; # do not show iptables details
185 my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, $verbose);
186 my ($test, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, $verbose);
187 my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables");
188
189 $res->{changes} = ($ipset_changes || $ruleset_changes || $ruleset_changesv6) ? 1 : 0;
190 }
191
192 return $res;
193 };
194
195 return PVE::Firewall::run_locked($code);
196 }});
197
198 __PACKAGE__->register_method ({
199 name => 'compile',
200 path => 'compile',
201 method => 'GET',
202 description => "Compile and print firewall rules. This is useful for testing.",
203 parameters => {
204 additionalProperties => 0,
205 properties => {},
206 },
207 returns => { type => 'null' },
208
209 code => sub {
210 my ($param) = @_;
211
212 local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
213
214 my $code = sub {
215
216 my $verbose = 1;
217
218 my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, $verbose);
219 my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile($cluster_conf, undef, undef, $verbose);
220
221 print "ipset cmdlist:\n";
222 my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, $verbose);
223
224 print "\niptables cmdlist:\n";
225 my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, $verbose);
226
227 print "\nip6tables cmdlist:\n";
228 my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables");
229
230 if ($ipset_changes || $ruleset_changes || $ruleset_changesv6) {
231 print "detected changes\n";
232 } else {
233 print "no changes\n";
234 }
235 if (!$cluster_conf->{options}->{enable}) {
236 print "firewall disabled\n";
237 }
238
239 };
240
241 PVE::Firewall::run_locked($code);
242
243 return undef;
244 }});
245
246 __PACKAGE__->register_method ({
247 name => 'localnet',
248 path => 'localnet',
249 method => 'GET',
250 description => "Print information about local network.",
251 parameters => {
252 additionalProperties => 0,
253 properties => {},
254 },
255 returns => { type => 'null' },
256 code => sub {
257 my ($param) = @_;
258
259 local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
260
261 my $nodename = PVE::INotify::nodename();
262 print "local hostname: $nodename\n";
263
264 my $ip = PVE::Cluster::remote_node_ip($nodename);
265 print "local IP address: $ip\n";
266
267 my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
268
269 my $localnet = PVE::Firewall::local_network() || '127.0.0.0/8';
270 print "network auto detect: $localnet\n";
271 if ($cluster_conf->{aliases}->{local_network}) {
272 print "using user defined local_network: $cluster_conf->{aliases}->{local_network}->{cidr}\n";
273 } else {
274 print "using detected local_network: $localnet\n";
275 }
276
277 return undef;
278 }});
279
280 __PACKAGE__->register_method ({
281 name => 'simulate',
282 path => 'simulate',
283 method => 'GET',
284 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.",
285 parameters => {
286 additionalProperties => 0,
287 properties => {
288 verbose => {
289 description => "Verbose output.",
290 type => 'boolean',
291 optional => 1,
292 default => 0,
293 },
294 from => {
295 description => "Source zone.",
296 type => 'string',
297 pattern => '(host|outside|vm\d+|ct\d+|vmbr\d+/\S+)',
298 optional => 1,
299 default => 'outside',
300 },
301 to => {
302 description => "Destination zone.",
303 type => 'string',
304 pattern => '(host|outside|vm\d+|ct\d+|vmbr\d+/\S+)',
305 optional => 1,
306 default => 'host',
307 },
308 protocol => {
309 description => "Protocol.",
310 type => 'string',
311 pattern => '(tcp|udp)',
312 optional => 1,
313 default => 'tcp',
314 },
315 dport => {
316 description => "Destination port.",
317 type => 'integer',
318 minValue => 1,
319 maxValue => 65535,
320 optional => 1,
321 },
322 sport => {
323 description => "Source port.",
324 type => 'integer',
325 minValue => 1,
326 maxValue => 65535,
327 optional => 1,
328 },
329 source => {
330 description => "Source IP address.",
331 type => 'string', format => 'ipv4',
332 optional => 1,
333 },
334 dest => {
335 description => "Destination IP address.",
336 type => 'string', format => 'ipv4',
337 optional => 1,
338 },
339 },
340 },
341 returns => { type => 'null' },
342 code => sub {
343 my ($param) = @_;
344
345 local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
346
347 my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile(undef, undef, undef, $param->{verbose});
348
349 PVE::FirewallSimulator::debug($param->{verbose} || 0);
350
351 my $host_ip = PVE::Cluster::remote_node_ip($nodename);
352
353 PVE::FirewallSimulator::reset_trace();
354 print Dumper($ruleset) if $param->{verbose};
355
356 my $test = {
357 from => $param->{from},
358 to => $param->{to},
359 proto => $param->{protocol} || 'tcp',
360 source => $param->{source},
361 dest => $param->{dest},
362 dport => $param->{dport},
363 sport => $param->{sport},
364 };
365
366 if (!defined($test->{to})) {
367 $test->{to} = 'host';
368 PVE::FirewallSimulator::add_trace("Set Zone: to => '$test->{to}'\n");
369 }
370 if (!defined($test->{from})) {
371 $test->{from} = 'outside',
372 PVE::FirewallSimulator::add_trace("Set Zone: from => '$test->{from}'\n");
373 }
374
375 my $vmdata = PVE::Firewall::read_local_vm_config();
376
377 print "Test packet:\n";
378
379 foreach my $k (qw(from to proto source dest dport sport)) {
380 printf(" %-8s: %s\n", $k, $test->{$k}) if defined($test->{$k});
381 }
382
383 $test->{action} = 'QUERY';
384
385 my $res = PVE::FirewallSimulator::simulate_firewall($ruleset, $ipset_ruleset,
386 $host_ip, $vmdata, $test);
387
388 print "ACTION: $res\n";
389
390 return undef;
391 }});
392
393 my $cmddef = {
394 start => [ __PACKAGE__, 'start', []],
395 restart => [ __PACKAGE__, 'restart', []],
396 stop => [ __PACKAGE__, 'stop', []],
397 compile => [ __PACKAGE__, 'compile', []],
398 simulate => [ __PACKAGE__, 'simulate', []],
399 localnet => [ __PACKAGE__, 'localnet', []],
400 status => [ __PACKAGE__, 'status', [], undef, sub {
401 my $res = shift;
402 my $status = ($res->{enable} ? "enabled" : "disabled") . '/' . $res->{status};
403
404 if ($res->{changes}) {
405 print "Status: $status (pending changes)\n";
406 } else {
407 print "Status: $status\n";
408 }
409 }],
410 };
411
412 my $cmd = shift;
413
414 PVE::CLIHandler::handle_cmd($cmddef, $0, $cmd, \@ARGV, undef, $0);
415
416 exit (0);
417
418 __END__
419
420 =head1 NAME
421
422 pve-firewall - PVE Firewall Daemon
423
424 =head1 SYNOPSIS
425
426 =include synopsis
427
428 =head1 DESCRIPTION
429
430 This service updates iptables rules periodically.
431
432 =include pve_copyright