/* * Copyright (c) 2020 Markus Hennecke * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "log.h" int run = 1; char *blacklist_table = "blacklist"; regex_t rx_failed_invalid_user; /* * Simplified regex match for IPv6 addresses, the code in add_address() * will check for a valid IPv4/IPv6 address anyway so keep the regex * simple. */ #define RX_FAILED_INVALID_USER "Failed password for " \ "(invalid user [^ ]+|root) from " \ "(" \ "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})" \ "|" \ "([0-9A-Fa-f][0-9A-Fa-f:]+)" \ ")" \ " port [0-9]+ ssh" #define AUTHLOG_FILENAME "/var/log/authlog" #define MAXLINELEN 1024 static int add_address(int, const char *, const char *); static char *rx_error_string(int, regex_t *); static void sighandler(int); static void usage(void); static void handle_authlog(int, FILE *); __dead void usage(void) { fprintf(stderr, "usage: %s [-dv]\n", getprogname()); exit(1); } char * rx_error_string(int rc, regex_t *rx) { static char error[1024]; regerror(rc, rx, error, sizeof(error)); return error; } void sighandler(int sig) { switch (sig) { case SIGTERM: case SIGINT: run = 0; break; default: break; } } void handle_authlog(int dev, FILE *authlog) { char line[MAXLINELEN]; while ((fgets(line, MAXLINELEN, authlog)) != NULL) { regmatch_t match[3]; int rc = regexec(&rx_failed_invalid_user, line, 3, match, 0); switch (rc) { case REG_NOMATCH: break; case 0: { const regmatch_t *m = &match[2]; const size_t len = m->rm_eo - m->rm_so; char *ip = strndup(line + m->rm_so, len); int n = add_address(dev, ip, blacklist_table); if (n == 1) log_info("Added %s to <%s>", ip, blacklist_table); free(ip); } break; default: log_warn("regexec: %s", rx_error_string(rc, &rx_failed_invalid_user)); break; } } if (feof(authlog)) clearerr(authlog); else if (ferror(authlog)) fatal(NULL); } int add_address(int dev, const char *ip, const char *tablename) { int rc; struct pfioc_table io; struct pfr_addr addr; struct pfr_table table; bzero(&addr, sizeof(struct pfr_addr)); rc = inet_pton(AF_INET, ip, &(addr.pfra_ip4addr)); if (-1 == rc) { rc = inet_pton(AF_INET6, ip, &(addr.pfra_ip6addr)); if (-1 == rc) { warn(NULL); return -1; } addr.pfra_af = AF_INET6; addr.pfra_net = 128; } else { addr.pfra_af = AF_INET; addr.pfra_net = 32; } bzero(&table, sizeof(struct pfr_table)); strlcpy(table.pfrt_name, tablename, PF_TABLE_NAME_SIZE); bzero(&io, sizeof(struct pfioc_table)); io.pfrio_flags = PFR_FLAG_FEEDBACK; io.pfrio_table = table; io.pfrio_buffer = &addr; io.pfrio_esize = sizeof(struct pfr_addr); io.pfrio_size = 1; if (ioctl(dev, DIOCRADDADDRS, &io) == -1) { log_warn("DIOCRADDADDRS"); return -1; } return io.pfrio_nadd; } int main(int argc, char **argv) { int dev; int queue, nev; int ch, rc; struct kevent ev[2]; int debug = 0, verbose = 0; FILE *authlog = NULL; if (geteuid() != 0) errx(1, "Need root privileges to run."); while ((ch = getopt(argc, argv, "dv")) != -1) { switch (ch) { case 'd': debug = 2; break; case 'v': verbose++; break; default: usage(); /* NOT REACHED */ } } argc -= optind; argv += optind; if (argc != 0) usage(); setprogname("ssh-sentry"); log_init(debug ? debug : 0, LOG_DAEMON); log_setverbose(verbose); dev = open("/dev/pf", O_RDWR); if (-1 == dev) err(1, "open /dev/pf failed"); rc = regcomp(&rx_failed_invalid_user, RX_FAILED_INVALID_USER, REG_EXTENDED); if (rc != 0) errx(1, "%s", rx_error_string(rc, &rx_failed_invalid_user)); log_init(debug, LOG_DAEMON); log_setverbose(verbose); if (!debug) { if (daemon(0, 0) == -1) fatal("daemon"); } signal(SIGTERM, sighandler); signal(SIGINT, sighandler); if ((queue = kqueue()) == -1) fatal("kqueue"); if (unveil(AUTHLOG_FILENAME, "r") == -1) fatal("unveil"); if (unveil(NULL, NULL) == -1) fatal("unveil"); #if 0 // XXX: pledge for pf does not contain DIOCRADDADDRS, so unless // that is added we can't pledge // The way to go would be to split the process into two, where // the child processes sole purpose would be to talk to /dev/pf if (pledge("stdio rpath pf") == -1) fatal("pledge"); #endif if ((authlog = fopen(AUTHLOG_FILENAME, "r")) == NULL) fatal("%s", AUTHLOG_FILENAME); if (fseek(authlog, 0L, SEEK_END) < 0) fatal("fseek"); add_events: EV_SET(&ev[0], fileno(authlog), EVFILT_READ, EV_ENABLE | EV_ADD | EV_CLEAR, 0, 0, NULL); EV_SET(&ev[1], fileno(authlog), EVFILT_VNODE, EV_ENABLE | EV_ADD | EV_CLEAR, NOTE_DELETE | NOTE_RENAME, 0, NULL); if (kevent(queue, ev, 2, NULL, 0, NULL)) fatal(NULL); log_info("Starting event loop..."); struct kevent ke; while (run) { nev = kevent(queue, NULL, 0, &ke, 1, NULL); if (nev == -1) { if (errno != EINTR) { log_warn("kevent"); break; } } if (nev > 0) { if (ke.filter == EVFILT_READ) { handle_authlog(dev, authlog); } else if (ke.filter == EVFILT_VNODE) { if (ke.fflags & NOTE_RENAME) { log_info("%s renamed...", AUTHLOG_FILENAME); authlog = freopen(AUTHLOG_FILENAME, "r", authlog); if (authlog == NULL) fatal("freopen"); goto add_events; } else if (ke.fflags & NOTE_DELETE) { log_info("%s renamed...", AUTHLOG_FILENAME); authlog = freopen(AUTHLOG_FILENAME, "r", authlog); if (authlog == NULL) fatal("freopen"); goto add_events; } } } } close(queue); close(dev); regfree(&rx_failed_invalid_user); return 0; }