]>
Commit | Line | Data |
---|---|---|
e2beb7aa DM |
1 | #!/usr/bin/perl |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | use PVE::SafeSyslog; | |
6 | use POSIX ":sys_wait_h"; | |
7 | use Fcntl ':flock'; | |
8 | use Getopt::Long; | |
9 | use Time::HiRes qw (gettimeofday); | |
10 | use PVE::Tools qw(dir_glob_foreach file_read_firstline); | |
11 | use PVE::INotify; | |
12 | use PVE::Cluster qw(cfs_read_file); | |
13 | use PVE::RPCEnvironment; | |
14 | use PVE::CLIHandler; | |
15 | use PVE::Firewall; | |
16 | ||
17 | use base qw(PVE::CLIHandler); | |
18 | ||
19 | my $pve_firewall_pidfile = "/var/run/pve-firewall.pid"; | |
20 | ||
21 | $SIG{'__WARN__'} = sub { | |
22 | my $err = $@; | |
23 | my $t = $_[0]; | |
24 | chomp $t; | |
25 | print "$t\n"; | |
26 | syslog('warning', "WARNING: %s", $t); | |
27 | $@ = $err; | |
28 | }; | |
29 | ||
30 | initlog('pve-firewall'); | |
31 | ||
32 | $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin'; | |
33 | ||
34 | die "please run as root\n" if $> != 0; | |
35 | ||
36 | PVE::INotify::inotify_init(); | |
37 | ||
38 | my $rpcenv = PVE::RPCEnvironment->init('cli'); | |
39 | ||
40 | $rpcenv->init_request(); | |
41 | $rpcenv->set_language($ENV{LANG}); | |
42 | $rpcenv->set_user('root@pam'); | |
43 | ||
44 | my $commandline = [$0, @ARGV]; | |
45 | ||
46 | $0 = "pve-firewall"; | |
47 | ||
48 | sub restart_server { | |
49 | my ($waittime) = @_; | |
50 | ||
51 | syslog('info', "server shutdown (restart)"); | |
52 | ||
53 | $ENV{RESTART_PVE_FIREWALL} = 1; | |
54 | ||
55 | sleep($waittime) if $waittime; # avoid high server load due to restarts | |
56 | ||
57 | exec (@$commandline); | |
58 | exit (-1); # never reached? | |
59 | } | |
60 | ||
61 | sub cleanup { | |
62 | unlink "$pve_firewall_pidfile.lock"; | |
63 | unlink $pve_firewall_pidfile; | |
64 | } | |
65 | ||
66 | sub lockpidfile { | |
67 | my $pidfile = shift; | |
68 | my $lkfn = "$pidfile.lock"; | |
69 | ||
70 | if (!open (FLCK, ">>$lkfn")) { | |
71 | my $msg = "can't aquire lock on file '$lkfn' - $!"; | |
72 | syslog ('err', $msg); | |
73 | die "ERROR: $msg\n"; | |
74 | } | |
75 | ||
76 | if (!flock (FLCK, LOCK_EX|LOCK_NB)) { | |
77 | close (FLCK); | |
78 | my $msg = "can't aquire lock '$lkfn' - $!"; | |
79 | syslog ('err', $msg); | |
80 | die "ERROR: $msg\n"; | |
81 | } | |
82 | } | |
83 | ||
84 | sub writepidfile { | |
85 | my $pidfile = shift; | |
86 | ||
87 | if (!open (PIDFH, ">$pidfile")) { | |
88 | my $msg = "can't open pid file '$pidfile' - $!"; | |
89 | syslog ('err', $msg); | |
90 | die "ERROR: $msg\n"; | |
91 | } | |
92 | print PIDFH "$$\n"; | |
93 | close (PIDFH); | |
94 | } | |
95 | ||
96 | my $restart_request = 0; | |
97 | my $next_update = 0; | |
98 | ||
99 | my $cycle = 0; | |
100 | my $updatetime = 10; | |
101 | ||
102 | my $initial_memory_usage; | |
103 | ||
104 | sub run_server { | |
105 | my ($param) = @_; | |
106 | ||
107 | # try to get the lock | |
108 | lockpidfile($pve_firewall_pidfile); | |
109 | ||
110 | # run in background | |
111 | my $spid; | |
112 | ||
113 | my $restart = $ENV{RESTART_PVE_FIREWALL}; | |
114 | ||
115 | delete $ENV{RESTART_PVE_FIREWALL}; | |
116 | ||
117 | if (!$param->{debug}) { | |
118 | open STDIN, '</dev/null' || die "can't read /dev/null"; | |
119 | open STDOUT, '>/dev/null' || die "can't write /dev/null"; | |
120 | } | |
121 | ||
122 | if (!$restart && !$param->{debug}) { | |
123 | $spid = fork(); | |
124 | if (!defined ($spid)) { | |
125 | my $msg = "can't put server into background - fork failed"; | |
126 | syslog('err', $msg); | |
127 | die "ERROR: $msg\n"; | |
128 | } elsif ($spid) { # parent | |
129 | exit (0); | |
130 | } | |
131 | } | |
132 | ||
133 | writepidfile($pve_firewall_pidfile); | |
134 | ||
135 | open STDERR, '>&STDOUT' || die "can't close STDERR\n"; | |
136 | ||
137 | $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = sub { | |
138 | syslog('info' , "server closing"); | |
139 | ||
140 | $SIG{INT} = 'DEFAULT'; | |
141 | ||
142 | # wait for children | |
143 | 1 while (waitpid(-1, POSIX::WNOHANG()) > 0); | |
144 | ||
145 | syslog('info' , "clear firewall rules"); | |
146 | eval { PVE::Firewall::remove_pvefw_chains(); die "STOP";}; | |
147 | warn $@ if $@; | |
148 | ||
149 | cleanup(); | |
150 | ||
151 | exit (0); | |
152 | }; | |
153 | ||
154 | $SIG{HUP} = sub { | |
155 | # wake up process, so this forces an immediate firewall rules update | |
156 | syslog('info' , "received signal HUP (restart)"); | |
157 | $restart_request = 1; | |
158 | }; | |
159 | ||
160 | if ($restart) { | |
161 | syslog('info' , "restarting server"); | |
162 | } else { | |
163 | syslog('info' , "starting server"); | |
164 | } | |
165 | ||
166 | for (;;) { # forever | |
167 | ||
168 | eval { | |
169 | ||
170 | local $SIG{'__WARN__'} = 'IGNORE'; # do not fill up logs | |
171 | ||
172 | $next_update = time() + $updatetime; | |
173 | ||
174 | my ($ccsec, $cusec) = gettimeofday (); | |
175 | eval { | |
176 | PVE::Cluster::cfs_update(); | |
177 | PVE::Firewall::update(); | |
178 | }; | |
179 | my $err = $@; | |
180 | ||
181 | if ($err) { | |
182 | syslog('err', "status update error: $err"); | |
183 | } | |
184 | ||
185 | my ($ccsec_end, $cusec_end) = gettimeofday (); | |
186 | my $cptime = ($ccsec_end-$ccsec) + ($cusec_end - $cusec)/1000000; | |
187 | ||
188 | syslog('info', sprintf("firewall update time (%.3f seconds)", $cptime)) | |
189 | if ($cptime > 5); | |
190 | ||
191 | $cycle++; | |
192 | ||
193 | my $mem = PVE::ProcFSTools::read_memory_usage(); | |
194 | ||
195 | if (!defined($initial_memory_usage) || ($cycle < 10)) { | |
196 | $initial_memory_usage = $mem->{resident}; | |
197 | } else { | |
198 | my $diff = $mem->{resident} - $initial_memory_usage; | |
199 | if ($diff > 5*1024*1024) { | |
200 | syslog ('info', "restarting server after $cycle cycles to " . | |
201 | "reduce memory usage (free $mem->{resident} ($diff) bytes)"); | |
202 | restart_server(); | |
203 | } | |
204 | } | |
205 | ||
206 | my $wcount = 0; | |
207 | while ((time() < $next_update) && | |
208 | ($wcount < $updatetime) && # protect against time wrap | |
209 | !$restart_request) { $wcount++; sleep (1); }; | |
210 | ||
211 | restart_server() if $restart_request; | |
212 | }; | |
213 | ||
214 | my $err = $@; | |
215 | ||
216 | if ($err) { | |
217 | syslog ('err', "ERROR: $err"); | |
218 | restart_server(5); | |
219 | exit (0); | |
220 | } | |
221 | } | |
222 | } | |
223 | ||
224 | __PACKAGE__->register_method ({ | |
225 | name => 'start', | |
226 | path => 'start', | |
227 | method => 'POST', | |
228 | description => "Start the Proxmox VE firewall service.", | |
229 | parameters => { | |
230 | additionalProperties => 0, | |
231 | properties => { | |
232 | debug => { | |
233 | description => "Debug mode - stay in foreground", | |
234 | type => "boolean", | |
235 | optional => 1, | |
236 | default => 0, | |
237 | }, | |
238 | }, | |
239 | }, | |
240 | returns => { type => 'null' }, | |
241 | ||
242 | code => sub { | |
243 | my ($param) = @_; | |
244 | ||
245 | run_server($param); | |
246 | ||
247 | return undef; | |
248 | }}); | |
249 | ||
250 | __PACKAGE__->register_method ({ | |
251 | name => 'stop', | |
252 | path => 'stop', | |
253 | method => 'POST', | |
16adff04 | 254 | description => "Stop firewall. This removes all Proxmox VE related iptable rules. The host is unprotected afterwards.", |
e2beb7aa DM |
255 | parameters => { |
256 | additionalProperties => 0, | |
257 | properties => {}, | |
258 | }, | |
259 | returns => { type => 'null' }, | |
260 | ||
261 | code => sub { | |
262 | my ($param) = @_; | |
263 | ||
264 | my $pid = int(PVE::Tools::file_read_firstline($pve_firewall_pidfile) || 0); | |
265 | ||
266 | if ($pid) { | |
267 | if (PVE::ProcFSTools::check_process_running($pid)) { | |
268 | kill(15, $pid); # send TERM signal | |
269 | # give max 5 seconds to shut down | |
270 | for (my $i = 0; $i < 5; $i++) { | |
271 | last if !PVE::ProcFSTools::check_process_running($pid); | |
272 | sleep (1); | |
273 | } | |
274 | ||
275 | # to be sure | |
276 | kill(9, $pid); | |
277 | waitpid($pid, 0); | |
278 | } | |
279 | if (-f $pve_firewall_pidfile) { | |
280 | # try to get the lock | |
281 | lockpidfile($pve_firewall_pidfile); | |
282 | cleanup(); | |
283 | } | |
284 | } | |
285 | ||
286 | return undef; | |
287 | }}); | |
288 | ||
289 | __PACKAGE__->register_method ({ | |
290 | name => 'status', | |
291 | path => 'status', | |
292 | method => 'GET', | |
293 | description => "Get firewall status.", | |
294 | parameters => { | |
295 | additionalProperties => 0, | |
296 | properties => {}, | |
297 | }, | |
298 | returns => { | |
299 | type => 'object', | |
300 | additionalProperties => 0, | |
301 | properties => { | |
302 | status => { | |
303 | type => 'string', | |
304 | enum => ['unknown', 'stopped', 'active'], | |
305 | }, | |
306 | changes => { | |
307 | description => "Set when there are pending changes.", | |
308 | type => 'boolean', | |
309 | optional => 1, | |
310 | } | |
311 | }, | |
312 | }, | |
313 | code => sub { | |
314 | my ($param) = @_; | |
315 | ||
316 | local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog | |
317 | ||
318 | my $code = sub { | |
319 | ||
320 | my $pid = int(PVE::Tools::file_read_firstline($pve_firewall_pidfile) || 0); | |
321 | my $running = PVE::ProcFSTools::check_process_running($pid); | |
322 | ||
323 | my $status = $running ? 'active' : 'stopped'; | |
324 | ||
325 | my $res = { status => $status }; | |
326 | if ($status eq 'active') { | |
327 | my ($ruleset, $ipset_ruleset) = PVE::Firewall::compile(); | |
328 | ||
329 | my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset); | |
330 | my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset); | |
331 | ||
332 | $res->{changes} = ($ipset_changes || $ruleset_changes) ? 1 : 0; | |
333 | } | |
334 | ||
335 | return $res; | |
336 | }; | |
337 | ||
338 | return PVE::Firewall::run_locked($code); | |
339 | }}); | |
340 | ||
341 | __PACKAGE__->register_method ({ | |
342 | name => 'compile', | |
343 | path => 'compile', | |
344 | method => 'POST', | |
16adff04 | 345 | description => "Compile and print firewall rules. This is useful for testing.", |
e2beb7aa DM |
346 | parameters => { |
347 | additionalProperties => 0, | |
348 | properties => {}, | |
349 | }, | |
350 | returns => { type => 'null' }, | |
351 | ||
352 | code => sub { | |
353 | my ($param) = @_; | |
354 | ||
355 | local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog | |
356 | ||
357 | my $code = sub { | |
358 | my ($ruleset, $ipset_ruleset) = PVE::Firewall::compile(); | |
359 | ||
360 | my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, 1); | |
361 | my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, 1); | |
362 | if ($ipset_changes || $ruleset_changes) { | |
363 | print "detected changes\n"; | |
364 | } else { | |
365 | print "no changes\n"; | |
366 | } | |
367 | }; | |
368 | ||
369 | PVE::Firewall::run_locked($code); | |
370 | ||
371 | return undef; | |
372 | }}); | |
373 | ||
374 | my $nodename = PVE::INotify::nodename(); | |
375 | ||
376 | my $cmddef = { | |
377 | start => [ __PACKAGE__, 'start', []], | |
378 | stop => [ __PACKAGE__, 'stop', []], | |
379 | compile => [ __PACKAGE__, 'compile', []], | |
380 | status => [ __PACKAGE__, 'status', [], undef, sub { | |
381 | my $res = shift; | |
382 | if ($res->{changes}) { | |
383 | print "Status: $res->{status} (pending changes)\n"; | |
384 | } else { | |
385 | print "Status: $res->{status}\n"; | |
386 | } | |
387 | }], | |
388 | }; | |
389 | ||
390 | my $cmd = shift; | |
391 | ||
392 | PVE::CLIHandler::handle_cmd($cmddef, $0, $cmd, \@ARGV, undef, $0); | |
393 | ||
394 | exit (0); | |
395 | ||
396 | __END__ | |
397 | ||
398 | =head1 NAME | |
399 | ||
16adff04 | 400 | pve-firewall - PVE Firewall Daemon |
e2beb7aa DM |
401 | |
402 | =head1 SYNOPSIS | |
403 | ||
16adff04 | 404 | =include synopsis |
e2beb7aa DM |
405 | |
406 | =head1 DESCRIPTION | |
407 | ||
408 | This service updates iptables rules periodically. | |
409 | ||
16adff04 | 410 | =include pve_copyright |