]> git.proxmox.com Git - pve-firewall.git/blame - src/PVE/Service/pve_firewall.pm
whitespace/indentation/line-length cleanup
[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;
0c32b7fb
DM
21
22use base qw(PVE::Daemon);
23
24my $cmdline = [$0, @ARGV];
25
26my %daemon_options = (restart_on_error => 5, stop_wait_time => 5);
27
28my $daemon = __PACKAGE__->new('pve-firewall', $cmdline, %daemon_options);
29
30my $nodename = PVE::INotify::nodename();
31
32sub init {
0c32b7fb 33 PVE::Cluster::cfs_update();
2e2b71e1 34
0c32b7fb
DM
35 PVE::Firewall::init();
36}
37
38my $restart_request = 0;
39my $next_update = 0;
40
2e2b71e1 41my $cycle = 0;
0c32b7fb
DM
42my $updatetime = 10;
43
44my $initial_memory_usage;
45
46sub shutdown {
47 my ($self) = @_;
48
49 syslog('info' , "server closing");
50
51 # wait for children
52 1 while (waitpid(-1, POSIX::WNOHANG()) > 0);
2e2b71e1 53
0c32b7fb
DM
54 syslog('info' , "clear firewall rules");
55
56 eval { PVE::Firewall::remove_pvefw_chains(); };
57 warn $@ if $@;
58
59 $self->exit_daemon(0);
60}
61
62sub hup {
63 my ($self) = @_;
64
65 $restart_request = 1;
66}
67
68sub run {
69 my ($self) = @_;
70
71 local $SIG{'__WARN__'} = 'IGNORE'; # do not fill up logs
72
73 for (;;) { # forever
0c32b7fb
DM
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.");
4a626429
TL
119$daemon->register_stop_command(
120 "Stop the Proxmox VE firewall service. Note, stopping actively removes all Proxmox VE related"
121 ." iptable rules rendering the host potentially unprotected."
122);
0c32b7fb
DM
123
124__PACKAGE__->register_method ({
125 name => 'status',
126 path => 'status',
127 method => 'GET',
128 description => "Get firewall status.",
129 parameters => {
2e2b71e1 130 additionalProperties => 0,
0c32b7fb
DM
131 properties => {},
132 },
2e2b71e1 133 returns => {
0c32b7fb
DM
134 type => 'object',
135 additionalProperties => 0,
136 properties => {
137 status => {
138 type => 'string',
139 enum => ['unknown', 'stopped', 'running'],
140 },
141 enable => {
142 description => "Firewall is enabled (in 'cluster.fw')",
143 type => 'boolean',
144 },
145 changes => {
146 description => "Set when there are pending changes.",
147 type => 'boolean',
148 optional => 1,
149 }
150 },
151 },
152 code => sub {
153 my ($param) = @_;
154
155 local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
156
157 my $code = sub {
158
159 my $status = $daemon->running() ? 'running' : 'stopped';
160
161 my $res = { status => $status };
162
40af93c4
TL
163 PVE::Firewall::set_verbose(1); # show syntax errors
164
165 my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef);
0c32b7fb
DM
166 $res->{enable} = $cluster_conf->{options}->{enable} ? 1 : 0;
167
168 if ($status eq 'running') {
2e2b71e1 169
40af93c4 170 my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = PVE::Firewall::compile($cluster_conf, undef, undef);
0c32b7fb 171
40af93c4
TL
172 PVE::Firewall::set_verbose(0); # do not show iptables details
173 my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset);
64e0adf4
AD
174 my ($test, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset->{filter});
175 my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{filter}, "ip6tables");
176 my (undef, $ruleset_changes_raw) = PVE::Firewall::get_ruleset_cmdlist($ruleset->{raw}, undef, 'raw');
177 my (undef, $ruleset_changesv6_raw) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{raw}, "ip6tables", 'raw');
40af93c4 178 my (undef, $ebtables_changes) = PVE::Firewall::get_ebtables_cmdlist($ebtables_ruleset);
0c32b7fb 179
64e0adf4 180 $res->{changes} = ($ipset_changes || $ruleset_changes || $ruleset_changesv6 || $ebtables_changes || $ruleset_changes_raw || $ruleset_changesv6_raw) ? 1 : 0;
0c32b7fb
DM
181 }
182
183 return $res;
184 };
185
186 return PVE::Firewall::run_locked($code);
187 }});
188
189__PACKAGE__->register_method ({
190 name => 'compile',
191 path => 'compile',
192 method => 'GET',
193 description => "Compile and print firewall rules. This is useful for testing.",
194 parameters => {
2e2b71e1 195 additionalProperties => 0,
0c32b7fb
DM
196 properties => {},
197 },
198 returns => { type => 'null' },
199
200 code => sub {
201 my ($param) = @_;
202
203 local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
204
205 my $code = sub {
206
40af93c4 207 PVE::Firewall::set_verbose(1);
0c32b7fb 208
40af93c4
TL
209 my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef);
210 my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = PVE::Firewall::compile($cluster_conf, undef, undef);
0c32b7fb
DM
211
212 print "ipset cmdlist:\n";
40af93c4 213 my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset);
0c32b7fb
DM
214
215 print "\niptables cmdlist:\n";
64e0adf4 216 my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset->{filter});
0c32b7fb
DM
217
218 print "\nip6tables cmdlist:\n";
64e0adf4 219 my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{filter}, "ip6tables");
0c32b7fb 220
151c209e 221 print "\nebtables cmdlist:\n";
40af93c4 222 my (undef, $ebtables_changes) = PVE::Firewall::get_ebtables_cmdlist($ebtables_ruleset);
151c209e 223
64e0adf4
AD
224 print "\niptables table raw cmdlist:\n";
225 my (undef, $ruleset_changes_raw) = PVE::Firewall::get_ruleset_cmdlist($ruleset->{raw}, undef, 'raw');
226
227 print "\nip6tables table raw cmdlist:\n";
228 my (undef, $ruleset_changesv6_raw) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{raw}, "ip6tables", 'raw');
229
230
231 if ($ipset_changes || $ruleset_changes || $ruleset_changesv6 || $ebtables_changes || $ruleset_changes_raw || $ruleset_changesv6_raw) {
0c32b7fb
DM
232 print "detected changes\n";
233 } else {
234 print "no changes\n";
235 }
236 if (!$cluster_conf->{options}->{enable}) {
237 print "firewall disabled\n";
238 }
0c32b7fb
DM
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 => {
2e2b71e1 252 additionalProperties => 0,
0c32b7fb
DM
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();
2e2b71e1 268
0c32b7fb
DM
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
c89fafa2
SR
277 if (PVE::Corosync::check_conf_exists(1)) {
278 my $corosync_conf = PVE::Cluster::cfs_read_file("corosync.conf");
279 my $corosync_node_found = 0;
280
281 print "\naccepting corosync traffic from/to:\n";
282
283 PVE::Corosync::for_all_corosync_addresses($corosync_conf, undef, sub {
ae2dc2fa 284 my ($curr_node_name, $curr_node_ip, undef, $key) = @_;
c89fafa2 285
b76a59e4
FG
286 return if $curr_node_name eq $nodename;
287
cb161529 288 $corosync_node_found = 1;
c89fafa2
SR
289
290 $key =~ m/(?:ring|link)(\d+)_addr/;
ae2dc2fa 291 print " - $curr_node_name: $curr_node_ip (link: $1)\n";
c89fafa2
SR
292 });
293
294 if (!$corosync_node_found) {
295 print " - no nodes found\n";
296 }
297 }
298
0c32b7fb
DM
299 return undef;
300 }});
301
302__PACKAGE__->register_method ({
303 name => 'simulate',
304 path => 'simulate',
305 method => 'GET',
4a626429
TL
306 description => "Simulate firewall rules. This does not simulates the kernel 'routing' table,"
307 ." but simply assumes that routing from source zone to destination zone is possible.",
0c32b7fb 308 parameters => {
2e2b71e1 309 additionalProperties => 0,
0c32b7fb
DM
310 properties => {
311 verbose => {
312 description => "Verbose output.",
313 type => 'boolean',
314 optional => 1,
315 default => 0,
316 },
317 from => {
318 description => "Source zone.",
319 type => 'string',
320 pattern => '(host|outside|vm\d+|ct\d+|vmbr\d+/\S+)',
321 optional => 1,
322 default => 'outside',
323 },
324 to => {
325 description => "Destination zone.",
326 type => 'string',
327 pattern => '(host|outside|vm\d+|ct\d+|vmbr\d+/\S+)',
328 optional => 1,
329 default => 'host',
330 },
331 protocol => {
332 description => "Protocol.",
333 type => 'string',
334 pattern => '(tcp|udp)',
335 optional => 1,
336 default => 'tcp',
337 },
338 dport => {
339 description => "Destination port.",
340 type => 'integer',
341 minValue => 1,
342 maxValue => 65535,
343 optional => 1,
344 },
345 sport => {
346 description => "Source port.",
347 type => 'integer',
348 minValue => 1,
349 maxValue => 65535,
350 optional => 1,
351 },
352 source => {
353 description => "Source IP address.",
354 type => 'string', format => 'ipv4',
355 optional => 1,
356 },
357 dest => {
358 description => "Destination IP address.",
359 type => 'string', format => 'ipv4',
360 optional => 1,
361 },
362 },
363 },
364 returns => { type => 'null' },
365 code => sub {
366 my ($param) = @_;
367
368 local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog
369
40af93c4
TL
370 PVE::Firewall::set_verbose($param->{verbose});
371
372 my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = PVE::Firewall::compile();
0c32b7fb 373
40af93c4 374 PVE::FirewallSimulator::debug();
2e2b71e1 375
0c32b7fb
DM
376 my $host_ip = PVE::Cluster::remote_node_ip($nodename);
377
378 PVE::FirewallSimulator::reset_trace();
64e0adf4
AD
379 print Dumper($ruleset->{filter}) if $param->{verbose};
380 print Dumper($ruleset->{raw}) if $param->{verbose};
0c32b7fb
DM
381
382 my $test = {
383 from => $param->{from},
384 to => $param->{to},
385 proto => $param->{protocol} || 'tcp',
386 source => $param->{source},
387 dest => $param->{dest},
388 dport => $param->{dport},
389 sport => $param->{sport},
390 };
391
392 if (!defined($test->{to})) {
393 $test->{to} = 'host';
2e2b71e1
SR
394 PVE::FirewallSimulator::add_trace("Set Zone: to => '$test->{to}'\n");
395 }
0c32b7fb
DM
396 if (!defined($test->{from})) {
397 $test->{from} = 'outside',
2e2b71e1 398 PVE::FirewallSimulator::add_trace("Set Zone: from => '$test->{from}'\n");
0c32b7fb
DM
399 }
400
401 my $vmdata = PVE::Firewall::read_local_vm_config();
402
403 print "Test packet:\n";
404
405 foreach my $k (qw(from to proto source dest dport sport)) {
406 printf(" %-8s: %s\n", $k, $test->{$k}) if defined($test->{$k});
407 }
408
409 $test->{action} = 'QUERY';
410
4a626429
TL
411 my $res = PVE::FirewallSimulator::simulate_firewall(
412 $ruleset->{filter}, $ipset_ruleset, $host_ip, $vmdata, $test);
2e2b71e1 413
0c32b7fb
DM
414 print "ACTION: $res\n";
415
416 return undef;
417 }});
418
419our $cmddef = {
420 start => [ __PACKAGE__, 'start', []],
421 restart => [ __PACKAGE__, 'restart', []],
422 stop => [ __PACKAGE__, 'stop', []],
423 compile => [ __PACKAGE__, 'compile', []],
424 simulate => [ __PACKAGE__, 'simulate', []],
425 localnet => [ __PACKAGE__, 'localnet', []],
426 status => [ __PACKAGE__, 'status', [], undef, sub {
427 my $res = shift;
428 my $status = ($res->{enable} ? "enabled" : "disabled") . '/' . $res->{status};
2e2b71e1 429
0c32b7fb
DM
430 if ($res->{changes}) {
431 print "Status: $status (pending changes)\n";
432 } else {
433 print "Status: $status\n";
434 }
435 }],
436 };
437
4381;