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