dnsbl.c 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. /*
  2. * Copyright (c) 2019 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 <arpa/inet.h>
  17. #include <sys/socket.h>
  18. #include <sys/stat.h>
  19. #include <sys/types.h>
  20. #include <sys/event.h>
  21. #include <sys/time.h>
  22. #include <sys/wait.h>
  23. #include <err.h>
  24. #include <errno.h>
  25. #include <fcntl.h>
  26. #include <netdb.h>
  27. #include <pwd.h>
  28. #include <regex.h>
  29. #include <signal.h>
  30. #include <stdio.h>
  31. #include <stdlib.h>
  32. #include <string.h>
  33. #include <syslog.h>
  34. #include <unistd.h>
  35. #include "dnsbl.h"
  36. #include "log.h"
  37. int run = 1;
  38. int reconfig = 0;
  39. regex_t rx_spamd_connect;
  40. #define CONFFILE "/etc/mail/spamd-dnsbld.conf"
  41. #define SPAMD_USER "_spamd"
  42. #define PATH_SPAMDB "/usr/sbin/spamdb"
  43. #define MAXLINELEN 8192
  44. #define RX_SPAMLOG "spamd\\[[0-9]+\\]: " \
  45. "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})" \
  46. ": connected \\([0-9]+/[0-9]+\\)"
  47. extern int h_errno;
  48. static char *rx_error_string(int, regex_t *);
  49. static void sighandler(int);
  50. static bool lookup_blacklist(const char *, in_addr_t);
  51. static bool handle_async_lookup(struct config *, in_addr_t);
  52. static void handle_logfile(struct config *);
  53. static void usage(void);
  54. __dead void
  55. usage(void)
  56. {
  57. fprintf(stderr, "usage: %s [-dhnv] [-f configfile]\n", getprogname());
  58. exit(1);
  59. }
  60. char *
  61. rx_error_string(int rc, regex_t *rx)
  62. {
  63. static char error[1024];
  64. regerror(rc, rx, error, sizeof(error));
  65. return error;
  66. }
  67. struct spamlist *
  68. spamlist_new(const char *name, const char *dns)
  69. {
  70. struct spamlist *l = calloc(1, sizeof(struct spamlist));
  71. if (l == NULL)
  72. fatal(NULL);
  73. l->name = strdup(name);
  74. if (l->name == NULL)
  75. fatal(NULL);
  76. l->dns = strdup(dns);
  77. if (l->dns == NULL)
  78. fatal(NULL);
  79. return l;
  80. }
  81. void
  82. spamlist_free(struct spamlist *l)
  83. {
  84. if (l) {
  85. free(l->name);
  86. free(l->dns);
  87. free(l);
  88. }
  89. }
  90. struct config *
  91. config_new(void)
  92. {
  93. struct config *c = calloc(1, sizeof(struct config));
  94. if (c == NULL)
  95. fatal(NULL);
  96. TAILQ_INIT(&c->lists);
  97. return c;
  98. }
  99. void
  100. config_free(struct config *c)
  101. {
  102. if (c) {
  103. struct spamlist *l;
  104. while ((l = TAILQ_FIRST(&c->lists)) != NULL) {
  105. TAILQ_REMOVE(&c->lists, l, entry);
  106. spamlist_free(l);
  107. }
  108. if (c->log)
  109. fclose(c->log);
  110. free(c->logfile);
  111. free(c);
  112. }
  113. }
  114. void
  115. config_add_spamlist(struct config *c, const char *name, const char *dns)
  116. {
  117. struct spamlist *l = spamlist_new(name, dns);
  118. TAILQ_INSERT_TAIL(&c->lists, l, entry);
  119. }
  120. void
  121. sighandler(int sig)
  122. {
  123. switch (sig) {
  124. case SIGCHLD: {
  125. wait(NULL);
  126. break;
  127. }
  128. case SIGHUP:
  129. reconfig = 1;
  130. break;
  131. case SIGTERM:
  132. case SIGINT:
  133. run = 0;
  134. break;
  135. default:
  136. break;
  137. }
  138. }
  139. bool
  140. lookup_blacklist(const char *bl, in_addr_t addr)
  141. {
  142. char *revipbl;
  143. const uint32_t hoaddr = ntohl(addr);
  144. const uint8_t *a = (const uint8_t *)&hoaddr;
  145. struct in_addr **ra;
  146. if (asprintf(&revipbl, "%d.%d.%d.%d.%s",
  147. a[0], a[1], a[2], a[3], bl) == -1)
  148. fatal(NULL);
  149. log_debug("Looking up: %s", revipbl);
  150. struct hostent *info = gethostbyname2(revipbl, AF_INET);
  151. if (info == NULL) {
  152. if (h_errno == HOST_NOT_FOUND)
  153. return false;
  154. log_warnx("gethostbyname2: %s", hstrerror(h_errno));
  155. return false;
  156. }
  157. for (ra = (struct in_addr **)info->h_addr_list; *ra; ++ra) {
  158. const uint32_t saddr = (*ra)->s_addr;
  159. return (((saddr & 0xff) == 127)
  160. && (((saddr >> 8) & 0xff) == 0));
  161. }
  162. return false;
  163. }
  164. bool
  165. handle_async_lookup(struct config *cfg, in_addr_t addr)
  166. {
  167. pid_t pid = fork();
  168. if (pid == 0) {
  169. // Child
  170. signal(SIGCHLD, SIG_DFL);
  171. signal(SIGHUP, SIG_DFL);
  172. struct passwd *pw = cfg->pw;
  173. if (setgroups(1, &pw->pw_gid) ||
  174. setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
  175. setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
  176. fatal("can't drop privileges");
  177. struct spamlist *l;
  178. struct in_addr ia;
  179. ia.s_addr = addr;
  180. const char *ip = inet_ntoa(ia);
  181. TAILQ_FOREACH(l, &cfg->lists, entry) {
  182. if (lookup_blacklist(l->dns, addr)) {
  183. log_info("%s didn't like %s", l->name, ip);
  184. execl(PATH_SPAMDB, "spamdb",
  185. "-t", "-a", ip, NULL);
  186. fatal("execl");
  187. }
  188. }
  189. log_info("%s was clean", inet_ntoa(ia));
  190. if (cfg->autowhitelist) {
  191. execl(PATH_SPAMDB, "spamdb", "-a", ip, NULL);
  192. fatal("execl");
  193. }
  194. exit(0);
  195. } else if (pid > 0) {
  196. // Parent
  197. return true;
  198. } else {
  199. fatal(NULL);
  200. }
  201. }
  202. void
  203. handle_logfile(struct config *cfg)
  204. {
  205. char line[MAXLINELEN];
  206. while ((fgets(line, MAXLINELEN, cfg->log)) != NULL) {
  207. regmatch_t match[2];
  208. int rc = regexec(&rx_spamd_connect, line, 2, match, 0);
  209. switch (rc) {
  210. case REG_NOMATCH:
  211. break;
  212. case 0: {
  213. const regmatch_t *m = &match[1];
  214. const size_t len = m->rm_eo - m->rm_so;
  215. char *ip = strndup(line + m->rm_so, len);
  216. handle_async_lookup(cfg, inet_addr(ip));
  217. free(ip);
  218. break;
  219. }
  220. default:
  221. log_warn("regexec: %s",
  222. rx_error_string(rc, &rx_spamd_connect));
  223. break;
  224. }
  225. }
  226. if (feof(cfg->log))
  227. clearerr(cfg->log);
  228. else if (ferror(cfg->log))
  229. fatal(NULL);
  230. }
  231. bool
  232. config_check(struct config *cfg)
  233. {
  234. struct stat sb;
  235. if (cfg->logfile == NULL) {
  236. cfg->logfile = strdup("/var/log/spamd");
  237. if (cfg->logfile == NULL)
  238. fatal(NULL);
  239. }
  240. if (strncmp("/var/log/", cfg->logfile, 9) != 0) {
  241. log_warnx("spamd logfile %s not under /var/log/",
  242. cfg->logfile);
  243. return false;
  244. }
  245. if (stat(cfg->logfile, &sb) == -1) {
  246. log_warnx("unable to stat logfile %s", cfg->logfile);
  247. return false;
  248. }
  249. if (!(sb.st_mode & S_IFREG)) {
  250. log_warnx("logfile %s is no regular file", cfg->logfile);
  251. return false;
  252. }
  253. if (! TAILQ_FIRST(&cfg->lists)) {
  254. log_warnx("no dns blacklists configured");
  255. return false;
  256. }
  257. return true;
  258. }
  259. struct config *
  260. config_reload(struct config *cfg)
  261. {
  262. struct config *n = config_new();
  263. if (!config_parse(n, cfg->config_file) && !config_check(n)) {
  264. log_warnx("Unable to reload config file %s", cfg->config_file);
  265. config_free(n);
  266. return NULL;
  267. }
  268. /* This will close the opened log and remove the events from the
  269. * kqueue. */
  270. config_free(cfg);
  271. return n;
  272. }
  273. int
  274. main(int argc, char **argv)
  275. {
  276. int rc, ch;
  277. int queue, nev;
  278. const char *conffile = CONFFILE;
  279. struct kevent ev[2];
  280. bool noaction = false;
  281. int debug = 0, verbose = 0;
  282. struct config *cfg, *ncfg;
  283. struct passwd *pw;
  284. int logdest = LOG_TO_STDERR;
  285. if (geteuid() != 0)
  286. errx(1, "Need root privileges to run.");
  287. if ((pw = getpwnam(SPAMD_USER)) == NULL)
  288. errx(1, "unknown user %s", SPAMD_USER);
  289. while ((ch = getopt(argc, argv, "df:nv")) != -1) {
  290. switch (ch) {
  291. case 'd':
  292. debug = 2;
  293. break;
  294. case 'f':
  295. conffile = optarg;
  296. break;
  297. case 'n':
  298. noaction = true;
  299. break;
  300. case 'v':
  301. verbose++;
  302. break;
  303. default:
  304. usage();
  305. /* NOT REACHED */
  306. }
  307. }
  308. argc -= optind;
  309. argv += optind;
  310. setprogname("spamd-dnsbld");
  311. if (!debug)
  312. logdest |= LOG_TO_SYSLOG;
  313. log_init(logdest, verbose, LOG_DAEMON);
  314. cfg = config_new();
  315. if (! config_parse(cfg, conffile))
  316. errx(1, "Unable to parse config file");
  317. if (noaction) {
  318. fprintf(stderr, "configuration OK\n");
  319. exit(0);
  320. }
  321. rc = regcomp(&rx_spamd_connect, RX_SPAMLOG, REG_EXTENDED);
  322. if (rc != 0)
  323. errx(1, "%s", rx_error_string(rc, &rx_spamd_connect));
  324. logdest = debug ? LOG_TO_STDERR : LOG_TO_SYSLOG;
  325. log_init(logdest, verbose, LOG_DAEMON);
  326. if (!debug) {
  327. if (daemon(0, 0) == -1)
  328. fatal("daemon");
  329. }
  330. signal(SIGCHLD, sighandler);
  331. signal(SIGHUP, sighandler);
  332. signal(SIGTERM, sighandler);
  333. signal(SIGINT, sighandler);
  334. if ((queue = kqueue()) == -1)
  335. fatal("kqueue");
  336. if (unveil(conffile, "r") == -1)
  337. fatal("unveil");
  338. if (unveil(PATH_SPAMDB, "x") == -1)
  339. fatal("unveil");
  340. if (unveil("/var/log", "r") == -1)
  341. fatal("unveil");
  342. if (pledge("stdio dns proc exec rpath id", NULL) == -1)
  343. fatal("pledge");
  344. reconfig:
  345. cfg->pw = pw;
  346. if ((cfg->log = fopen(cfg->logfile, "r")) == NULL)
  347. fatal("%s", cfg->logfile);
  348. if (fseek(cfg->log, 0L, SEEK_END) < 0)
  349. fatal("fseek");
  350. add_events:
  351. EV_SET(&ev[0], fileno(cfg->log), EVFILT_READ,
  352. EV_ENABLE | EV_ADD | EV_CLEAR, 0, 0, NULL);
  353. EV_SET(&ev[1], fileno(cfg->log), EVFILT_VNODE,
  354. EV_ENABLE | EV_ADD | EV_CLEAR,
  355. NOTE_DELETE | NOTE_RENAME, 0, NULL);
  356. if (kevent(queue, ev, 2, NULL, 0, NULL) < 0)
  357. fatal(NULL);
  358. log_info("Starting event loop...");
  359. struct kevent ke;
  360. while (run) {
  361. nev = kevent(queue, NULL, 0, &ke, 1, NULL);
  362. if (nev == -1) {
  363. if (errno != EINTR) {
  364. log_warn("kevent");
  365. break;
  366. }
  367. }
  368. if (nev > 0) {
  369. if (ke.filter == EVFILT_READ) {
  370. handle_logfile(cfg);
  371. } else if (ke.filter == EVFILT_VNODE) {
  372. if (ke.fflags & NOTE_RENAME) {
  373. log_info("%s renamed...", cfg->logfile);
  374. cfg->log = freopen(cfg->logfile, "r",
  375. cfg->log);
  376. if (cfg->log == NULL)
  377. fatal("freopen");
  378. goto add_events;
  379. } else if (ke.fflags & NOTE_DELETE) {
  380. log_info("%s deleted...", cfg->logfile);
  381. cfg->log = freopen(cfg->logfile, "r",
  382. cfg->log);
  383. if (cfg->log == NULL)
  384. fatal("freopen");
  385. goto add_events;
  386. }
  387. }
  388. }
  389. if (reconfig) {
  390. reconfig = 0;
  391. ncfg = config_reload(cfg);
  392. if (ncfg) {
  393. log_info("Config file reloaded successfully");
  394. cfg = ncfg;
  395. goto reconfig;
  396. }
  397. }
  398. }
  399. close(queue);
  400. regfree(&rx_spamd_connect);
  401. config_free(cfg);
  402. // Wait up to 2 seconds for child processes before exiting
  403. alarm(2);
  404. while (wait(NULL) != -1);
  405. alarm(0);
  406. return 0;
  407. }