#!/usr/bin/perl use strict; use warnings; use PVE::SafeSyslog; use POSIX ":sys_wait_h"; use Fcntl ':flock'; use Getopt::Long; use Time::HiRes qw (gettimeofday); use PVE::Tools qw(dir_glob_foreach file_read_firstline); use PVE::INotify; use PVE::Cluster qw(cfs_read_file); use PVE::RPCEnvironment; use PVE::CLIHandler; use PVE::Firewall; use base qw(PVE::CLIHandler); my $pve_firewall_pidfile = "/var/run/pve-firewall.pid"; $SIG{'__WARN__'} = sub { my $err = $@; my $t = $_[0]; chomp $t; print "$t\n"; syslog('warning', "WARNING: %s", $t); $@ = $err; }; initlog('pve-firewall'); $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin'; die "please run as root\n" if $> != 0; PVE::INotify::inotify_init(); my $rpcenv = PVE::RPCEnvironment->init('cli'); $rpcenv->init_request(); $rpcenv->set_language($ENV{LANG}); $rpcenv->set_user('root@pam'); my $commandline = [$0, @ARGV]; $0 = "pve-firewall"; sub restart_server { my ($waittime) = @_; syslog('info', "server shutdown (restart)"); $ENV{RESTART_PVE_FIREWALL} = 1; sleep($waittime) if $waittime; # avoid high server load due to restarts exec (@$commandline); exit (-1); # never reached? } sub cleanup { unlink "$pve_firewall_pidfile.lock"; unlink $pve_firewall_pidfile; } sub lockpidfile { my $pidfile = shift; my $lkfn = "$pidfile.lock"; if (!open (FLCK, ">>$lkfn")) { my $msg = "can't aquire lock on file '$lkfn' - $!"; syslog ('err', $msg); die "ERROR: $msg\n"; } if (!flock (FLCK, LOCK_EX|LOCK_NB)) { close (FLCK); my $msg = "can't aquire lock '$lkfn' - $!"; syslog ('err', $msg); die "ERROR: $msg\n"; } } sub writepidfile { my $pidfile = shift; if (!open (PIDFH, ">$pidfile")) { my $msg = "can't open pid file '$pidfile' - $!"; syslog ('err', $msg); die "ERROR: $msg\n"; } print PIDFH "$$\n"; close (PIDFH); } my $restart_request = 0; my $next_update = 0; my $cycle = 0; my $updatetime = 10; my $initial_memory_usage; sub run_server { my ($param) = @_; # try to get the lock lockpidfile($pve_firewall_pidfile); # run in background my $spid; my $restart = $ENV{RESTART_PVE_FIREWALL}; delete $ENV{RESTART_PVE_FIREWALL}; PVE::Cluster::cfs_update(); PVE::Firewall::init(); if (!$param->{debug}) { open STDIN, '/dev/null' || die "can't write /dev/null"; } if (!$restart && !$param->{debug}) { $spid = fork(); if (!defined ($spid)) { my $msg = "can't put server into background - fork failed"; syslog('err', $msg); die "ERROR: $msg\n"; } elsif ($spid) { # parent exit (0); } } writepidfile($pve_firewall_pidfile); open STDERR, '>&STDOUT' || die "can't close STDERR\n"; $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = sub { syslog('info' , "server closing"); $SIG{INT} = 'DEFAULT'; # wait for children 1 while (waitpid(-1, POSIX::WNOHANG()) > 0); syslog('info' , "clear firewall rules"); eval { PVE::Firewall::remove_pvefw_chains(); die "STOP";}; warn $@ if $@; cleanup(); exit (0); }; $SIG{HUP} = sub { # wake up process, so this forces an immediate firewall rules update syslog('info' , "received signal HUP (restart)"); $restart_request = 1; }; if ($restart) { syslog('info' , "restarting server"); } else { syslog('info' , "starting server"); } for (;;) { # forever eval { local $SIG{'__WARN__'} = 'IGNORE'; # do not fill up logs $next_update = time() + $updatetime; my ($ccsec, $cusec) = gettimeofday (); eval { PVE::Cluster::cfs_update(); PVE::Firewall::update(); }; my $err = $@; if ($err) { syslog('err', "status update error: $err"); } my ($ccsec_end, $cusec_end) = gettimeofday (); my $cptime = ($ccsec_end-$ccsec) + ($cusec_end - $cusec)/1000000; syslog('info', sprintf("firewall update time (%.3f seconds)", $cptime)) if ($cptime > 5); $cycle++; my $mem = PVE::ProcFSTools::read_memory_usage(); if (!defined($initial_memory_usage) || ($cycle < 10)) { $initial_memory_usage = $mem->{resident}; } else { my $diff = $mem->{resident} - $initial_memory_usage; if ($diff > 5*1024*1024) { syslog ('info', "restarting server after $cycle cycles to " . "reduce memory usage (free $mem->{resident} ($diff) bytes)"); restart_server(); } } my $wcount = 0; while ((time() < $next_update) && ($wcount < $updatetime) && # protect against time wrap !$restart_request) { $wcount++; sleep (1); }; restart_server() if $restart_request; }; my $err = $@; if ($err) { syslog ('err', "ERROR: $err"); restart_server(5); exit (0); } } } __PACKAGE__->register_method ({ name => 'start', path => 'start', method => 'POST', description => "Start the Proxmox VE firewall service.", parameters => { additionalProperties => 0, properties => { debug => { description => "Debug mode - stay in foreground", type => "boolean", optional => 1, default => 0, }, }, }, returns => { type => 'null' }, code => sub { my ($param) = @_; run_server($param); return undef; }}); __PACKAGE__->register_method ({ name => 'stop', path => 'stop', method => 'POST', description => "Stop firewall. This removes all Proxmox VE related iptable rules. The host is unprotected afterwards.", parameters => { additionalProperties => 0, properties => {}, }, returns => { type => 'null' }, code => sub { my ($param) = @_; my $pid = int(PVE::Tools::file_read_firstline($pve_firewall_pidfile) || 0); if ($pid) { if (PVE::ProcFSTools::check_process_running($pid)) { kill(15, $pid); # send TERM signal # give max 5 seconds to shut down for (my $i = 0; $i < 5; $i++) { last if !PVE::ProcFSTools::check_process_running($pid); sleep (1); } # to be sure kill(9, $pid); waitpid($pid, 0); } if (-f $pve_firewall_pidfile) { # try to get the lock lockpidfile($pve_firewall_pidfile); cleanup(); } } return undef; }}); __PACKAGE__->register_method ({ name => 'status', path => 'status', method => 'GET', description => "Get firewall status.", parameters => { additionalProperties => 0, properties => {}, }, returns => { type => 'object', additionalProperties => 0, properties => { status => { type => 'string', enum => ['unknown', 'stopped', 'active'], }, changes => { description => "Set when there are pending changes.", type => 'boolean', optional => 1, } }, }, code => sub { my ($param) = @_; local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog my $code = sub { my $pid = int(PVE::Tools::file_read_firstline($pve_firewall_pidfile) || 0); my $running = PVE::ProcFSTools::check_process_running($pid); my $status = $running ? 'active' : 'stopped'; my $res = { status => $status }; if ($status eq 'active') { my ($ruleset, $ipset_ruleset) = PVE::Firewall::compile(); my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset); my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset); $res->{changes} = ($ipset_changes || $ruleset_changes) ? 1 : 0; } return $res; }; return PVE::Firewall::run_locked($code); }}); __PACKAGE__->register_method ({ name => 'compile', path => 'compile', method => 'POST', description => "Compile and print firewall rules. This is useful for testing.", parameters => { additionalProperties => 0, properties => {}, }, returns => { type => 'null' }, code => sub { my ($param) = @_; local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog my $code = sub { my ($ruleset, $ipset_ruleset) = PVE::Firewall::compile(); my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, 1); my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, 1); if ($ipset_changes || $ruleset_changes) { print "detected changes\n"; } else { print "no changes\n"; } }; PVE::Firewall::run_locked($code); return undef; }}); my $nodename = PVE::INotify::nodename(); my $cmddef = { start => [ __PACKAGE__, 'start', []], stop => [ __PACKAGE__, 'stop', []], compile => [ __PACKAGE__, 'compile', []], status => [ __PACKAGE__, 'status', [], undef, sub { my $res = shift; if ($res->{changes}) { print "Status: $res->{status} (pending changes)\n"; } else { print "Status: $res->{status}\n"; } }], }; my $cmd = shift; PVE::CLIHandler::handle_cmd($cmddef, $0, $cmd, \@ARGV, undef, $0); exit (0); __END__ =head1 NAME pve-firewall - PVE Firewall Daemon =head1 SYNOPSIS =include synopsis =head1 DESCRIPTION This service updates iptables rules periodically. =include pve_copyright