Commit | Line | Data |
---|---|---|
0c32b7fb DM |
1 | package PVE::Service::pve_firewall; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
0c32b7fb | 5 | |
4a626429 TL |
6 | use Data::Dumper; |
7 | use Time::HiRes qw (gettimeofday usleep); | |
8 | ||
9 | use PVE::CLIHandler; | |
0c32b7fb | 10 | use PVE::Cluster qw(cfs_read_file); |
c89fafa2 | 11 | use PVE::Corosync; |
4a626429 TL |
12 | use PVE::Daemon; |
13 | use PVE::INotify; | |
14 | use PVE::ProcFSTools; | |
0c32b7fb | 15 | use PVE::RPCEnvironment; |
4a626429 TL |
16 | use PVE::SafeSyslog; |
17 | use PVE::Tools qw(dir_glob_foreach file_read_firstline); | |
18 | ||
0c32b7fb DM |
19 | use PVE::Firewall; |
20 | use PVE::FirewallSimulator; | |
0c32b7fb DM |
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 { | |
0c32b7fb | 33 | PVE::Cluster::cfs_update(); |
2e2b71e1 | 34 | |
0c32b7fb DM |
35 | PVE::Firewall::init(); |
36 | } | |
37 | ||
7761d1a1 | 38 | my ($next_update, $cycle, $restart_request) = (0, 0, 0); |
0c32b7fb DM |
39 | my $updatetime = 10; |
40 | ||
41 | my $initial_memory_usage; | |
42 | ||
43 | sub shutdown { | |
44 | my ($self) = @_; | |
45 | ||
7761d1a1 | 46 | syslog('info' , "server shutting down"); |
0c32b7fb DM |
47 | |
48 | # wait for children | |
49 | 1 while (waitpid(-1, POSIX::WNOHANG()) > 0); | |
2e2b71e1 | 50 | |
7761d1a1 | 51 | syslog('info' , "clear PVE-generated firewall rules"); |
0c32b7fb DM |
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 | |
0c32b7fb DM |
71 | $next_update = time() + $updatetime; |
72 | ||
73 | my ($ccsec, $cusec) = gettimeofday (); | |
74 | eval { | |
75 | PVE::Cluster::cfs_update(); | |
76 | PVE::Firewall::update(); | |
77 | }; | |
7761d1a1 | 78 | if (my $err = $@) { |
0c32b7fb DM |
79 | syslog('err', "status update error: $err"); |
80 | } | |
81 | ||
7761d1a1 | 82 | my ($ccsec_end, $cusec_end) = gettimeofday(); |
0c32b7fb DM |
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(); | |
2e2b71e1 | 91 | |
0c32b7fb DM |
92 | if (!defined($initial_memory_usage) || ($cycle < 10)) { |
93 | $initial_memory_usage = $mem->{resident}; | |
94 | } else { | |
95 | my $diff = $mem->{resident} - $initial_memory_usage; | |
7761d1a1 | 96 | if ($diff > 5 * 1024 * 1024) { |
0c32b7fb DM |
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; | |
2e2b71e1 | 104 | while ((time() < $next_update) && |
0c32b7fb DM |
105 | ($wcount < $updatetime) && # protect against time wrap |
106 | !$restart_request) { $wcount++; sleep (1); }; | |
2e2b71e1 | 107 | |
0c32b7fb DM |
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."); | |
4a626429 TL |
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 | ); | |
0c32b7fb DM |
118 | |
119 | __PACKAGE__->register_method ({ | |
120 | name => 'status', | |
121 | path => 'status', | |
122 | method => 'GET', | |
123 | description => "Get firewall status.", | |
124 | parameters => { | |
2e2b71e1 | 125 | additionalProperties => 0, |
0c32b7fb DM |
126 | properties => {}, |
127 | }, | |
2e2b71e1 | 128 | returns => { |
0c32b7fb DM |
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 | ||
40af93c4 TL |
158 | PVE::Firewall::set_verbose(1); # show syntax errors |
159 | ||
160 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef); | |
0c32b7fb DM |
161 | $res->{enable} = $cluster_conf->{options}->{enable} ? 1 : 0; |
162 | ||
163 | if ($status eq 'running') { | |
2e2b71e1 | 164 | |
40af93c4 | 165 | my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = PVE::Firewall::compile($cluster_conf, undef, undef); |
0c32b7fb | 166 | |
40af93c4 TL |
167 | PVE::Firewall::set_verbose(0); # do not show iptables details |
168 | my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset); | |
64e0adf4 AD |
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'); | |
40af93c4 | 173 | my (undef, $ebtables_changes) = PVE::Firewall::get_ebtables_cmdlist($ebtables_ruleset); |
0c32b7fb | 174 | |
64e0adf4 | 175 | $res->{changes} = ($ipset_changes || $ruleset_changes || $ruleset_changesv6 || $ebtables_changes || $ruleset_changes_raw || $ruleset_changesv6_raw) ? 1 : 0; |
0c32b7fb DM |
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 => { | |
2e2b71e1 | 190 | additionalProperties => 0, |
0c32b7fb DM |
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 | ||
40af93c4 | 202 | PVE::Firewall::set_verbose(1); |
0c32b7fb | 203 | |
40af93c4 TL |
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); | |
0c32b7fb DM |
206 | |
207 | print "ipset cmdlist:\n"; | |
40af93c4 | 208 | my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset); |
0c32b7fb DM |
209 | |
210 | print "\niptables cmdlist:\n"; | |
64e0adf4 | 211 | my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset->{filter}); |
0c32b7fb DM |
212 | |
213 | print "\nip6tables cmdlist:\n"; | |
64e0adf4 | 214 | my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{filter}, "ip6tables"); |
0c32b7fb | 215 | |
151c209e | 216 | print "\nebtables cmdlist:\n"; |
40af93c4 | 217 | my (undef, $ebtables_changes) = PVE::Firewall::get_ebtables_cmdlist($ebtables_ruleset); |
151c209e | 218 | |
64e0adf4 AD |
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) { | |
0c32b7fb DM |
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 | } | |
0c32b7fb DM |
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 => { | |
2e2b71e1 | 247 | additionalProperties => 0, |
0c32b7fb DM |
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(); | |
2e2b71e1 | 263 | |
0c32b7fb DM |
264 | my $localnet = PVE::Firewall::local_network() || '127.0.0.0/8'; |
265 | print "network auto detect: $localnet\n"; | |
7761d1a1 TL |
266 | if (my $local_network = $cluster_conf->{aliases}->{local_network}) { |
267 | print "using user defined local_network: $local_network->{cidr}\n"; | |
0c32b7fb DM |
268 | } else { |
269 | print "using detected local_network: $localnet\n"; | |
270 | } | |
271 | ||
c89fafa2 SR |
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 { | |
ae2dc2fa | 279 | my ($curr_node_name, $curr_node_ip, undef, $key) = @_; |
c89fafa2 | 280 | |
b76a59e4 FG |
281 | return if $curr_node_name eq $nodename; |
282 | ||
cb161529 | 283 | $corosync_node_found = 1; |
c89fafa2 SR |
284 | |
285 | $key =~ m/(?:ring|link)(\d+)_addr/; | |
ae2dc2fa | 286 | print " - $curr_node_name: $curr_node_ip (link: $1)\n"; |
c89fafa2 SR |
287 | }); |
288 | ||
289 | if (!$corosync_node_found) { | |
290 | print " - no nodes found\n"; | |
291 | } | |
292 | } | |
293 | ||
0c32b7fb DM |
294 | return undef; |
295 | }}); | |
296 | ||
297 | __PACKAGE__->register_method ({ | |
298 | name => 'simulate', | |
299 | path => 'simulate', | |
300 | method => 'GET', | |
4a626429 TL |
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.", | |
0c32b7fb | 303 | parameters => { |
2e2b71e1 | 304 | additionalProperties => 0, |
0c32b7fb DM |
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 | ||
40af93c4 TL |
365 | PVE::Firewall::set_verbose($param->{verbose}); |
366 | ||
367 | my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = PVE::Firewall::compile(); | |
0c32b7fb | 368 | |
40af93c4 | 369 | PVE::FirewallSimulator::debug(); |
2e2b71e1 | 370 | |
0c32b7fb DM |
371 | my $host_ip = PVE::Cluster::remote_node_ip($nodename); |
372 | ||
373 | PVE::FirewallSimulator::reset_trace(); | |
64e0adf4 AD |
374 | print Dumper($ruleset->{filter}) if $param->{verbose}; |
375 | print Dumper($ruleset->{raw}) if $param->{verbose}; | |
0c32b7fb DM |
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'; | |
2e2b71e1 SR |
389 | PVE::FirewallSimulator::add_trace("Set Zone: to => '$test->{to}'\n"); |
390 | } | |
0c32b7fb DM |
391 | if (!defined($test->{from})) { |
392 | $test->{from} = 'outside', | |
2e2b71e1 | 393 | PVE::FirewallSimulator::add_trace("Set Zone: from => '$test->{from}'\n"); |
0c32b7fb DM |
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 | ||
4a626429 TL |
406 | my $res = PVE::FirewallSimulator::simulate_firewall( |
407 | $ruleset->{filter}, $ipset_ruleset, $host_ip, $vmdata, $test); | |
2e2b71e1 | 408 | |
0c32b7fb DM |
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}; | |
2e2b71e1 | 424 | |
0c32b7fb DM |
425 | if ($res->{changes}) { |
426 | print "Status: $status (pending changes)\n"; | |
427 | } else { | |
428 | print "Status: $status\n"; | |
429 | } | |
430 | }], | |
431 | }; | |
432 | ||
433 | 1; |