X-Git-Url: https://git.proxmox.com/?p=pve-firewall.git;a=blobdiff_plain;f=src%2Fpve-firewall;h=5b62430fc6e7b6070f69e7cd88bdfb30c28c4509;hp=f2ccd307369ebfa4dcfa08d7b389813b3cef4b92;hb=02ecc1827e094ab86f643ed236d956ccc8c0b219;hpb=259db1e6564e2b01a3bd256ee0855e6bf3cd0d63 diff --git a/src/pve-firewall b/src/pve-firewall index f2ccd30..5b62430 100755 --- a/src/pve-firewall +++ b/src/pve-firewall @@ -3,596 +3,23 @@ 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 PVE::FirewallSimulator; -use Data::Dumper; - -use base qw(PVE::CLIHandler); - -my $pve_firewall_pidfile = "/var/run/pve-firewall.pid"; +use PVE::Service::pve_firewall; $SIG{'__WARN__'} = sub { my $err = $@; my $t = $_[0]; chomp $t; - print "$t\n"; - syslog('warning', "WARNING: %s", $t); + print STDERR "$t\n"; + syslog('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 $nodename = PVE::INotify::nodename(); - -my $commandline = [$0, @ARGV]; - -$0 = "pve-firewall"; - -mkdir "/var/lib/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 - - PVE::INotify::inotify_close(); - - 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; - }}); +my $prepare = sub { + my $rpcenv = PVE::RPCEnvironment->init('cli'); -__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', 'running'], - }, - enable => { - description => "Firewall is enabled (in 'cluster.fw')", - type => 'boolean', - }, - 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 ? 'running' : 'stopped'; - - my $res = { status => $status }; - - my $verbose = 1; # show syntax errors - my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, $verbose); - $res->{enable} = $cluster_conf->{options}->{enable} ? 1 : 0; - - if ($status eq 'running') { - - my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile($cluster_conf, undef, undef, $verbose); - - $verbose = 0; # do not show iptables details - my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, $verbose); - my ($test, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, $verbose); - my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables"); - - $res->{changes} = ($ipset_changes || $ruleset_changes || $ruleset_changesv6) ? 1 : 0; - } - - return $res; - }; - - return PVE::Firewall::run_locked($code); - }}); - -__PACKAGE__->register_method ({ - name => 'compile', - path => 'compile', - method => 'GET', - 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 $verbose = 1; - - my $cluster_conf = PVE::Firewall::load_clusterfw_conf(undef, $verbose); - my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile($cluster_conf, undef, undef, $verbose); - - print "ipset cmdlist:\n"; - my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset, $verbose); - - print "\niptables cmdlist:\n"; - my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset, $verbose); - - print "\nip6tables cmdlist:\n"; - my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables"); - - if ($ipset_changes || $ruleset_changes || $ruleset_changesv6) { - print "detected changes\n"; - } else { - print "no changes\n"; - } - if (!$cluster_conf->{options}->{enable}) { - print "firewall disabled\n"; - } - - }; - - PVE::Firewall::run_locked($code); - - return undef; - }}); - -__PACKAGE__->register_method ({ - name => 'localnet', - path => 'localnet', - method => 'GET', - description => "Print information about local network.", - parameters => { - additionalProperties => 0, - properties => {}, - }, - returns => { type => 'null' }, - code => sub { - my ($param) = @_; - - local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog - - my $nodename = PVE::INotify::nodename(); - print "local hostname: $nodename\n"; - - my $ip = PVE::Cluster::remote_node_ip($nodename); - print "local IP address: $ip\n"; - - my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); - - my $localnet = PVE::Firewall::local_network() || '127.0.0.0/8'; - print "network auto detect: $localnet\n"; - if ($cluster_conf->{aliases}->{local_network}) { - print "using user defined local_network: $cluster_conf->{aliases}->{local_network}->{cidr}\n"; - } else { - print "using detected local_network: $localnet\n"; - } - - return undef; - }}); - -__PACKAGE__->register_method ({ - name => 'simulate', - path => 'simulate', - method => 'GET', - description => "Simulate firewall rules. This does not simulate kernel 'routing' table. Instead, this simply assumes that routing from source zone to destination zone is possible.", - parameters => { - additionalProperties => 0, - properties => { - verbose => { - description => "Verbose output.", - type => 'boolean', - optional => 1, - default => 0, - }, - from => { - description => "Source zone.", - type => 'string', - pattern => '(host|outside|vm\d+|ct\d+|vmbr\d+/\S+)', - optional => 1, - default => 'outside', - }, - to => { - description => "Destination zone.", - type => 'string', - pattern => '(host|outside|vm\d+|ct\d+|vmbr\d+/\S+)', - optional => 1, - default => 'host', - }, - protocol => { - description => "Protocol.", - type => 'string', - pattern => '(tcp|udp)', - optional => 1, - default => 'tcp', - }, - dport => { - description => "Destination port.", - type => 'integer', - minValue => 1, - maxValue => 65535, - optional => 1, - }, - sport => { - description => "Source port.", - type => 'integer', - minValue => 1, - maxValue => 65535, - optional => 1, - }, - source => { - description => "Source IP address.", - type => 'string', format => 'ipv4', - optional => 1, - }, - dest => { - description => "Destination IP address.", - type => 'string', format => 'ipv4', - optional => 1, - }, - }, - }, - returns => { type => 'null' }, - code => sub { - my ($param) = @_; - - local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog - - my ($ruleset, $ipset_ruleset, $rulesetv6) = PVE::Firewall::compile(undef, undef, undef, $param->{verbose}); - - PVE::FirewallSimulator::debug($param->{verbose} || 0); - - my $host_ip = PVE::Cluster::remote_node_ip($nodename); - - PVE::FirewallSimulator::reset_trace(); - print Dumper($ruleset) if $param->{verbose}; - - my $test = { - from => $param->{from}, - to => $param->{to}, - proto => $param->{protocol} || 'tcp', - source => $param->{source}, - dest => $param->{dest}, - dport => $param->{dport}, - sport => $param->{sport}, - }; - - if (!defined($test->{to})) { - $test->{to} = 'host'; - PVE::FirewallSimulator::add_trace("Set Zone: to => '$test->{to}'\n"); - } - if (!defined($test->{from})) { - $test->{from} = 'outside', - PVE::FirewallSimulator::add_trace("Set Zone: from => '$test->{from}'\n"); - } - - my $vmdata = PVE::Firewall::read_local_vm_config(); - - print "Test packet:\n"; - - foreach my $k (qw(from to proto source dest dport sport)) { - printf(" %-8s: %s\n", $k, $test->{$k}) if defined($test->{$k}); - } - - $test->{action} = 'QUERY'; - - my $res = PVE::FirewallSimulator::simulate_firewall($ruleset, $ipset_ruleset, - $host_ip, $vmdata, $test); - - print "ACTION: $res\n"; - - return undef; - }}); - -my $cmddef = { - start => [ __PACKAGE__, 'start', []], - stop => [ __PACKAGE__, 'stop', []], - compile => [ __PACKAGE__, 'compile', []], - simulate => [ __PACKAGE__, 'simulate', []], - localnet => [ __PACKAGE__, 'localnet', []], - status => [ __PACKAGE__, 'status', [], undef, sub { - my $res = shift; - my $status = ($res->{enable} ? "enabled" : "disabled") . '/' . $res->{status}; - - if ($res->{changes}) { - print "Status: $status (pending changes)\n"; - } else { - print "Status: $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. + $rpcenv->init_request(); + $rpcenv->set_language($ENV{LANG}); + $rpcenv->set_user('root@pam'); +}; -=include pve_copyright +PVE::Service::pve_firewall->run_cli_handler(prepare => $prepare);