#!/usr/bin/perl -T
#
###########################################################################
#                                                                         #
# sshsentry   v4.1.0                                                      #
#                                                                         #
# Monitor the authentication log, detect repeated failed SSH login        #
# attempts and blacklist the hosts whence such attempts originate.        #
#                                                                         #
# This file is part of the sshsentry software package.                    #
#                                                                         #
# Copyright (C) 2006 - 2012 by Andreas Stempfhuber <andi@afulinux.de>     #
#                                                                         #
# Based on sshd_sentry, copyright by Victor Danilchenko, 09/22/2004       # 
# Based on login_sentry, copyright by Jesse Shrieve, 10/22/2004           #
#                                                                         #
# This program is free software: you can redistribute it and/or modify    #
# it under the terms of the GNU General Public License as published by    #
# the Free Software Foundation, either version 3 of the License, or       #
# (at your option) any later version.                                     #
#                                                                         #
# This program is distributed in the hope that it will be useful,         #
# but WITHOUT ANY WARRANTY; without even the implied warranty of          #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           #
# GNU General Public License for more details.                            #
#                                                                         #
# You should have received a copy of the GNU General Public License       #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.   #
#                                                                         #
###########################################################################

use strict;
use warnings;
use POSIX;
use Switch;
use Socket;		# inet_ntoa, inet_aton
use FileHandle;		# clearerr
use Symbol qw(gensym);
use IPC::Open3;
use Getopt::Long;
use Sys::Hostname;
use Sys::Syslog;
use Data::Dumper;
use Linux::Inotify2;
use Net::CIDR::Lite;
use File::Spec::Functions qw(splitpath rel2abs);

# System defaults
$main::VERSION = "4.1.0";		# Version number
$ENV{"PATH"} = '';			# Reset path for security reasons
my $name = (splitpath(__FILE__))[2];	# Name of this script
my $hosts_cidr = Net::CIDR::Lite->new;	# Initialise CIDR
my ($time_to_die, $alarm);
our $wakeup = 0;			# Wake-up timer
our $wakeup_daily = 0;			# Wake-up daily timer
our $inotify;
our $hosts = {};
our $users = {};

# Default command line configuration options
my $conffile = "/etc/$name.conf";	# Configuration file to read
my $pidfile = "/var/run/$name.pid";	# PID file to use
my $storagedir = "/var/lib/$name";	# Storage directory
my ($debug, $foreground, $ipsetrestore);

# Default conffile configuration options
our $denymethod = 'file';		# Method used to block the hosts
our $denyfile = '/etc/hosts.deny';	# Location of the hosts.deny file
our $tag = 'SENTRY';			# Tag to use inside hosts.deny file
our $ipset = '/usr/sbin/ipset';		# Location of the ipset command
our $ipset_blacklist_temporary = 'blacklist_temporary';	# ipset set name
our $ipset_blacklist_permanent = 'blacklist_permanent';	# ipset set name
our $logfacility = 'LOG_AUTHPRIV';	# Syslog facility (man 3 syslog)
our $logfile = '/var/log/auth.log';	# Logfile to parse
our $hosts_threshold = 6;		# How many failed logins per host?
our $hosts_duration = 10;		# How long to block for? (in minutes)
our $hosts_penalty = 2;			# Size of extra penalty for bad users
our $hosts_attacks = 3;			# Attacks to block hosts permanently
our $hosts_expire = 180;		# Days to expire collected host data
our $users_threshold = 5;		# Failed logins to count as bad user
our $users_expire = 360;		# Days to expire collected user data
our $sendmail = '/usr/lib/sendmail';	# Sendmail (compatible) executable
our $mailfrom = 'root@' . hostname;	# From address for emails
our ($mailto, @hosts_exclude, @users_exclude);

# Print help message
sub help () {
	return << "EOT";

Usage: $name [OPTION]...
Monitor the authentication log, detect repeated failed SSH login attempts
and blacklist the hosts whence such attempts originate.

Mandatory arguments to long options are mandatory for short options too.
  -c, --configfile=FILENAME   Configuration file to read
                              (default = $conffile).
  -d, --debug                 Enable debug messages.
  -f, --foreground            Start $name in foreground.
  -h, --help                  Show this help message.
  -p, --pidfile=FILENAME      File to store the PID of the daemon
                              (default = $pidfile).
  -r, --ipsetrestore          Restore ipset session and exit.
  -s, --storagedir=DIRECTORY  Storage directory for hosts, users and ipset
                              sessions (default = $storagedir).
  -v, --version               Show version information.

EOT
}

