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