ssh-sentry.c 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. /*
  2. * Copyright (c) 2020 Markus Hennecke <markus-hennecke@markus-hennecke.de>
  3. *
  4. * Permission to use, copy, modify, and distribute this software for any
  5. * purpose with or without fee is hereby granted, provided that the above
  6. * copyright notice and this permission notice appear in all copies.
  7. *
  8. * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  9. * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  10. * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  11. * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  12. * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  13. * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  14. * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. */
  16. #include <sys/types.h>
  17. #include <sys/event.h>
  18. #include <sys/time.h>
  19. #include <sys/socket.h>
  20. #include <sys/ioctl.h>
  21. #include <sys/fcntl.h>
  22. #include <sys/param.h>
  23. #include <arpa/inet.h>
  24. #include <net/if.h>
  25. #include <net/pfvar.h>
  26. #include <err.h>
  27. #include <errno.h>
  28. #include <regex.h>
  29. #include <stdarg.h>
  30. #include <stdio.h>
  31. #include <stdlib.h>
  32. #include <string.h>
  33. #include <strings.h>
  34. #include <syslog.h>
  35. #include <unistd.h>
  36. #include "log.h"
  37. int run = 1;
  38. char *blacklist_table = "blacklist";
  39. regex_t rx_failed_invalid_user;
  40. /*
  41. * Simplified regex match for IPv6 addresses, the code in add_address()
  42. * will check for a valid IPv4/IPv6 address anyway so keep the regex
  43. * simple.
  44. */
  45. #define RX_FAILED_INVALID_USER "Failed password for " \
  46. "(invalid user [^ ]+|root) from " \
  47. "(" \
  48. "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})" \
  49. "|" \
  50. "([0-9A-Fa-f][0-9A-Fa-f:]+)" \
  51. ")" \
  52. " port [0-9]+ ssh"
  53. #define AUTHLOG_FILENAME "/var/log/authlog"
  54. #define MAXLINELEN 1024
  55. static int add_address(int, const char *, const char *);
  56. static char *rx_error_string(int, regex_t *);
  57. static void sighandler(int);
  58. static void usage(void);
  59. static void handle_authlog(int, FILE *);
  60. __dead void
  61. usage(void)
  62. {
  63. fprintf(stderr, "usage: %s [-dv]\n", getprogname());
  64. exit(1);
  65. }
  66. char *
  67. rx_error_string(int rc, regex_t *rx)
  68. {
  69. static char error[1024];
  70. regerror(rc, rx, error, sizeof(error));
  71. return error;
  72. }
  73. void
  74. sighandler(int sig)
  75. {
  76. switch (sig) {
  77. case SIGTERM:
  78. case SIGINT:
  79. run = 0;
  80. break;
  81. default:
  82. break;
  83. }
  84. }
  85. void
  86. handle_authlog(int dev, FILE *authlog)
  87. {
  88. char line[MAXLINELEN];
  89. while ((fgets(line, MAXLINELEN, authlog)) != NULL) {
  90. regmatch_t match[3];
  91. int rc = regexec(&rx_failed_invalid_user, line, 3, match, 0);
  92. switch (rc) {
  93. case REG_NOMATCH:
  94. break;
  95. case 0: {
  96. const regmatch_t *m = &match[2];
  97. const size_t len = m->rm_eo - m->rm_so;
  98. char *ip = strndup(line + m->rm_so, len);
  99. int n = add_address(dev, ip, blacklist_table);
  100. if (n == 1)
  101. log_info("Added %s to <%s>", ip,
  102. blacklist_table);
  103. free(ip);
  104. }
  105. break;
  106. default:
  107. log_warn("regexec: %s",
  108. rx_error_string(rc, &rx_failed_invalid_user));
  109. break;
  110. }
  111. }
  112. if (feof(authlog))
  113. clearerr(authlog);
  114. else if (ferror(authlog))
  115. fatal(NULL);
  116. }
  117. int
  118. add_address(int dev, const char *ip, const char *tablename)
  119. {
  120. int rc;
  121. struct pfioc_table io;
  122. struct pfr_addr addr;
  123. struct pfr_table table;
  124. bzero(&addr, sizeof(struct pfr_addr));
  125. rc = inet_pton(AF_INET, ip, &(addr.pfra_ip4addr));
  126. if (-1 == rc) {
  127. rc = inet_pton(AF_INET6, ip, &(addr.pfra_ip6addr));
  128. if (-1 == rc) {
  129. warn(NULL);
  130. return -1;
  131. }
  132. addr.pfra_af = AF_INET6;
  133. addr.pfra_net = 128;
  134. } else {
  135. addr.pfra_af = AF_INET;
  136. addr.pfra_net = 32;
  137. }
  138. bzero(&table, sizeof(struct pfr_table));
  139. strlcpy(table.pfrt_name, tablename, PF_TABLE_NAME_SIZE);
  140. bzero(&io, sizeof(struct pfioc_table));
  141. io.pfrio_flags = PFR_FLAG_FEEDBACK;
  142. io.pfrio_table = table;
  143. io.pfrio_buffer = &addr;
  144. io.pfrio_esize = sizeof(struct pfr_addr);
  145. io.pfrio_size = 1;
  146. if (ioctl(dev, DIOCRADDADDRS, &io) == -1) {
  147. log_warn("DIOCRADDADDRS");
  148. return -1;
  149. }
  150. return io.pfrio_nadd;
  151. }
  152. int
  153. main(int argc, char **argv)
  154. {
  155. int dev;
  156. int queue, nev;
  157. int ch, rc;
  158. struct kevent ev[2];
  159. int debug = 0, verbose = 0;
  160. FILE *authlog = NULL;
  161. if (geteuid() != 0)
  162. errx(1, "Need root privileges to run.");
  163. while ((ch = getopt(argc, argv, "dv")) != -1) {
  164. switch (ch) {
  165. case 'd':
  166. debug = 2;
  167. break;
  168. case 'v':
  169. verbose++;
  170. break;
  171. default:
  172. usage();
  173. /* NOT REACHED */
  174. }
  175. }
  176. argc -= optind;
  177. argv += optind;
  178. if (argc != 0)
  179. usage();
  180. setprogname("ssh-sentry");
  181. log_init(debug ? debug : 0, LOG_DAEMON);
  182. log_setverbose(verbose);
  183. dev = open("/dev/pf", O_RDWR);
  184. if (-1 == dev)
  185. err(1, "open /dev/pf failed");
  186. rc = regcomp(&rx_failed_invalid_user, RX_FAILED_INVALID_USER,
  187. REG_EXTENDED);
  188. if (rc != 0)
  189. errx(1, "%s", rx_error_string(rc, &rx_failed_invalid_user));
  190. log_init(debug, LOG_DAEMON);
  191. log_setverbose(verbose);
  192. if (!debug) {
  193. if (daemon(0, 0) == -1)
  194. fatal("daemon");
  195. }
  196. signal(SIGTERM, sighandler);
  197. signal(SIGINT, sighandler);
  198. if ((queue = kqueue()) == -1)
  199. fatal("kqueue");
  200. if (unveil(AUTHLOG_FILENAME, "r") == -1)
  201. fatal("unveil");
  202. if (unveil(NULL, NULL) == -1)
  203. fatal("unveil");
  204. #if 0
  205. // XXX: pledge for pf does not contain DIOCRADDADDRS, so unless
  206. // that is added we can't pledge
  207. // The way to go would be to split the process into two, where
  208. // the child processes sole purpose would be to talk to /dev/pf
  209. if (pledge("stdio rpath pf") == -1)
  210. fatal("pledge");
  211. #endif
  212. if ((authlog = fopen(AUTHLOG_FILENAME, "r")) == NULL)
  213. fatal("%s", AUTHLOG_FILENAME);
  214. if (fseek(authlog, 0L, SEEK_END) < 0)
  215. fatal("fseek");
  216. add_events:
  217. EV_SET(&ev[0], fileno(authlog), EVFILT_READ,
  218. EV_ENABLE | EV_ADD | EV_CLEAR, 0, 0, NULL);
  219. EV_SET(&ev[1], fileno(authlog), EVFILT_VNODE,
  220. EV_ENABLE | EV_ADD | EV_CLEAR,
  221. NOTE_DELETE | NOTE_RENAME, 0, NULL);
  222. if (kevent(queue, ev, 2, NULL, 0, NULL))
  223. fatal(NULL);
  224. log_info("Starting event loop...");
  225. struct kevent ke;
  226. while (run) {
  227. nev = kevent(queue, NULL, 0, &ke, 1, NULL);
  228. if (nev == -1) {
  229. if (errno != EINTR) {
  230. log_warn("kevent");
  231. break;
  232. }
  233. }
  234. if (nev > 0) {
  235. if (ke.filter == EVFILT_READ) {
  236. handle_authlog(dev, authlog);
  237. } else if (ke.filter == EVFILT_VNODE) {
  238. if (ke.fflags & NOTE_RENAME) {
  239. log_info("%s renamed...",
  240. AUTHLOG_FILENAME);
  241. authlog = freopen(AUTHLOG_FILENAME,
  242. "r", authlog);
  243. if (authlog == NULL)
  244. fatal("freopen");
  245. goto add_events;
  246. } else if (ke.fflags & NOTE_DELETE) {
  247. log_info("%s renamed...",
  248. AUTHLOG_FILENAME);
  249. authlog = freopen(AUTHLOG_FILENAME,
  250. "r", authlog);
  251. if (authlog == NULL)
  252. fatal("freopen");
  253. goto add_events;
  254. }
  255. }
  256. }
  257. }
  258. close(queue);
  259. close(dev);
  260. regfree(&rx_failed_invalid_user);
  261. return 0;
  262. }