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