476 lines
11 KiB
C++
476 lines
11 KiB
C++
|
/**
|
||
|
* @PROJECT ForbidHosts
|
||
|
* @COPYRIGHT See COPYING in the top level directory
|
||
|
* @FILE forbidhosts.cpp
|
||
|
* @PURPOSE Tool for checking IPv4 & IPv6 failed connections
|
||
|
* @DEVELOPERS Pierre Schweitzer <pierre@reactos.org>
|
||
|
* Rafal Kupiec <belliash@asiotec.eu.org>
|
||
|
*/
|
||
|
|
||
|
#include <sys/stat.h>
|
||
|
#include <sys/inotify.h>
|
||
|
#include <poll.h>
|
||
|
#include <errno.h>
|
||
|
#include <fcntl.h>
|
||
|
#include <syslog.h>
|
||
|
#include <unistd.h>
|
||
|
|
||
|
#include <string>
|
||
|
#include <vector>
|
||
|
#include <cstring>
|
||
|
#include <algorithm>
|
||
|
#include <cstdio>
|
||
|
#include <cstdarg>
|
||
|
#include <csignal>
|
||
|
|
||
|
#include "forbidhosts.h"
|
||
|
|
||
|
using namespace std;
|
||
|
|
||
|
void assertException(const char *file, unsigned int line, const char *assert, bool critical) {
|
||
|
syslog(LOG_ERR, "Assertion '%s' failed at line %d in file %s", assert, line, file);
|
||
|
if(critical) {
|
||
|
shutdown(EXIT_FAILURE);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void assignGlobalVars() {
|
||
|
global.max_attempts = MAXATTEMPTS;
|
||
|
global.host_expire = HOSTEXPIRE;
|
||
|
global.failure_penalty = FAILUREPENALTY;
|
||
|
global.deny_file = DENYFILE;
|
||
|
global.log_file = LOGFILE;
|
||
|
global.pid_file = PIDFILE;
|
||
|
global.protocol = "sshd";
|
||
|
}
|
||
|
|
||
|
bool compare(const host_t &lhs, const host_t &rhs) {
|
||
|
return (lhs.expires > rhs.expires);
|
||
|
}
|
||
|
|
||
|
void daemonize(string workdir) {
|
||
|
int pidfile;
|
||
|
char pid[10];
|
||
|
struct sigaction sigact;
|
||
|
pid_t daemon;
|
||
|
|
||
|
if(getppid() == 1) {
|
||
|
return;
|
||
|
}
|
||
|
setlogmask(LOG_MASK(LOG_INFO) | LOG_MASK(LOG_ERR) | LOG_MASK(LOG_CRIT));
|
||
|
openlog("ForbidHosts", LOG_CONS, LOG_USER);
|
||
|
syslog(LOG_INFO, "Starting up daemon.");
|
||
|
daemon = fork();
|
||
|
if(daemon < 0) {
|
||
|
syslog(LOG_CRIT, "Unable to execute fork().");
|
||
|
shutdown(EXIT_FAILURE);
|
||
|
}
|
||
|
if(daemon > 0) {
|
||
|
syslog(LOG_INFO, "Child process created, working in background. Parent exiting.");
|
||
|
exit(EXIT_SUCCESS);
|
||
|
}
|
||
|
sigact.sa_handler = &signalHandler;
|
||
|
sigact.sa_flags = SA_RESTART;
|
||
|
sigfillset(&sigact.sa_mask);
|
||
|
sigaction(SIGABRT, &sigact, NULL);
|
||
|
sigaction(SIGHUP, &sigact, NULL);
|
||
|
sigaction(SIGINT, &sigact, NULL);
|
||
|
sigaction(SIGKILL, &sigact, NULL);
|
||
|
sigaction(SIGQUIT, &sigact, NULL);
|
||
|
sigaction(SIGTERM, &sigact, NULL);
|
||
|
sigaction(SIGUSR1, &sigact, NULL);
|
||
|
sigaction(SIGUSR2, &sigact, NULL);
|
||
|
umask(0);
|
||
|
if(setsid() < 0) {
|
||
|
shutdown(EXIT_FAILURE);
|
||
|
}
|
||
|
if((chdir(workdir.c_str())) < 0) {
|
||
|
syslog(LOG_CRIT, "Unable to change working directory to '%s'.", workdir.c_str());
|
||
|
shutdown(EXIT_FAILURE);
|
||
|
}
|
||
|
close(STDIN_FILENO);
|
||
|
close(STDOUT_FILENO);
|
||
|
close(STDERR_FILENO);
|
||
|
pidfile = open(global.pid_file.c_str(), O_RDWR | O_CREAT, 0600);
|
||
|
if(pidfile == -1) {
|
||
|
syslog(LOG_CRIT, "Unable to open '%s' PID lock file.", global.pid_file.c_str());
|
||
|
shutdown(EXIT_FAILURE);
|
||
|
}
|
||
|
if(lockf(pidfile, F_TLOCK,0) == -1) {
|
||
|
syslog(LOG_CRIT, "Unable to lock '%s' PID lock file.", global.pid_file.c_str());
|
||
|
shutdown(EXIT_FAILURE);
|
||
|
}
|
||
|
sprintf(pid, "%d\n", getpid());
|
||
|
assertSoft(write(pidfile, pid, strlen(pid)) == (ssize_t) strlen(pid));
|
||
|
close(pidfile);
|
||
|
syslog(LOG_INFO, "Daemon is UP & RUNNING.");
|
||
|
}
|
||
|
|
||
|
void debug(const char *msg, ...) {
|
||
|
int fp;
|
||
|
char *log = NULL;
|
||
|
va_list args;
|
||
|
|
||
|
if(global.debug.length()) {
|
||
|
if((fp = open(global.debug.c_str(), O_WRONLY | O_APPEND))) {
|
||
|
va_start(args, msg);
|
||
|
sprintf(log, msg, args);
|
||
|
va_end(args);
|
||
|
assertSoft(write(fp, log, strlen(log)) == (ssize_t) strlen(log));
|
||
|
close(fp);
|
||
|
sync();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void denyHost(string host) {
|
||
|
int colon;
|
||
|
int deny;
|
||
|
string entry;
|
||
|
|
||
|
deny = open(global.deny_file.c_str(), O_WRONLY | O_APPEND);
|
||
|
if(deny < 0) {
|
||
|
debug("Banning host '%s' in file %s failed for unknown reason.", host.c_str(), global.deny_file.c_str());
|
||
|
syslog(LOG_ERR, "Banning host '%s' in file %s failed for unknown reason.", host.c_str(), global.deny_file.c_str());
|
||
|
shutdown(EXIT_FAILURE);
|
||
|
}
|
||
|
colon = host.find_first_of(':');
|
||
|
if(colon != -1) {
|
||
|
entry = global.protocol + ": [" + host + "]\n";
|
||
|
} else {
|
||
|
entry = global.protocol + ": " + host + "\n";
|
||
|
}
|
||
|
assertSoft(write(deny, entry.c_str(), entry.length()) == (ssize_t) entry.length());
|
||
|
close(deny);
|
||
|
sync();
|
||
|
debug("Banned host '%s' for excessive abusive.", host.c_str());
|
||
|
syslog(LOG_INFO, "Banned host '%s' for excessive abusive.", host.c_str());
|
||
|
}
|
||
|
|
||
|
int main(int argc, char *argv[]) {
|
||
|
int i_auth;
|
||
|
int i_notify;
|
||
|
int next_opt;
|
||
|
int val;
|
||
|
vector<banned_t> banned;
|
||
|
vector<host_t> hosts;
|
||
|
|
||
|
assignGlobalVars();
|
||
|
while((next_opt = getopt(argc, argv, "t:p:n:l:e:d:D:av")) != -1) {
|
||
|
switch(next_opt) {
|
||
|
case 'a':
|
||
|
global.protocol = "ALL";
|
||
|
break;
|
||
|
case 'd':
|
||
|
global.deny_file = strdup(optarg);
|
||
|
break;
|
||
|
case 'D':
|
||
|
global.debug = strdup(optarg);
|
||
|
break;
|
||
|
case 'e':
|
||
|
val = sscanf(optarg, "%u", &global.host_expire);
|
||
|
if(val > 0) {
|
||
|
global.host_expire = global.host_expire > 0 ? global.host_expire : HOSTEXPIRE;
|
||
|
} else {
|
||
|
global.host_expire = HOSTEXPIRE;
|
||
|
}
|
||
|
break;
|
||
|
case 'l':
|
||
|
global.log_file = strdup(optarg);
|
||
|
break;
|
||
|
case 'n':
|
||
|
global.banned_names = strdup(optarg);
|
||
|
break;
|
||
|
case 'p':
|
||
|
global.pid_file = strdup(optarg);
|
||
|
break;
|
||
|
case 't':
|
||
|
val = sscanf(optarg, "%u", &global.max_attempts);
|
||
|
if(val > 0) {
|
||
|
global.max_attempts = global.max_attempts > 0 ? global.max_attempts : MAXATTEMPTS;
|
||
|
} else {
|
||
|
global.max_attempts = MAXATTEMPTS;
|
||
|
}
|
||
|
break;
|
||
|
case 'v':
|
||
|
printVersion();
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
daemonize("/tmp");
|
||
|
logfile = open(global.log_file.c_str(), O_RDONLY | O_NONBLOCK);
|
||
|
if(logfile < 0) {
|
||
|
syslog(LOG_CRIT, "Unable to open '%s' log file for reading.", global.log_file.c_str());
|
||
|
shutdown(EXIT_FAILURE);
|
||
|
}
|
||
|
lseek(logfile, 0, SEEK_END);
|
||
|
i_notify = inotify_init1(IN_NONBLOCK);
|
||
|
if(i_notify < 0) {
|
||
|
close(logfile);
|
||
|
syslog(LOG_CRIT, "Unable to initialize iNotify subsystem.");
|
||
|
shutdown(EXIT_FAILURE);
|
||
|
}
|
||
|
i_auth = inotify_add_watch(i_notify, global.log_file.c_str(), IN_MODIFY);
|
||
|
if(i_auth < 0) {
|
||
|
close(i_notify);
|
||
|
close(logfile);
|
||
|
syslog(LOG_CRIT, "Unable to attach a watcher on a log file.");
|
||
|
shutdown(EXIT_FAILURE);
|
||
|
}
|
||
|
for(;;) {
|
||
|
struct pollfd fds[] = {i_notify, POLLIN, 0};
|
||
|
int timeout = -1;
|
||
|
if(!hosts.empty()) {
|
||
|
timeout = hosts.back().expires - time(0) * 1000;
|
||
|
}
|
||
|
int event = poll(fds, 1, timeout);
|
||
|
if(event > 0) {
|
||
|
struct inotify_event i_event;
|
||
|
assertSoft(read(i_notify, &i_event, sizeof(struct inotify_event)) == sizeof(struct inotify_event));
|
||
|
}
|
||
|
while(!readLine(logfile, hosts, banned));
|
||
|
while(!banned.empty()) {
|
||
|
if(banned.back().expires > time(0)) {
|
||
|
break;
|
||
|
}
|
||
|
banned.pop_back();
|
||
|
}
|
||
|
while(!hosts.empty()) {
|
||
|
if(hosts.back().expires > time(0)) {
|
||
|
break;
|
||
|
}
|
||
|
hosts.pop_back();
|
||
|
}
|
||
|
sleep(1);
|
||
|
}
|
||
|
close(i_auth);
|
||
|
close(i_notify);
|
||
|
close(logfile);
|
||
|
}
|
||
|
|
||
|
void printVersion() {
|
||
|
printf("ForbidHosts Daemon v%s\n\n", FHVERSION);
|
||
|
exit(EXIT_SUCCESS);
|
||
|
}
|
||
|
|
||
|
unsigned int readLine(int file, vector<host_t> &hosts, vector<banned_t> &banned) {
|
||
|
char line[255];
|
||
|
char *address;
|
||
|
string host;
|
||
|
bool loggedin = false;
|
||
|
static string last_addr = "";
|
||
|
unsigned int length;
|
||
|
unsigned int addr_length;
|
||
|
unsigned int red = 0;
|
||
|
unsigned int repeated = 1;
|
||
|
|
||
|
for(;;) {
|
||
|
red = 0;
|
||
|
while(red < sizeof(line) / sizeof(char)) {
|
||
|
length = read(file, &line[red], sizeof(char));
|
||
|
if(length < 1) {
|
||
|
if(red == 0) {
|
||
|
return length;
|
||
|
} else {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if(line[red] == '\n') {
|
||
|
line[red] = '\0';
|
||
|
break;
|
||
|
}
|
||
|
red++;
|
||
|
}
|
||
|
if(!validateEntry(line, &address, &addr_length, &loggedin)) {
|
||
|
if(!last_addr.empty()) {
|
||
|
repeated = validateRepeated(line);
|
||
|
if(repeated == 0) {
|
||
|
last_addr = "";
|
||
|
return length;
|
||
|
}
|
||
|
} else {
|
||
|
last_addr = "";
|
||
|
return length;
|
||
|
}
|
||
|
} else {
|
||
|
host = address;
|
||
|
host.erase(addr_length);
|
||
|
last_addr = host;
|
||
|
}
|
||
|
if(updateHost(last_addr, hosts, banned, repeated, loggedin)) {
|
||
|
hosts.push_back(host_t(time(0), host));
|
||
|
}
|
||
|
sort(hosts.begin(), hosts.end(), compare);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void shutdown(int code) {
|
||
|
if(code != 0) {
|
||
|
syslog(LOG_CRIT, "Emergency daemon shutdown due to errors.");
|
||
|
} else {
|
||
|
syslog(LOG_INFO, "Shutting down daemon.");
|
||
|
}
|
||
|
unlink(global.pid_file.c_str());
|
||
|
exit(code);
|
||
|
}
|
||
|
|
||
|
void signalHandler(int signal) {
|
||
|
switch(signal) {
|
||
|
case SIGHUP:
|
||
|
syslog(LOG_WARNING, "Received SIGHUP signal!");
|
||
|
break;
|
||
|
case SIGABRT:
|
||
|
case SIGINT:
|
||
|
case SIGQUIT:
|
||
|
case SIGTERM:
|
||
|
shutdown(EXIT_SUCCESS);
|
||
|
break;
|
||
|
case SIGUSR1:
|
||
|
case SIGUSR2:
|
||
|
syslog(LOG_INFO, "Received SIGUSR1 signal - probably logs rotated.");
|
||
|
syslog(LOG_INFO, "Reloading system log file.");
|
||
|
lseek(logfile, 0, SEEK_END);
|
||
|
break;
|
||
|
default:
|
||
|
syslog(LOG_WARNING, "Unhandled signal caught: %s.", strsignal(signal));
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool updateHost(const string &host, vector<host_t> &hosts, vector<banned_t> &banned, unsigned int repeated, bool loggedin) {
|
||
|
bool insert = true;
|
||
|
|
||
|
assertSoft(!banned.empty());
|
||
|
for(vector<banned_t>::iterator it = banned.begin(); it != banned.end(); ++it) {
|
||
|
if((*it).ipaddr.compare(host) == 0) {
|
||
|
insert = false;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if(insert) {
|
||
|
assertSoft(!host.empty());
|
||
|
for(vector<host_t>::iterator it = hosts.begin(); it != hosts.end(); ++it) {
|
||
|
if((*it).ipaddr.compare(host) == 0) {
|
||
|
insert = false;
|
||
|
(*it).attempts += repeated;
|
||
|
if(loggedin) {
|
||
|
hosts.erase(it);
|
||
|
} else if((*it).attempts >= global.max_attempts) {
|
||
|
denyHost((*it).ipaddr);
|
||
|
hosts.erase(it);
|
||
|
banned.push_back(banned_t(time(0), host));
|
||
|
} else {
|
||
|
(*it).expires += (global.host_expire * 60);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
assertSoft((insert && repeated == 1) || !insert);
|
||
|
return (loggedin ? false : insert);
|
||
|
}
|
||
|
|
||
|
bool validateEntry(char *entry, char **address, unsigned int *length, bool *loggedin) {
|
||
|
char *sshd;
|
||
|
char *method;
|
||
|
char *user;
|
||
|
char *host;
|
||
|
char *end;
|
||
|
|
||
|
*loggedin = false;
|
||
|
sshd = strstr(entry, " sshd[");
|
||
|
if(sshd != 0) {
|
||
|
method = strstr(sshd, ": Failed ");
|
||
|
if(method != 0) {
|
||
|
method += sizeof(": Failed ");
|
||
|
user = strstr(method, " for ");
|
||
|
if(user == 0) {
|
||
|
return false;
|
||
|
}
|
||
|
user += sizeof(" for ");
|
||
|
host = strstr(user, " from ");
|
||
|
if(host == 0) {
|
||
|
return false;
|
||
|
}
|
||
|
host += sizeof(" from ") - sizeof('\0');
|
||
|
end = strstr(host, " port ");
|
||
|
if(end == 0) {
|
||
|
return false;
|
||
|
}
|
||
|
} else {
|
||
|
method = strstr(sshd, ": Invalid ");
|
||
|
if(method != 0) {
|
||
|
user = strstr(method, " user ");
|
||
|
if(user == 0) {
|
||
|
return false;
|
||
|
}
|
||
|
user += sizeof (" user ");
|
||
|
host = strstr(user, " from ");
|
||
|
if(host == 0) {
|
||
|
return false;
|
||
|
}
|
||
|
host += sizeof(" from ") - sizeof('\0');
|
||
|
end = strchr(host, '\0');
|
||
|
} else {
|
||
|
method = strstr(sshd, ": Address ");
|
||
|
if(method != 0) {
|
||
|
host = strdup(method);
|
||
|
host += sizeof(": Address ") - sizeof('\0');
|
||
|
user = strstr(host, " POSSIBLE BREAK-IN ATTEMPT");
|
||
|
if(user == 0) {
|
||
|
return false;
|
||
|
}
|
||
|
end = strstr(host, " maps to ");
|
||
|
if(end == 0) {
|
||
|
return false;
|
||
|
}
|
||
|
} else {
|
||
|
method = strstr(sshd, ": Accepted ");
|
||
|
if(method != 0) {
|
||
|
user = strstr(method, " for ");
|
||
|
if(user == 0) {
|
||
|
return false;
|
||
|
}
|
||
|
user += sizeof(" for ");
|
||
|
host = strstr(user, " from ");
|
||
|
if(host == 0) {
|
||
|
return false;
|
||
|
}
|
||
|
host += sizeof(" from ") - sizeof('\0');
|
||
|
end = strchr(host, '\0');
|
||
|
*loggedin = true;
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
*address = host;
|
||
|
*length = (end - host);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
unsigned int validateRepeated(char *line) {
|
||
|
char *sshd;
|
||
|
char *times;
|
||
|
char *end;
|
||
|
|
||
|
sshd = strstr(line, " sshd[");
|
||
|
if(sshd == 0) {
|
||
|
return 0;
|
||
|
}
|
||
|
times = strstr(sshd, ": last message repeated ");
|
||
|
if(times == 0) {
|
||
|
return 0;
|
||
|
}
|
||
|
times += sizeof(": last message repeated ") - sizeof('\0');
|
||
|
end = strstr(times, " times");
|
||
|
if(end == 0) {
|
||
|
return 0;
|
||
|
}
|
||
|
return strtoul(times, 0, 10);
|
||
|
}
|