]> git.proxmox.com Git - pve-firewall.git/blob - src/pve-firewall
use Daemon class from pve-common
[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 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 $rpcenv = PVE::RPCEnvironment->init('cli');
28
29 $rpcenv->init_request();
30 $rpcenv->set_language($ENV{LANG});
31 $rpcenv->set_user('root@pam');
32
33 my $nodename = PVE::INotify::nodename();
34
35 sub init {
36
37 PVE::Cluster::cfs_update();
38
39 PVE::Firewall::init();
40 }
41
42 my $restart_request = 0;
43 my $next_update = 0;
44
45 my $cycle = 0;
46 my $updatetime = 10;
47
48 my $initial_memory_usage;
49
50 sub shutdown {
51 my ($self) = @_;
52
53 syslog('info' , "server closing");
54
55 # wait for children
56 1 while (waitpid(-1, POSIX::WNOHANG()) > 0);
57
58 syslog('info' , "clear firewall rules");
59
60 eval { PVE::Firewall::remove_pvefw_chains(); };
61 warn $@ if $@;
62
63 $self->exit_daemon(0);
64 }
65
66 sub hup {
67 my ($self) = @_;
68
69 syslog('info' , "received signal HUP");
70
71 $restart_request = 1;
72 }
73
74 sub run {
75 my ($self) = @_;
76
77 local $SIG{'__WARN__'} = 'IGNORE'; # do not fill up logs
78
79 for (;;) { # forever
80
81 $next_update = time() + $updatetime;
82
83 my ($ccsec, $cusec) = gettimeofday ();
84 eval {
85 PVE::Cluster::cfs_update();
86 PVE::Firewall::update();
87 };
88 my $err = $@;
89
90 if ($err) {
91 syslog('err', "status update error: $err");
92 }
93
94 my ($ccsec_end, $cusec_end) = gettimeofday ();
95 my $cptime = ($ccsec_end-$ccsec) + ($cusec_end - $cusec)/1000000;
96
97 syslog('info', sprintf("firewall update time (%.3f seconds)", $cptime))
98 if ($cptime > 5);
99
100 $cycle++;
101
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();
112 }
113 }
114
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.");
126 $daemon->register_restart_command(__PACKAGE__,
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.");
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',
148 enum => ['unknown', 'stopped', 'running'],
149 },
150 enable => {
151 description => "Firewall is enabled (in 'cluster.fw')",
152 type => 'boolean',
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
168 my $status = $daemon->running() ? 'running' : 'stopped';
169
170 my $res = { status => $status };
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') {
177
178 my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile($cluster_conf, undef, undef, $verbose);
179
180 $verbose = 0; # do not show iptables details
181 my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, $verbose);
182 my ($test, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, $verbose);
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;
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',
197 method => 'GET',
198 description => "Compile and print firewall rules. This is useful for testing.",
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 {
211
212 my $verbose = 1;
213
214 my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, $verbose);
215 my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile($cluster_conf, undef, undef, $verbose);
216
217 print "ipset cmdlist:\n";
218 my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, $verbose);
219
220 print "\niptables cmdlist:\n";
221 my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, $verbose);
222
223 print "\nip6tables cmdlist:\n";
224 my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables");
225
226 if ($ipset_changes || $ruleset_changes || $ruleset_changesv6) {
227 print "detected changes\n";
228 } else {
229 print "no changes\n";
230 }
231 if (!$cluster_conf->{options}->{enable}) {
232 print "firewall disabled\n";
233 }
234
235 };
236
237 PVE::Firewall::run_locked($code);
238
239 return undef;
240 }});
241
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
276 __PACKAGE__->register_method ({
277 name => 'simulate',
278 path => 'simulate',
279 method => 'GET',
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.",
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
341 local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
342
343 my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile(undef, undef, undef, $param->{verbose});
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 }});
388
389 my $cmddef = {
390 start => [ __PACKAGE__, 'start', []],
391 restart => [ __PACKAGE__, 'restart', []],
392 stop => [ __PACKAGE__, 'stop', []],
393 compile => [ __PACKAGE__, 'compile', []],
394 simulate => [ __PACKAGE__, 'simulate', []],
395 localnet => [ __PACKAGE__, 'localnet', []],
396 status => [ __PACKAGE__, 'status', [], undef, sub {
397 my $res = shift;
398 my $status = ($res->{enable} ? "enabled" : "disabled") . '/' . $res->{status};
399
400 if ($res->{changes}) {
401 print "Status: $status (pending changes)\n";
402 } else {
403 print "Status: $status\n";
404 }
405 }],
406 };
407
408 my $cmd = shift;
409
410 PVE::CLIHandler::handle_cmd($cmddef, $0, $cmd, \@ARGV, undef, $0);
411
412 exit (0);
413
414 __END__
415
416 =head1 NAME
417
418 pve-firewall - PVE Firewall Daemon
419
420 =head1 SYNOPSIS
421
422 =include synopsis
423
424 =head1 DESCRIPTION
425
426 This service updates iptables rules periodically.
427
428 =include pve_copyright