# Print version message
sub version() {
	return << "EOT";

$name $main::VERSION

Copyright (C) 2006 - 2012 by Andreas Stempfhuber <andi\@afulinux.de> 

Based on sshd_sentry, copyright by Victor Danilchenko, 09/22/2004
Based on login_sentry, copyright by Jesse Shrieve, 10/22/2004

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

EOT
}

# Send mail to users
sub mail_to_users ($$) {
	my $subject = shift;
	my $message = shift;

	if ($mailto) {
		if (open(MAIL, '|-', "$sendmail -t")) {
			print MAIL "From: $mailfrom\n";
			print MAIL "To: $mailto\n";
			print MAIL "Subject: $subject\n";
			print MAIL "\n$message\n";
			close MAIL or syslog('LOG_WARNING',
				"Unable to send mail");

		} else {
			syslog('LOG_WARNING',
				"Can't execute sendmail at $sendmail");
		}
	}
}

# Send mail and die
sub die_with_mail ($) {
	my $text = shift;

	mail_to_users("$name died on " . hostname, "$text: $!");
	syslog('LOG_ERR', "$text and died");
	die "$text: $!\n";
}

# Log or print debug message
sub debug ($) {
	my $text = shift;

	if ($debug) {
		if (! $foreground and ! $ipsetrestore) {
			syslog('LOG_DEBUG', $text);
		} else {
			print scalar(localtime()) . " " . $text . "\n";
		}
	}
}

# Setup inotify
sub setup_inotify ($) {
	my $logfile = shift;

	# Initialise inotify
	$inotify = new Linux::Inotify2
		or die_with_mail "Unable to create new inotify object";

	# Install watcher
	$inotify->watch($logfile, IN_MODIFY);
	$inotify->watch((splitpath($logfile))[1], IN_CREATE);
}

