You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

405 lines
11 KiB

/* Copyright 2016 Alex 'AdUser' Z (ad_user@runbox.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include "common.h"
#include "log.h"
#include "config.h"
#include "appconfig.h"
#include "matches.h"
#include "ipaddr.h"
#include "event.h"
#include "source.h"
#include "filter.h"
#include "backend.h"
#include "statefile.h"
#include "jail.h"
#include "buf.h"
#include "commands.h"
#include "csocket.h"
#include <getopt.h>
#include <signal.h>
#include <sys/resource.h>
/**
* @def SA_REGISTER
* Register signal handler
*/
#define SA_REGISTER(SIGNUM, HANDLER) \
memset(&act, 0x0, sizeof(act)); \
act.sa_handler = HANDLER; \
if (sigaction(SIGNUM, &act, NULL) != 0) { \
f2b_log_msg(log_fatal, "can't register handler for " #SIGNUM); \
return EXIT_FAILURE; \
}
enum { stop = 0, run, reconfig, logrotate, test } state = run;
void signal_handler(int signum) {
switch (signum) {
case SIGUSR1:
f2b_log_msg(log_info, "got SIGUSR1, reopening log file");
state = logrotate;
break;
case SIGTERM:
case SIGINT:
f2b_log_msg(log_info, "got SIGTERM/SIGINT, exiting");
state = stop;
break;
case SIGHUP:
f2b_log_msg(log_note, "got SIGHUP, reloading config");
state = reconfig;
break;
default:
break;
}
}
void usage(int exitcode) {
fprintf(stderr, "Usage: f2b [-c <config>] [-d] [-h] [-t]\n");
exit(exitcode);
}
static time_t started = 0;
#ifndef WITH_CSOCKET
/* add stubs to reduce #ifdef count */
bool
f2b_csocket_create (f2b_config_section_t *config) {
UNUSED(path);
f2b_log_msg(log_warn, "control socket support was disabled at compile-time");
return NULL;
}
void
f2b_csocket_destroy() {
return;
}
int
f2b_csocket_poll(void (*cb)(const f2b_cmd_t *cmd, f2b_buf_t *res)) {
UNUSED(cb); return 0;
}
void
f2b_csocket_cmd_process(const f2b_cmd_t *cmd, f2b_buf_t *res) {
UNUSED(cmd); UNUSED(res); return;
}
void
f2b_csocket_event_broadcast(const char *msg) {
UNUSED(msg);
}
#else /* WITH_CSOCKET */
void
f2b_csocket_cmd_process(const f2b_cmd_t *cmd, f2b_buf_t *res) {
f2b_jail_t *jail = NULL;
char buf[4096] = "";
size_t len = 0;
assert(cmd != NULL);
assert(res != NULL);
if (cmd->type == CMD_UNKNOWN)
return;
if (cmd->type == CMD_HELP) {
f2b_buf_append(res, f2b_cmd_help(), 0);
return;
}
if (cmd->type >= CMD_JAIL_STATUS && cmd->type <= CMD_JAIL_FILTER_RELOAD) {
if ((jail = f2b_jail_find(jails, cmd->args[1])) == NULL) {
len = snprintf(buf, sizeof(buf), "-can't find jail '%s'\n", cmd->args[1]);
f2b_buf_append(res, buf, len);
return;
}
}
if (cmd->type == CMD_RELOAD) {
state = reconfig;
} else if (cmd->type == CMD_LOG_ROTATE) {
state = logrotate;
} else if (cmd->type == CMD_LOG_LEVEL) {
f2b_log_set_level(cmd->args[2]);
} else if (cmd->type == CMD_SHUTDOWN) {
state = stop;
} else if (cmd->type == CMD_STATUS) {
time_t uptime = time(NULL) - started;
len = snprintf(buf, sizeof(buf), "uptime: %ld days, %.1f hrs\n",
uptime / 86400, (float) (uptime % 86400) / 3600);
f2b_buf_append(res, buf, len);
len = snprintf(buf, sizeof(buf), "pid: %u\npidfile: %s\n", getpid(), appconfig.pidfile_path);
f2b_buf_append(res, buf, len);
len = snprintf(buf, sizeof(buf), "statedir: %s\n", appconfig.statedir_path);
f2b_buf_append(res, buf, len);
f2b_buf_append(res, "jails:\n", 0);
for (jail = jails; jail != NULL; jail = jail->next) {
len = snprintf(buf, sizeof(buf), "- %s\n", jail->name);
f2b_buf_append(res, buf, len);
}
return;
} else if (cmd->type == CMD_JAIL_STATUS) {
f2b_jail_cmd_status(buf, sizeof(buf), jail);
f2b_buf_append(res, buf, 0);
return;
} else if (cmd->type == CMD_JAIL_SET) {
f2b_jail_cmd_set(buf, sizeof(buf), jail, cmd->args[3], cmd->args[4]);
f2b_buf_append(res, buf, 0);
return;
} else if (cmd->type == CMD_JAIL_IP_STATUS) {
f2b_jail_cmd_ip_xxx(buf, sizeof(buf), jail, 0, cmd->args[4]);
f2b_buf_append(res, buf, 0);
return;
} else if (cmd->type == CMD_JAIL_IP_BAN) {
f2b_jail_cmd_ip_xxx(buf, sizeof(buf), jail, 1, cmd->args[4]);
f2b_buf_append(res, buf, 0);
return;
} else if (cmd->type == CMD_JAIL_IP_RELEASE) {
f2b_jail_cmd_ip_xxx(buf, sizeof(buf), jail, -1, cmd->args[4]);
f2b_buf_append(res, buf, 0);
return;
} else if (cmd->type == CMD_JAIL_SOURCE_STATS) {
if (jail->flags & JAIL_HAS_SOURCE) {
f2b_source_cmd_stats(buf, sizeof(buf), jail->source);
f2b_buf_append(res, buf, 0);
} else {
f2b_buf_append(res, "-this jail has no source\n", 0);
}
return;
} else if (cmd->type == CMD_JAIL_FILTER_STATS) {
if (jail->flags & JAIL_HAS_FILTER) {
f2b_filter_cmd_stats(buf, sizeof(buf), jail->filter);
f2b_buf_append(res, buf, 0);
} else {
f2b_buf_append(res, "-this jail has no filter\n", 0);
}
return;
} else if (cmd->type == CMD_JAIL_FILTER_RELOAD) {
if (jail->flags & JAIL_HAS_FILTER) {
f2b_filter_cmd_reload(buf, sizeof(buf), jail->filter);
f2b_buf_append(res, buf, 0);
} else {
f2b_buf_append(res, "-this jail has no filter\n", 0);
}
return;
} else {
f2b_buf_append(res, "-error: unknown command\n", 0);
return;
}
f2b_buf_append(res, "+ok\n", 0); /* default reply */
return;
}
#endif /* WITH_CSOCKET */
void
jails_start(f2b_config_t *config) {
f2b_jail_t *jail = NULL;
f2b_config_section_t *jail_config = NULL;
assert(config != NULL);
for (jail_config = config->jails; jail_config != NULL; jail_config = jail_config->next) {
if ((jail = f2b_jail_create(jail_config)) == NULL) {
f2b_log_msg(log_error, "can't create jail '%s'", jail_config->name);
continue;
}
if (!(jail->flags & JAIL_ENABLED)) {
f2b_log_msg(log_debug, "ignoring disabled jail '%s'", jail->name);
free(jail);
continue;
}
if (!f2b_jail_init(jail, config)) {
f2b_log_msg(log_error, "can't init jail '%s'", jail_config->name);
free(jail);
continue;
}
f2b_jail_start(jail);
jail->next = jails;
jails = jail;
}
}
void
jails_stop(f2b_jail_t *jails) {
f2b_jail_t *jail = jails;
f2b_jail_t *next = NULL;
for (; jail != NULL; ) {
next = jail->next;
f2b_jail_stop(jail);
free(jail);
jail = next;
}
}
int main(int argc, char *argv[]) {
struct sigaction act;
f2b_config_t config;
char opt = '\0';
while ((opt = getopt(argc, argv, "c:dht")) != -1) {
switch (opt) {
case 'c':
strlcpy(appconfig.config_path, optarg, sizeof(appconfig.config_path));
break;
case 'd':
appconfig.daemon = true;
break;
case 'h':
usage(EXIT_SUCCESS);
break;
case 't':
state = test;
break;
default:
usage(EXIT_FAILURE);
break;
}
}
SA_REGISTER(SIGTERM, &signal_handler);
SA_REGISTER(SIGINT, &signal_handler);
SA_REGISTER(SIGHUP, &signal_handler);
SA_REGISTER(SIGUSR1, &signal_handler);
if (appconfig.config_path[0] == '\0')
usage(EXIT_FAILURE);
memset(&config, 0x0, sizeof(config));
if (f2b_config_load(&config, appconfig.config_path, true) != true) {
f2b_log_msg(log_error, "can't load config from '%s'", appconfig.config_path);
return EXIT_FAILURE;
}
if (state == test) {
fprintf(stderr, "config test ok\n");
exit(EXIT_SUCCESS);
}
f2b_appconfig_update(config.main);
if (appconfig.daemon) {
pid_t pid = fork();
if (pid > 0)
exit(EXIT_SUCCESS);
/* parent */
if (pid < 0) {
/* parent */
perror("child: fork() failed");
exit(EXIT_FAILURE);
}
/* child */
setsid();
if (getuid() == 0 &&
(setuid(appconfig.uid) != 0 ||
setgid(appconfig.gid) != 0)) {
perror("child: setuid()/setgid() failed");
exit(EXIT_FAILURE);
}
if (chdir("/") != 0) {
perror("child: chdir('/') failed");
exit(EXIT_FAILURE);
}
if (freopen("/dev/null", "r", stdin) == NULL ||
freopen("/dev/null", "w", stdout) == NULL ||
freopen("/dev/null", "w", stderr) == NULL) {
perror("child: freopen() on std streams failed");
exit(EXIT_FAILURE);
}
}
if (appconfig.nice != 0) {
errno = 0;
if (nice(appconfig.nice) == -1 && errno) {
f2b_log_msg(log_warn, "can't set process priority: %s", strerror(errno));
}
}
if (appconfig.coredumps) {
struct rlimit rl;
if (getrlimit(RLIMIT_CORE, &rl) < 0)
f2b_log_msg(log_error, "can't get current coresize limit");
rl.rlim_cur = rl.rlim_max;
if (setrlimit(RLIMIT_CORE, &rl) < 0)
f2b_log_msg(log_error, "can't get current coresize limit");
}
if (appconfig.pidfile_path[0] != '\0') {
FILE *pidfile = NULL;
if ((pidfile = fopen(appconfig.pidfile_path, "w")) != NULL) {
if (flock(fileno(pidfile), LOCK_EX | LOCK_NB) != 0) {
const char *err = (errno == EWOULDBLOCK)
? "another instance already running"
: strerror(errno);
f2b_log_msg(log_fatal, "can't lock pidfile: %s", err);
exit(EXIT_FAILURE);
}
fprintf(pidfile, "%d\n", getpid());
fflush(pidfile);
} else {
f2b_log_msg(log_warn, "can't open pidfile: %s", strerror(errno));
}
}
if (appconfig.statedir_path[0] != '\0') {
struct stat st;
if (stat(appconfig.statedir_path, &st) < 0) {
if (errno == ENOENT) {
mode_t mode = S_IRWXU | S_IRGRP | S_IXGRP; /* 0750 */
mkdir(appconfig.statedir_path, mode);
} else {
f2b_log_msg(log_error, "statedir not exists or unaccessible: %s", strerror(errno));
}
} else if (!S_ISDIR(st.st_mode)) {
f2b_log_msg(log_error, "statedir not a directory: %s", strerror(errno));
}
}
if (config.csocket) {
f2b_csocket_create(config.csocket);
f2b_event_handler_register(&f2b_csocket_event_broadcast);
}
if (config.defaults)
f2b_jail_set_defaults(config.defaults);
started = time(NULL);
jails_start(&config);
f2b_config_free(&config);
if (!jails) {
f2b_log_msg(log_fatal, "no jails configured, exiting");
return EXIT_FAILURE;
}
while (state) {
for (f2b_jail_t *jail = jails; jail != NULL; jail = jail->next) {
f2b_jail_process(jail);
}
f2b_csocket_poll(f2b_csocket_cmd_process);
sleep(1);
if (state == logrotate && strcmp(appconfig.logdest, "file") == 0) {
state = run;
f2b_log_to_file(appconfig.logfile_path);
}
if (state == reconfig) {
state = run;
memset(&config, 0x0, sizeof(config));
if (f2b_config_load(&config, appconfig.config_path, true)) {
jails_stop(jails);
jails = NULL;
if (config.defaults)
f2b_jail_set_defaults(config.defaults);
jails_start(&config);
} else {
f2b_log_msg(log_error, "can't load config from '%s'", appconfig.config_path);
}
f2b_config_free(&config);
}
}
f2b_csocket_destroy();
jails_stop(jails);
jails = NULL;
return EXIT_SUCCESS;
}