+#!/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};
+
+ if (!$param->{debug}) {
+ open STDIN, '</dev/null' || die "can't read /dev/null";
+ open STDOUT, '>/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 will remove all rules installed by this script. 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 amd print firewall rules. This is only 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
+
+pvestatd - PVE Firewall Daemon
+
+=head1 SYNOPSIS
+
+pve-firewall
+
+=head1 DESCRIPTION
+
+This service updates iptables rules periodically.
+
+
+
+