# Process SSH log line
sub process_ssh_line ($) {
	my $line = shift;

	# Remove newline
	chomp $line;

	# Watch for ssh logins
	if ($line =~ /\bsshd\[\d+\]: (Failed|Accepted) \S+ for (?:(?:illegal|invalid) user )?(\S+) from (?:::ffff:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) port \d+(?: ssh2)?$/) {
		debug("SSH login detected: $1, $2, $3");
		handle_match($1, $2, $3);
	}
	# Watch for failed ssh logins via PAM
	elsif ($line =~ /\bsshd\[\d+\]: error: PAM: Authentication failure for (\S+) from (\S+)$/) {
		my $ip = inet_ntoa(scalar(gethostbyname($2)));
		debug("SSH login via PAM detected: failed, $1, $ip");
		handle_match('failed', $1, $ip);
	}
	# Watch for portscans
	elsif ($line =~ /\bsshd\[\d+\]: Did not receive identification string from (?:::ffff:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/) {
		debug("Portscan detected: portscan, , $1");
		handle_match('portscan', '', $1);
	}
	# Watch for bad protocol version
	elsif ($line =~ /\bsshd\[\d+\]: Bad protocol version identification .* from (?:::ffff:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/) {
		debug("Bad protocol version detected: bad_protocol, , $1");
		handle_match('bad_protocol', '', $1);
	}
}

# Handle match
sub handle_match ($$$) {
	my $result = shift;
	my $user = shift;
	my $host = shift;

	if (! @hosts_exclude or ! $hosts_cidr->find($host)) {
		if ($result =~ /accepted/i) {
			# Successful login, validate this host and user
			delete $hosts->{$host} if ($hosts->{$host});
			delete $users->{$user} if ($users->{$user});
			accepted_login($user, $host);
		} elsif ($result =~ /portscan/i) {
			# Portscan
			$hosts->{$host}->{portscan}++;
			$hosts->{$host}->{count} += 1+$hosts_penalty;
			$hosts->{$host}->{mtime} = time();
		} elsif ($result =~ /bad_protocol/i) {
			# Bad protocol
			$hosts->{$host}->{bad_protocol}++;
			$hosts->{$host}->{count} += 1+$hosts_penalty;
			$hosts->{$host}->{mtime} = time();
		} else {
			$hosts->{$host}->{users}->{$user}++;
			$hosts->{$host}->{count}++;
			if ($users->{$user}->{count} and
			    $users->{$user}->{count} >= $users_threshold) {
				# Add extra penalty for known-exploited users
				$hosts->{$host}->{count} += $hosts_penalty;
				debug("Extra penalty added for user '$user'");
			}
			$hosts->{$host}->{mtime} = time();
			# Update user data
			$users->{$user}->{count}++;
			$users->{$user}->{mtime} = time();
		}

		if ($hosts->{$host}->{count} and
		    $hosts->{$host}->{count} >= $hosts_threshold and
		    (! $hosts->{$host}->{blocked} or
		     $hosts->{$host}->{blocked} + 45 < time())) {
			# Delete old blocked state
			delete $hosts->{$host}->{blocked}
				if ($hosts->{$host}->{blocked});
			# Too many authentication failures for the host
			$hosts->{$host}->{attacks}++;
			if ($hosts->{$host}->{attacks} < $hosts_attacks) {
				block_temporary($host);
			} else {
				block_permanent($host);
			}
		}
	}
}

# Login has been accepted
sub accepted_login ($$) {
	my $user = shift;
	my $host = shift;

	if (! @users_exclude or ! grep (/^\Q$user\E$/, @users_exclude)) {
		my $fqdn = gethostbyaddr(inet_aton($host), AF_INET);
		# Make sure that FQDN is defined if DNS has no entry
		$fqdn = "" if (! defined($fqdn));
		mail_to_users("$name on " . hostname .
			": Accepted " . $user . "@" . $host,
			"Accepted login from:\n\tuser => $user\n\t" .
			"host => $host\n\tFQDN => $fqdn");
	}
}

# Block host temporary
sub block_temporary ($) {
	my $host = shift;
	my $expo = time() + $hosts_duration * 60;
	my $time = strftime('%Y-%m-%d %H.%M.%S', localtime($expo));

	if ($denymethod !~ /ipset/i) {
		my $str = sprintf ("ALL : %-16s \# %s %i (expires %s)\n",
			$host, $tag, $expo, $time);

		open (DENY, '>>', $denyfile) or
			die_with_mail "Can't write to $denyfile";
		printf DENY $str;
		close DENY;

		# Update wake-up timer
		wakeup_timer($expo);

	} else {
		ipset_add_host($host, $ipset_blacklist_temporary);
	}

	syslog('LOG_NOTICE', "Denying connections from $host (count " .
		$hosts->{$host}->{count} . ", expires $time)");

	$Data::Dumper::Terse = 1;
	mail_to_users("$name on " . hostname . ": Blocking $host",
		"Blocking $host for $hosts_duration minutes (expires $time)." .
		"\n\nObject contents:\n        " . Dumper($hosts->{$host}));

	# Delete old data of host except attacks counter
	my $attacks = $hosts->{$host}->{attacks};
	delete $hosts->{$host};
	$hosts->{$host}->{attacks} = $attacks;
	$hosts->{$host}->{mtime} = time();

	# Mark host as blocked
	$hosts->{$host}->{blocked} = time();
}

# Block host permanent
sub block_permanent ($) {
	my $host = shift;

	if ($denymethod !~ /ipset/i) {
		my $time = strftime('%Y-%m-%d %H.%M.%S', localtime());
		my $str = sprintf ("ALL : %-16s \# %s permanently blocked" .
			" (%s)\n", $host, $tag, $time);

		open (DENY, '>>', $denyfile) or
			die_with_mail "Can't write to $denyfile";
		printf DENY $str;
		close DENY;
	} else {
		ipset_add_host($host, $ipset_blacklist_temporary);
		ipset_add_host($host, $ipset_blacklist_permanent);
		save_ipset_session();
	}

	syslog('LOG_NOTICE', "Permanently denying connections from $host" .
		" due to " . $hosts->{$host}->{attacks} . " attacks");

	$Data::Dumper::Terse = 1;
	mail_to_users("$name on " . hostname . ": Blocking $host permanently",
		"Blocking $host permanently due to " .
		$hosts->{$host}->{attacks} . " attacks." .
		"\n\nObject contents:\n        " . Dumper($hosts->{$host}));

	# Mark host as blocked
	$hosts->{$host}->{blocked} = time();
}

# Add new host to the ipset blacklist
sub ipset_add_host($$) {
	my $host = shift;
	my $blacklist = shift;

	if ($blacklist =~ /$ipset_blacklist_temporary/) {
		$host = $host . "," . $hosts_duration * 60;
	}

	exec_ipset("-A $blacklist $host", 'syslog');
}

# Expire old entries in $denyfile
sub expire_denials () {
	my @new = ();
	my $change = 0;
	my $indices = {};

	debug("Reading deny file $denyfile");
	open(DENY, $denyfile) or die_with_mail "Can't read $denyfile";

	while (my $line = <DENY>) {
		if ($line =~ /\#\s*\b$tag\b\s*(\d+)/) {
			# Our line, process it
			my $expo = $1;
			if ($expo > time()) {
				# This entry has future timestamp, decide what
				# to do with it
				my $host = ($line =~ /^[^:]+:\s*([^:\s]+)/)[0];
				if ($indices->{$host}) {
					# We already saw a line for this host,
					# decide which line to keep
					if ($expo > $indices->{$host}->{expo}) {
						# The new entry has a greater
						# expiration time, keep it
						$new[$indices->{$host}->
							{index}] = $line;

						# Update wake-up timer
						wakeup_timer($expo);
					}
					# Else do nothing and skip this line,
					# keep the one we had
					
					# We merged two entries into one, must
					# dump the data
					$change = 1;
				} else {
					# This is the first time we see a
					# record for this host, keep it
					$indices->{$host}->{index} = @new;
					$indices->{$host}->{expo} = $expo;
					push(@new, $line);

					# Update wake-up timer
					wakeup_timer($expo);
				}
			} else {
				# We reaped an entry, set the change flag
				$change = 1;
			}
		} else {
			# Not our line, keep it
			push(@new, $line);
		}
	}

	# Don't forget to close the $denyfile
	close DENY;

	if ($change) {
		# We changed the content, write it back to file
		debug("Updating deny file $denyfile");
		my ($mode, $uid, $gid) = (stat($denyfile))[2,4,5];
		open(DENY, '>', $denyfile) or
			die_with_mail "Can't write to $denyfile";
		print DENY @new;
		close DENY;
		chown($uid, $gid, $denyfile);
		chmod($mode, $denyfile);
	}
}

# Delete old hosts and users entries
sub garbage_collection () {
	my $hosts_expo = time() - $hosts_expire * 60 * 60 * 24;
	my $users_expo = time() - $users_expire * 60 * 60 * 24;

	debug("Starting garbage collection");

	for my $host (keys %$hosts) {
		if (! $hosts->{$host}->{mtime} or
		    $hosts->{$host}->{mtime} < $hosts_expo) {
			debug("Host $host expired or empty");
			delete $hosts->{$host};
		}
		elsif ($hosts->{$host}->{blocked} and
		    $hosts->{$host}->{blocked} + 45 < time() and
		    $hosts->{$host}->{attacks} >= $hosts_attacks) {
			debug("Host $host is blocked and no longer required");
			delete $hosts->{$host};
		}
	}

	for my $user (keys %$users) {
		if (! $users->{$user}->{mtime} or
		    $users->{$user}->{mtime} < $users_expo) {
			debug("User '$user' expired or empty");
			delete $users->{$user};
		}
	}

	debug("Finished garbage collection");
}

# Update wake-up timer
sub wakeup_timer ($) {
	my $expo = shift;

	if ($expo == 0) {
		# Next wake-up in one day
		$wakeup = time() + 60 * 60 * 24;
	} else {
		# Store earliest wake-up time
		$wakeup = $expo if ($wakeup > $expo);
	}

	debug("Next wake-up at " . scalar(localtime($wakeup)));
}

# Untaint user supplied file or directory name
sub untaint ($) {
	my $file = shift;

	if (rel2abs($file) =~ /^([-\@\w.\/+ ]+)$/) {
		# Untaint filename
		return $1;
	} else {
		print "File or directory '$file' is insecure.\n";
		exit 1;
	}
}

# Read configuration file
sub read_configfile($) {
	my $conffile = shift;

	if (-r $conffile) {
		debug("Reading configuration file $conffile");
		my $ret = do $conffile;
		die "$conffile: $@\n" if $@;
		die "$conffile: $!\n" unless defined $ret;
		die "$conffile: can't read\n" unless $ret;
	} else {
		die "Configuration file $conffile missing.\n";
	}
}

# Read hosts and users database from storage
sub read_storage () {
	my $storagefile = "$storagedir/$name.dat";

	if (-r $storagefile) {
		debug("Reading storage file $storagefile");
		my $ret = do $storagefile;
		die "$storagefile: $@\n" if $@;
		die "$storagefile: $!\n" unless defined $ret;
		die "$storagefile: can't read\n" unless $ret;
	} else {
		syslog('LOG_WARNING', "Missing storage file $storagefile");
	}
}

# Restore ipset session
sub restore_ipset_session () {
	die "$ipset is not an executable" if (! -x $ipset);

	foreach($ipset_blacklist_permanent, $ipset_blacklist_temporary) {
		if (! exec_ipset("-L $_ >/dev/null", 'debug')) {
			# Restore ipset session from file
			my $ipsetfile = "$storagedir/ipset-$_.dat";
			debug("Restoring ipset session from $ipsetfile");
			if (! -r $ipsetfile or
			    ! exec_ipset("-R <'$ipsetfile'", 'debug')) {
				debug("Restoring failed, creating new set");
				if ($_ =~ /$ipset_blacklist_permanent/) {
					# Create new permanent blacklist
					exec_ipset("-N $_ iphash", 'die');
				} else {
					# Create new temporary blacklist
					exec_ipset("-N $_ iptree" .
						" --timeout 2592000", 'die');
				}
			}
		} else {
			debug("IP set $_ already exists, no need to restore");
		}
	}
}

# Execute ipset command
sub exec_ipset ($$) {
	my $cmdline = shift;
	my $action = shift;
	my ($pid, $infh, $outfh, $errfh);

	$errfh = gensym();
	$pid = open3($infh, $outfh, $errfh, "$ipset $cmdline");
	waitpid($pid, 0);
	my $exit_status = $? >> 8;
	my $error = <$errfh>;

	if ($exit_status) {
		# Remove newline
		chomp $error;

		switch ($action) {
			case "syslog"	{ syslog('LOG_WARNING', $error); }
			case "debug"	{ debug $error; }
			case "warn"	{ print STDERR $error . "\n"; }
			case "die"	{ die $error . "\n"; }
		}

		return 0;

	}

	return 1;
}

# Write hosts and users database to storage
sub save_storage () {
	my $storagefile = "$storagedir/$name.dat";

	create_dir($storagedir);

	# Setup data dumper
	$Data::Dumper::Terse = 0;

	debug("Writing storage file $storagefile");
	open(STORE, '>', $storagefile) or
		die_with_mail "Can't write to $storagefile";
	print STORE Data::Dumper->Dump([$hosts, $users], [qw (hosts users)]);
	close STORE;
}

# Save ipset session
sub save_ipset_session () {
	if ($denymethod =~ /ipset/i) {
		create_dir($storagedir);

		foreach ($ipset_blacklist_permanent,
		   $ipset_blacklist_temporary) {
			if (exec_ipset("-L $_ >/dev/null", 'debug')) {
				my $ipsetfile = "$storagedir/ipset-$_.dat";
				debug("Saving ipset session to $ipsetfile");
				exec_ipset("-S $_ >'$ipsetfile'", 'syslog');
			}
		}
	}
}

# Create directory
sub create_dir ($) {
	my $dir = shift;

	if (! -d $dir) {
		syslog('LOG_NOTICE', "Creating directory $dir");
		mkdir $dir or die_with_mail "Can't create directory $dir";
	}
}

# Handle alarm signal
sub sigalarm_handler () {
	my $day = (localtime())[3];

	# Reset wake-up timer
	wakeup_timer(0);

	expire_denials() if ($denymethod !~ /ipset/i);

	if ($wakeup_daily != $day) {
		# Daily tasks
		$wakeup_daily = $day;
		garbage_collection();
		save_ipset_session();
		save_storage();
	}
}

# Handle USR1 signal
sub sigusr1_handler () {
	save_ipset_session();
	save_storage();
}

# Handle other signals
sub signal_handler () {
	$time_to_die = 1;
}


#
# Main
#

# Get command line options
Getopt::Long::Configure ("no_ignore_case", "gnu_getopt");
GetOptions ("c|configfile=s"  => sub { $conffile = untaint($_[1]) },
	    "d|debug"         => \$debug,
	    "f|foreground"    => \$foreground,
	    "h|help|?"        => sub { print help(); exit 0 },
	    "r|ipsetrestore"  => \$ipsetrestore,
	    "p|pidfile=s"     => sub { $pidfile = untaint($_[1]) },
	    "s|storagedir=s" => sub { $storagedir = untaint($_[1]) },
	    "v|version"       => sub { print version(); exit 0 },
) or print help() and exit 1;

# Read configuration file
read_configfile($conffile);

# Restore ipset session
if ($denymethod =~ /ipset/i or $ipsetrestore) {
	restore_ipset_session();
	exit if ($ipsetrestore);
}

# Add hosts_exclude list to CIDR
foreach (@hosts_exclude) {
	$hosts_cidr->add_any($_);
}

if (! $foreground) {
	# Daemonize
	my $pid = fork;
	die "Couldn't fork: $!\n" unless defined $pid;

	if ($pid) {
		unless (open(PIDFILE, '>', $pidfile)) {
			kill 15, $pid;
			die "Can't write to $pidfile: $!\n";
		}
		print PIDFILE "$pid\n";
		close PIDFILE;
		exit;
	}

	POSIX::setsid() or die "Can't start a new session: $!\n";
	openlog($name, 'cons,pid', $logfacility);
	syslog('LOG_INFO', "Starting as daemon and using logfile $logfile");
} else {
	# Run in foreground and send syslog messages to standard error, too
	openlog($name, 'perror,nofatal,pid', $logfacility);
}

# Read hosts and users database from storage
read_storage();

# Get inode of logfile
my $inode = (stat($logfile))[1];
unless (open(LOG, $logfile)) {
	syslog('LOG_ERR', "Can't read log file $logfile");
	die "Can't read log file $logfile: $!\n";
}
# Go to the end of the logfile
seek(LOG, 0, 2);

# Setup data dumper
$Data::Dumper::Purity = 1;
$Data::Dumper::Deepcopy = 1;

# Setup inotify
setup_inotify($logfile);

# Install signal handlers
$SIG{ALRM} = \&sigalarm_handler;
$SIG{USR1} = \&sigusr1_handler;
$SIG{INT} = $SIG{TERM} = $SIG{HUP} = \&signal_handler;

# Spin infinitely
while (! $time_to_die) {
	# Set alarm to wake-up timer
	$alarm = $wakeup - time();
	$alarm = 1 if ($alarm < 1);
	alarm($alarm);

	# Wait till an inotify event is received
	# (this call will block until an event is received or interrupted by
	# an alarm)
	my @events = $inotify->read();

	# Disable alarm so that it can't disturb
	alarm(0);

	if (@events) {
		debug("Inotify event " . $events[0]->mask . " received");

		my $new_inode = (stat($logfile))[1];
		if ($new_inode and $inode != $new_inode) {
			# Logs have been rotated, open new log file
			debug("Logs have been rotated, opening new log file");
			close LOG;
			open(LOG, $logfile) or die_with_mail
				"Can't reopen rotated log file $logfile";
			$inode = $new_inode;
			# Setup inotify for new file
			setup_inotify($logfile);
		} else {
			# The logs have not been rotated, reset EOF
			LOG->clearerr();
		}

		# Parse new log lines
		while (my $line = <LOG>) {
			if ($line =~ /\bsshd\b/) {
				# Parse SSH log line
				process_ssh_line($line);
			}
		}
	} else {
		debug("Wake-up without inotify event");
	}
}

# Save ipset session
save_ipset_session();

# Save hosts and users database to storage
save_storage();

# It's time to die
if (! $foreground) {
	syslog('LOG_INFO', "Received signal, daemon exiting");
	unlink $pidfile;
}
