/*- * Copyright (c) 2004 Free (Olivier Beyssac) * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "options.h" #include "parse_args.h" #include "parse_config.h" #include "net.h" #include "cmd.h" #include "iptree.h" #include "daemon.h" #include "netlist.h" #include "utils.h" #define BANNER "220 "PROGNAME" "PROGRAM_VERSION"\r\n" #ifdef FD_SETSIZE #define MAX_CLIENTS (FD_SETSIZE >= 160 ? 150 : FD_SETSIZE - 10) #else #define MAX_CLIENTS (150) #endif /* FD_SETSIZE */ struct options opt; /* global runtime options/parameters */ iptree ipt; /* global iptree */ int ncli = 0; /* clients connected */ int listen_sd; /* listening socket */ /* Information about clients */ struct { int sd; struct sockaddr_in addr; int reply; int mode; time_t last; } client[MAX_CLIENTS]; #define close_client(i) \ do { \ close(client[i].sd); \ client[i].sd = -1; \ client[i].last = 0; \ ncli--; \ } while(0) static void dump_data(void) { iptree_dump2files(ipt); opt.last_dump = time(NULL); } /* Exit cleanly */ static void exit_signal_handler(const int signo) { int i; syslog(LOG_INFO, "terminated by signal %d, closing all connections", signo); close(listen_sd); for (i = 0; i < MAX_CLIENTS; i++) if (client[i].sd != -1) { if (opt.log_level >= 3) syslog(LOG_INFO, "closing slot %d", i); close(client[i].sd); } /* Dump data to disk before exiting */ dump_data(); if (opt.daemon && unlink(opt.pid_filename) < 0) syslog(LOG_ERR, "unlink: %s", strerror(errno)); exit(EXIT_SUCCESS); } /* Show some stats when we catch USR1 */ static void usr1_signal_handler(const int signo) { struct rusage u; time_t t = time(NULL); syslog(LOG_INFO, "caught signal %d", signo); syslog(LOG_INFO, "start time: %lu (age: %lu secs)", (unsigned long)opt.start_time, (unsigned long)t - opt.start_time); syslog(LOG_INFO, "submissions: %lu", opt.submissions); syslog(LOG_INFO, "insertions in BL: %lu", opt.insertqueries); syslog(LOG_INFO, "notifies: %lu", opt.notifies); syslog(LOG_INFO, "decrements: %lu", opt.decrqueries); syslog(LOG_INFO, "positive BL queries: %lu", opt.positive_blqueries); syslog(LOG_INFO, "total BL queries: %lu", opt.blqueries); syslog(LOG_INFO, "bad requests: %lu", opt.bad_requests); syslog(LOG_INFO, "denied requests: %lu", opt.denied_requests); syslog(LOG_INFO, "clients: %d/%d", ncli, MAX_CLIENTS); iptree_show_stats(ipt, t); if (getrusage(RUSAGE_SELF, &u) == -1) { syslog(LOG_ERR, "getrusage(): %s", strerror(errno)); return; } syslog(LOG_INFO, "utime=%li.%lis - stime=%li.%lis", u.ru_utime.tv_sec, u.ru_utime.tv_usec, u.ru_stime.tv_sec, u.ru_stime.tv_usec); syslog(LOG_INFO, "maxrss=%li", u.ru_maxrss); syslog(LOG_INFO, "ixrss=%li - idrss=%li", u.ru_ixrss, u.ru_idrss); syslog(LOG_INFO, "minflt=%li - majflt=%li", u.ru_minflt, u.ru_majflt); syslog(LOG_INFO, "nswap=%li", u.ru_nswap); syslog(LOG_INFO, "inblock=%li - oublock=%li", u.ru_inblock, u.ru_oublock); syslog(LOG_INFO, "msgsnd=%li - msgrcv=%li", u.ru_msgsnd, u.ru_msgrcv); syslog(LOG_INFO, "nsignals=%li", u.ru_nsignals); syslog(LOG_INFO, "nvcsw=%lu - nivcsw=%lu", u.ru_nvcsw, u.ru_nivcsw); #ifdef SHOW_PROC_STATUS sprintf(filename, "/proc/%d/status", getpid()); if ((f = fopen(filename, "r")) == NULL) { syslog(LOG_ERR, "fopen: %s", strerror(errno)); return; } while (fgets(buf, sizeof(buf), f)) syslog(LOG_INFO, buf); fclose(f); #endif /* PROC_STATUS_EXISTS */ } /* Dump data to disk when catching USR2 */ static void usr2_signal_handler(const int signo) { syslog(LOG_INFO, "caught signal %d", signo); dump_data(); } /* Catch terminated childs */ static void chld_signal_handler(const int signo) { pid_t pid; int status; status = signo; /* don't like gcc warnings :) */ while ((pid = waitpid(-1, &status, WNOHANG)) > 0) if (opt.log_level >= 3) syslog(LOG_INFO, "child %d terminated", pid); } static void usage(void) { printf("Usage: %s [-v] [-p port] [-n] [-l loglevel]\n", opt.progname); printf("\nOptions:\n"); printf(" -h this help\n"); printf(" -v output version information and exit\n"); printf(" -f file use a specific configuration file\n"); printf(" (default: %s)\n", opt.config_file); printf(" -n do not fork as a daemon\n"); printf(" -a ip address to bind to (default: %s)\n", opt.listening_ip ? opt.listening_ip : "any"); printf(" -p port port to listen to (default: %s)\n", opt.port); printf(" -t number min. time interval before blacklisting (default: %lu secs)\n", (unsigned long)opt.interval); printf(" -m number maximum submissions in time interval (default %d)\n", opt.max_req); printf(" -i number IP list size (default: %d)\n", opt.list_size); printf(" -b number blacklist size (default: %d)\n", opt.blacklist_size); printf(" -e number blacklist expiration (default: %lu secs)\n", (unsigned long)opt.blacklist_expiration); printf(" -l number log level (0-3, default: %d)\n", opt.log_level); printf(" -u user user to run as\n"); printf(" -g group group to run as\n"); printf(" -T number client timeout (default %lu secs)\n", (unsigned long)opt.client_timeout); printf(" -I file which file to dump IP list do\n"); printf(" (default: `%s')\n", opt.wl_filename); printf(" -B file which file to dump blacklist to\n"); printf(" (default: `%s')\n", opt.bl_filename); printf(" -P file PID filename (default: `%s')\n", opt.pid_filename); printf(" -A file ACL filename (default: `%s')\n", opt.acl_filename); printf(" -W file whitelist filename (default: `%s')\n", opt.whitelist_filename); } /* Return a free slot for a client. FIXME: should be linear and keep maxsd more optimal for select() below */ static int get_client_slot(const int max) { int i, j; if (max == MAX_CLIENTS) return -1; for (i = 0, j = MAX_CLIENTS - 1; i < MAX_CLIENTS && j != i && client[i].sd != -1 && client[j].sd != -1; i++, j--); if (i < MAX_CLIENTS && client[i].sd == -1) return i; if (j > 0 && client[j].sd == -1) return j; return -1; } /* Reload ACL and whitelist files if they changed and if they contain valid information */ static void reload_files(void) { struct stat sb; netlist nl; #if 0 struct options sv_opt; /* Untested code. */ /* Config */ if (stat(opt.config_file, &sb) == 0) { if (S_ISREG(sb.st_mode) && sb.st_mtime > opt.config_mtime) { /* File modified */ opt.config_mtime = sb.st_mtime; if (opt.log_level >= 2) syslog(LOG_INFO, "%s changed on disk, reloading", opt.config_file); memcpy(&sv_opt, &opt, sizeof(struct options)); if (!parse_config()) { syslog(LOG_ERR, "error while parsing %s, restoring old configuration", opt.config_file); memcpy(&opt, &sv_opt, sizeof(struct options)); } } } else if (errno == ENOENT && opt.config_mtime) { /* File removed */ if (opt.log_level >= 2) syslog(LOG_INFO, "%s disappeared", opt.config_file); opt.config_mtime = 0; } #endif /* ACL */ if (stat(opt.acl_filename, &sb) == 0) { if (S_ISREG(sb.st_mode) && sb.st_mtime > opt.acl_mtime) { /* File modified */ opt.acl_mtime = sb.st_mtime; if (opt.log_level >= 2) syslog(LOG_INFO, "%s changed on disk, reloading", opt.acl_filename); if ((nl = netlist_init()) == NULL) { syslog(LOG_ERR, "netlist_init() failed for ACL, keeping old configuration"); } else if (netlist_acl_getfromfile(nl, opt.acl_filename) == -1) { syslog(LOG_ERR, "error while parsing %s, keeping old configuration", opt.acl_filename); free(nl); } else { netlist_free(opt.acl); opt.acl = nl; } } } else if (errno == ENOENT && opt.acl_mtime) { /* File removed */ if (opt.log_level >= 2) syslog(LOG_INFO, "%s disappeared, cleaning list", opt.acl_filename); netlist_free(opt.acl); opt.acl = netlist_init(); opt.acl_mtime = 0; } /* Whitelist */ if (stat(opt.whitelist_filename, &sb) == 0) { if (S_ISREG(sb.st_mode) && sb.st_mtime > opt.whitelist_mtime) { /* File modified */ opt.whitelist_mtime = sb.st_mtime; if (opt.log_level >= 2) syslog(LOG_INFO, "%s changed on disk, reloading", opt.whitelist_filename); if ((nl = netlist_init()) == NULL) { syslog(LOG_ERR, "netlist_init() failed for whitelist, keeping old configuration"); } else if (netlist_whitelist_getfromfile(nl, opt.whitelist_filename) == -1) { syslog(LOG_ERR, "error while parsing %s, keeping old configuration", opt.whitelist_filename); free(nl); } else { netlist_free(opt.whitelist); opt.whitelist = nl; } } } else if (errno == ENOENT && opt.whitelist_mtime) { /* File removed */ if (opt.log_level >= 2) syslog(LOG_INFO, "%s disappeared, cleaning list", opt.whitelist_filename); netlist_free(opt.whitelist); opt.whitelist = netlist_init(); opt.whitelist_mtime = 0; } } /* Set effective user and group. Exit with 1 on error */ static void set_eugid(void) { int id; struct passwd *pw; struct group *gr; if (opt.gid != NULL) { if ((gr = getgrnam(opt.gid)) == NULL && ((id = xstrtol(opt.gid)) < 0 || (gr = getgrgid(id)) == NULL)) { fprintf(stderr, "%s: Invalid group: %s\n", opt.progname, opt.gid); exit(EXIT_FAILURE); } if (setegid(gr->gr_gid) == -1) { perror("setegid"); exit(EXIT_FAILURE); } if (setgroups(1, &gr->gr_gid) == -1) { perror("setgroups"); exit(EXIT_FAILURE); } } if (opt.uid != NULL) { if ((pw = getpwnam(opt.uid)) == NULL && ((id = xstrtol(opt.uid)) < 0 || (pw = getpwuid(id)) == NULL)) { fprintf(stderr, "%s: Invalid user: %s\n", opt.progname, opt.uid); exit(EXIT_FAILURE); } if (seteuid(pw->pw_uid) == -1) { perror("seteuid"); exit(EXIT_FAILURE); } } } /* Fill the buffer with the reply code and human understandable string */ static void buildreply(const int code, char *buffer, const size_t maxsize) { size_t len; switch (code) { case 200: snprintf(buffer, maxsize, "%d OK", code); break; case 421: snprintf(buffer, maxsize, "%d Blacklisted", code); break; case 500: snprintf(buffer, maxsize, "%d Syntax error", code); break; case 600: snprintf(buffer, maxsize, "%d Authorization failed", code); break; default: snprintf(buffer, maxsize, "%d", code); } len = strlen(buffer); if (len >= maxsize - 2) sprintf(buffer + len - 2, "\r\n"); else sprintf(buffer + len, "\r\n"); return; } extern int main(int argc, char **argv) { int i, client_sd, maxsd, ret; struct sockaddr_in cliaddr; size_t csize = sizeof(cliaddr); ssize_t rd, wd; char buf[MAX_CMD_LEN+1]; fd_set rset, svrset, wset, svwset; int slot, errors; struct sigaction sa; struct stat sb; struct timeval tv; openlog(PROGNAME, LOG_PID, LOG_DAEMON); xsigaction(sa, SIGINT, exit_signal_handler); xsigaction(sa, SIGTERM, exit_signal_handler); xsigaction(sa, SIGUSR1, usr1_signal_handler); xsigaction(sa, SIGUSR2, usr2_signal_handler); xsigaction(sa, SIGCHLD, chld_signal_handler); options_init(argv[0]); parse_args_for_config_file(argc, argv); if (!parse_config()) exit(EXIT_FAILURE); if ((ret = parse_args(argc, argv)) == -1) { usage(); exit(EXIT_FAILURE); } else if (ret == 0) { usage(); exit(EXIT_SUCCESS); } if (netlist_acl_getfromfile(opt.acl, opt.acl_filename) == -1) { fprintf(stderr, "%s: Error while parsing %s\n", opt.progname, opt.acl_filename); exit(EXIT_FAILURE); } if (netlist_whitelist_getfromfile(opt.whitelist, opt.whitelist_filename) == -1) { fprintf(stderr, "%s: Error while parsing %s\n", opt.progname, opt.whitelist_filename); exit(EXIT_FAILURE); } if (stat(opt.config_file, &sb) == 0 && S_ISREG(sb.st_mode)) opt.config_mtime = sb.st_mtime; if (stat(opt.acl_filename, &sb) == 0 && S_ISREG(sb.st_mode)) opt.acl_mtime = sb.st_mtime; if (stat(opt.whitelist_filename, &sb) == 0 && S_ISREG(sb.st_mode)) opt.whitelist_mtime = sb.st_mtime; set_eugid(); if (opt.log_level >= 3) options_log(); opt.syslog = 1; if (opt.daemon) daemon_init(); umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); if ((listen_sd = net_server_sock_create(opt.listening_ip, opt.port)) == -1) exit(EXIT_FAILURE); net_socket_set_nb(listen_sd); ipt = iptree_init(); /* Initialize all client slots */ for (i = 0; i < MAX_CLIENTS; i++) { client[i].sd = -1; client[i].reply = -1; client[i].last = 0; } FD_ZERO(&svrset); FD_SET(listen_sd, &svrset); FD_ZERO(&svwset); /* FIXME: should optimize maxsd count for select() */ maxsd = MAX_CLIENTS; syslog(LOG_INFO, "listening on port %s", opt.port); errors = 0; for (;;) { int nsel; time_t t = time(NULL); if (t - opt.last_dump > opt.dump_delay) dump_data(); /* Close idle connections */ if (ncli) for (i = 0; i < MAX_CLIENTS; i++) if (client[i].sd != -1 && client[i].last && t - client[i].last >= opt.client_timeout) { if (opt.log_level >= 2) syslog(LOG_INFO, "connection with %s closed (timeout)", inet_ntoa(client[i].addr.sin_addr)); if (FD_ISSET(client[i].sd, &svrset)) FD_CLR(client[i].sd, &svrset); if (FD_ISSET(client[i].sd, &svwset)) FD_CLR(client[i].sd, &svwset); close_client(i); } /* Reload ACL and whitelist files if needed */ reload_files(); rset = svrset; wset = svwset; /* Wake up at least every 60 seconds to perform some cleanup */ tv.tv_sec = 60; tv.tv_usec = 0; if ((nsel = select(maxsd + 1, &rset, &wset, NULL, &tv)) == -1 && errno != EINTR) { syslog(LOG_ERR, "error in select(): %s", strerror(errno)); if (++errors >= 3) { syslog(LOG_ERR, "too many errors, aborting"); close(listen_sd); exit(EXIT_FAILURE); } continue; } if (FD_ISSET(listen_sd, &rset)) { /* First look for incoming connections */ nsel--; client_sd = accept(listen_sd, (struct sockaddr *)&cliaddr, &csize); if (client_sd < 0) { if (errno != EINTR && errno != EWOULDBLOCK) syslog(LOG_ERR, "accept: %s (errno=%d)", strerror(errno), errno); } else { slot = get_client_slot(ncli); if (slot == -1) close(client_sd); else { int mode = netlist_getmode(opt.acl, ntohl(cliaddr.sin_addr.s_addr)); if (!mode) { close(client_sd); syslog(LOG_INFO, "closed unauthorized connection from %s", inet_ntoa(cliaddr.sin_addr)); } else { ncli++; net_socket_set_nb(client_sd); client[slot].sd = client_sd; client[slot].addr.sin_addr = cliaddr.sin_addr; client[slot].mode = mode; client[slot].last = time(NULL); if (opt.log_level == 2) syslog(LOG_INFO, "connect from %s (%d/%d) [%d]", inet_ntoa(cliaddr.sin_addr), ncli, MAX_CLIENTS, mode); write(client_sd, BANNER, strlen(BANNER)); FD_SET(client_sd, &svrset); } /* if (!mode) [...] else */ } /* if (slot == -1) [...] else */ } /* if (client_sd < 0) [...] else */ } /* if (FD_ISSET(listen_sd, &rset) */ /* Then have a look at open connections */ for (i = 0; nsel > 0 && i < maxsd; i++) { if (client[i].sd != -1) { if (FD_ISSET(client[i].sd, &rset)) { /* Something to read */ nsel--; if ((rd = read(client[i].sd, &buf, MAX_CMD_LEN)) <= 0) { if (rd < 0 && (errno == EINTR || errno == EWOULDBLOCK)) continue; if (rd < 0) syslog(LOG_ERR, "read: %s", strerror(errno)); FD_CLR(client[i].sd, &svrset); close_client(i); } else { client[i].reply = read_cmd(buf, rd, inet_ntoa(cliaddr.sin_addr), ipt, client[i].mode); client[i].last = time(NULL); FD_CLR(client[i].sd, &svrset); FD_SET(client[i].sd, &svwset); } } else if (FD_ISSET(client[i].sd, &wset)) { /* Something to write */ nsel--; buildreply(client[i].reply, buf, MAX_CMD_LEN + 1); wd = write(client[i].sd, &buf, strlen(buf)); if (wd < 0 && (errno == EINTR || errno == EWOULDBLOCK)) continue; if (wd < 0) syslog(LOG_ERR, "write: %s", strerror(errno)); FD_CLR(client[i].sd, &svwset); close_client(i); if (opt.log_level == 2) syslog(LOG_INFO, "connection with %s closed (%d/%d)", inet_ntoa(client[i].addr.sin_addr), ncli, MAX_CLIENTS); } /* if (FD_ISSET(client[i].sd, &rset)) [...] else [...] */ } /* if (client[i].sd != -1) */ } /* for (i = 0; i < MAX_CLIENTS; i++) */ } /* for (;;) */ /* Can never be called but exit with a polite code */ exit(EXIT_SUCCESS); }