/* $OpenBSD: spamd-setup.c,v 1.50 2017/07/07 00:10:15 djm Exp $ */ /* * Copyright (c) 2003 Bob Beck. 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 ``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 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 #define PATH_FTP "/usr/bin/ftp" #define PATH_PFCTL "/sbin/pfctl" #define PATH_SPAMD_CONF "/etc/mail/spamd.conf" #define SPAMD_ARG_MAX 256 /* max # of args to an exec */ #define SPAMD_USER "_spamd" struct cidr { u_int32_t addr; u_int8_t bits; }; struct bl { u_int32_t addr; int8_t b; int8_t w; }; struct blacklist { char *name; char *message; struct bl *bl; size_t blc, bls; u_int8_t black; }; u_int32_t imask(u_int8_t); u_int8_t maxblock(u_int32_t, u_int8_t); u_int8_t maxdiff(u_int32_t, u_int32_t); struct cidr *range2cidrlist(struct cidr *, u_int *, u_int *, u_int32_t, u_int32_t); void cidr2range(struct cidr, u_int32_t *, u_int32_t *); char *atop(u_int32_t); int parse_netblock(char *, struct bl *, struct bl *, int); int open_child(char *, char **, int); int fileget(char *); int open_file(char *, char *); char *fix_quoted_colons(char *); void do_message(FILE *, char *); struct bl *add_blacklist(struct bl *, size_t *, size_t *, gzFile, int); int cmpbl(const void *, const void *); struct cidr *collapse_blacklist(struct bl *, size_t, u_int *); int configure_spamd(u_short, char *, char *, struct cidr *, u_int); int configure_pf(struct cidr *); int getlist(char **, char *, struct blacklist *, struct blacklist *); __dead void usage(void); uid_t spamd_uid; gid_t spamd_gid; int debug; int dryrun; int greyonly = 1; extern char *__progname; #define MAXIMUM(a,b) (((a)>(b))?(a):(b)) u_int32_t imask(u_int8_t b) { if (b == 0) return (0); return (0xffffffffU << (32 - b)); } u_int8_t maxblock(u_int32_t addr, u_int8_t bits) { u_int32_t m; while (bits > 0) { m = imask(bits - 1); if ((addr & m) != addr) return (bits); bits--; } return (bits); } u_int8_t maxdiff(u_int32_t a, u_int32_t b) { u_int8_t bits = 0; u_int32_t m; b++; while (bits < 32) { m = imask(bits); if ((a & m) != (b & m)) return (bits); bits++; } return (bits); } struct cidr * range2cidrlist(struct cidr *list, u_int *cli, u_int *cls, u_int32_t start, u_int32_t end) { u_int8_t maxsize, diff; struct cidr *tmp; while (end >= start) { maxsize = maxblock(start, 32); diff = maxdiff(start, end); maxsize = MAXIMUM(maxsize, diff); if (*cls <= *cli + 1) { /* one extra for terminator */ tmp = reallocarray(list, *cls + 32, sizeof(struct cidr)); if (tmp == NULL) err(1, NULL); list = tmp; *cls += 32; } list[*cli].addr = start; list[*cli].bits = maxsize; (*cli)++; start = start + (1 << (32 - maxsize)); } return (list); } void cidr2range(struct cidr cidr, u_int32_t *start, u_int32_t *end) { *start = cidr.addr; *end = cidr.addr + (1 << (32 - cidr.bits)) - 1; } char * atop(u_int32_t addr) { struct in_addr in; memset(&in, 0, sizeof(in)); in.s_addr = htonl(addr); return (inet_ntoa(in)); } int parse_netblock(char *buf, struct bl *start, struct bl *end, int white) { char astring[16], astring2[16]; unsigned maskbits; struct cidr c; /* skip leading spaces */ while (*buf == ' ') buf++; /* bail if it's a comment */ if (*buf == '#') return (0); /* otherwise, look for a netblock of some sort */ if (sscanf(buf, "%15[^/]/%u", astring, &maskbits) == 2) { /* looks like a cidr */ memset(&c.addr, 0, sizeof(c.addr)); if (inet_net_pton(AF_INET, astring, &c.addr, sizeof(c.addr)) == -1) return (0); c.addr = ntohl(c.addr); if (maskbits > 32) return (0); c.bits = maskbits; cidr2range(c, &start->addr, &end->addr); end->addr += 1; } else if (sscanf(buf, "%15[0123456789.]%*[ -]%15[0123456789.]", astring, astring2) == 2) { /* looks like start - end */ memset(&start->addr, 0, sizeof(start->addr)); memset(&end->addr, 0, sizeof(end->addr)); if (inet_net_pton(AF_INET, astring, &start->addr, sizeof(start->addr)) == -1) return (0); start->addr = ntohl(start->addr); if (inet_net_pton(AF_INET, astring2, &end->addr, sizeof(end->addr)) == -1) return (0); end->addr = ntohl(end->addr) + 1; if (start > end) return (0); } else if (sscanf(buf, "%15[0123456789.]", astring) == 1) { /* just a single address */ memset(&start->addr, 0, sizeof(start->addr)); if (inet_net_pton(AF_INET, astring, &start->addr, sizeof(start->addr)) == -1) return (0); start->addr = ntohl(start->addr); end->addr = start->addr + 1; } else return (0); if (white) { start->b = 0; start->w = 1; end->b = 0; end->w = -1; } else { start->b = 1; start->w = 0; end->b = -1; end->w = 0; } return (1); } void drop_privileges(void) { if (setgroups(1, &spamd_gid) != 0) err(1, "setgroups %ld", (long)spamd_gid); if (setresgid(spamd_gid, spamd_gid, spamd_gid) != 0) err(1, "setresgid %ld", (long)spamd_gid); if (setresuid(spamd_uid, spamd_uid, spamd_uid) != 0) err(1, "setresuid %ld", (long)spamd_uid); } int open_child(char *file, char **argv, int drop_privs) { int pdes[2]; if (pipe(pdes) != 0) return (-1); switch (fork()) { case -1: close(pdes[0]); close(pdes[1]); return (-1); case 0: /* child */ close(pdes[0]); if (pdes[1] != STDOUT_FILENO) { dup2(pdes[1], STDOUT_FILENO); close(pdes[1]); } if (drop_privs) drop_privileges(); closefrom(STDERR_FILENO + 1); execvp(file, argv); _exit(1); } /* parent */ close(pdes[1]); return (pdes[0]); } int fileget(char *url) { char *argv[6]; argv[0] = "ftp"; argv[1] = "-V"; argv[2] = "-o"; argv[3] = "-"; argv[4] = url; argv[5] = NULL; if (debug) fprintf(stderr, "Getting %s\n", url); return (open_child(PATH_FTP, argv, 1)); } int open_file(char *method, char *file) { char *url; char **ap, **argv; int len, i, oerrno; if ((method == NULL) || (strcmp(method, "file") == 0)) return (open(file, O_RDONLY)); if (strcmp(method, "http") == 0 || strcmp(method, "https") == 0 || strcmp(method, "ftp") == 0) { if (asprintf(&url, "%s://%s", method, file) == -1) return (-1); i = fileget(url); free(url); return (i); } else if (strcmp(method, "exec") == 0) { len = strlen(file); argv = calloc(len, sizeof(char *)); if (argv == NULL) return (-1); for (ap = argv; ap < &argv[len - 1] && (*ap = strsep(&file, " \t")) != NULL;) { if (**ap != '\0') ap++; } *ap = NULL; i = open_child(argv[0], argv, 0); oerrno = errno; free(argv); errno = oerrno; return (i); } errx(1, "Unknown method %s", method); return (-1); /* NOTREACHED */ } /* * fix_quoted_colons walks through a buffer returned by cgetent. We * look for quoted strings, to escape colons (:) in quoted strings for * getcap by replacing them with \C so cgetstr() deals with it correctly * without having to see the \C bletchery in a configuration file that * needs to have urls in it. Frees the buffer passed to it, passes back * another larger one, with can be used with cgetxxx(), like the original * buffer, it must be freed by the caller. * This should really be a temporary fix until there is a sanctioned * way to make getcap(3) handle quoted strings like this in a nicer * way. */ char * fix_quoted_colons(char *buf) { int in = 0; size_t i, j = 0; char *newbuf, last; /* Allocate enough space for a buf of all colons (impossible). */ newbuf = malloc(2 * strlen(buf) + 1); if (newbuf == NULL) return (NULL); last = '\0'; for (i = 0; i < strlen(buf); i++) { switch (buf[i]) { case ':': if (in) { newbuf[j++] = '\\'; newbuf[j++] = 'C'; } else newbuf[j++] = buf[i]; break; case '"': if (last != '\\') in = !in; newbuf[j++] = buf[i]; break; default: newbuf[j++] = buf[i]; } last = buf[i]; } free(buf); newbuf[j] = '\0'; return (newbuf); } void do_message(FILE *sdc, char *msg) { size_t i, bs = 0, bu = 0, len; ssize_t n; char *buf = NULL, last, *tmp; int fd; len = strlen(msg); if (msg[0] == '"' && msg[len - 1] == '"') { /* quoted msg, escape newlines and send it out */ msg[len - 1] = '\0'; buf = msg + 1; bu = len - 2; goto sendit; } else { /* * message isn't quoted - try to open a local * file and read the message from it. */ fd = open(msg, O_RDONLY); if (fd == -1) err(1, "Can't open message from %s", msg); for (;;) { if (bu == bs) { tmp = realloc(buf, bs + 8192); if (tmp == NULL) err(1, NULL); bs += 8192; buf = tmp; } n = read(fd, buf + bu, bs - bu); if (n == 0) { goto sendit; } else if (n == -1) { err(1, "Can't read from %s", msg); } else bu += n; } buf[bu]='\0'; } sendit: fprintf(sdc, ";\""); last = '\0'; for (i = 0; i < bu; i++) { /* handle escaping the things spamd wants */ switch (buf[i]) { case 'n': if (last == '\\') fprintf(sdc, "\\\\n"); else fputc('n', sdc); last = '\0'; break; case '\n': fprintf(sdc, "\\n"); last = '\0'; break; case '"': fputc('\\', sdc); /* FALLTHROUGH */ default: fputc(buf[i], sdc); last = '\0'; } } fputc('"', sdc); if (bs != 0) free(buf); } /* retrieve a list from fd. add to blacklist bl */ struct bl * add_blacklist(struct bl *bl, size_t *blc, size_t *bls, gzFile gzf, int white) { int i, n, start, bu = 0, bs = 0, serrno = 0; char *buf = NULL, *tmp; struct bl *blt; for (;;) { /* read in gzf, then parse */ if (bu == bs) { tmp = realloc(buf, bs + (1024 * 1024) + 1); if (tmp == NULL) { serrno = errno; free(buf); buf = NULL; bs = 0; goto bldone; } bs += 1024 * 1024; buf = tmp; } n = gzread(gzf, buf + bu, bs - bu); if (n == 0) goto parse; else if (n == -1) { serrno = errno; goto bldone; } else bu += n; } parse: start = 0; /* we assume that there is an IP for every 14 bytes */ if (*blc + bu / 7 >= *bls) { *bls += bu / 7; blt = reallocarray(bl, *bls, sizeof(struct bl)); if (blt == NULL) { *bls -= bu / 7; serrno = errno; goto bldone; } bl = blt; } for (i = 0; i <= bu; i++) { if (*blc + 1 >= *bls) { *bls += 1024; blt = reallocarray(bl, *bls, sizeof(struct bl)); if (blt == NULL) { *bls -= 1024; serrno = errno; goto bldone; } bl = blt; } if (i == bu || buf[i] == '\n') { buf[i] = '\0'; if (parse_netblock(buf + start, bl + *blc, bl + *blc + 1, white)) *blc += 2; start = i + 1; } } if (bu == 0) errno = EIO; bldone: free(buf); if (serrno) errno = serrno; return (bl); } int cmpbl(const void *a, const void *b) { if (((struct bl *)a)->addr > ((struct bl *) b)->addr) return (1); if (((struct bl *)a)->addr < ((struct bl *) b)->addr) return (-1); return (0); } /* * collapse_blacklist takes blacklist/whitelist entries sorts, removes * overlaps and whitelist portions, and returns netblocks to blacklist * as lists of nonoverlapping cidr blocks suitable for feeding in * printable form to pfctl or spamd. */ struct cidr * collapse_blacklist(struct bl *bl, size_t blc, u_int *clc) { int bs = 0, ws = 0, state=0; u_int cli, cls, i; u_int32_t bstart = 0; struct cidr *cl; int laststate; u_int32_t addr; if (blc == 0) return (NULL); /* * Overallocate by 10% to avoid excessive realloc due to white * entries splitting up CIDR blocks. */ cli = 0; cls = (blc / 2) + (blc / 20) + 1; cl = reallocarray(NULL, cls, sizeof(struct cidr)); if (cl == NULL) return (NULL); qsort(bl, blc, sizeof(struct bl), cmpbl); for (i = 0; i < blc;) { laststate = state; addr = bl[i].addr; do { bs += bl[i].b; ws += bl[i].w; i++; } while (bl[i].addr == addr); if (state == 1 && bs == 0) state = 0; else if (state == 0 && bs > 0) state = 1; if (ws > 0) state = 0; if (laststate == 0 && state == 1) { /* start blacklist */ bstart = addr; } if (laststate == 1 && state == 0) { /* end blacklist */ cl = range2cidrlist(cl, &cli, &cls, bstart, addr - 1); } laststate = state; } cl[cli].addr = 0; *clc = cli; return (cl); } int configure_spamd(u_short dport, char *name, char *message, struct cidr *blacklists, u_int count) { int lport = IPPORT_RESERVED - 1, s; struct sockaddr_in sin; FILE* sdc; s = rresvport(&lport); if (s == -1) return (-1); memset(&sin, 0, sizeof sin); sin.sin_len = sizeof(sin); sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); sin.sin_family = AF_INET; sin.sin_port = htons(dport); if (connect(s, (struct sockaddr *)&sin, sizeof sin) == -1) return (-1); sdc = fdopen(s, "w"); if (sdc == NULL) { close(s); return (-1); } fputs(name, sdc); do_message(sdc, message); fprintf(sdc, ";inet;%u", count); while (blacklists->addr != 0) { fprintf(sdc, ";%s/%u", atop(blacklists->addr), blacklists->bits); blacklists++; } fputc('\n', sdc); fclose(sdc); close(s); return (0); } int configure_pf(struct cidr *blacklists) { char *argv[9]= {"pfctl", "-q", "-t", "spamd", "-T", "replace", "-f" "-", NULL}; static FILE *pf = NULL; int pdes[2]; if (pf == NULL) { if (pipe(pdes) != 0) return (-1); switch (fork()) { case -1: close(pdes[0]); close(pdes[1]); return (-1); case 0: /* child */ close(pdes[1]); if (pdes[0] != STDIN_FILENO) { dup2(pdes[0], STDIN_FILENO); close(pdes[0]); } closefrom(STDERR_FILENO + 1); execvp(PATH_PFCTL, argv); _exit(1); } /* parent */ close(pdes[0]); pf = fdopen(pdes[1], "w"); if (pf == NULL) { close(pdes[1]); return (-1); } } while (blacklists->addr != 0) { fprintf(pf, "%s/%u\n", atop(blacklists->addr), blacklists->bits); blacklists++; } return (0); } int getlist(char ** db_array, char *name, struct blacklist *blist, struct blacklist *blistnew) { char *buf, *method, *file, *message; int fd, black = 0, serror; size_t blc, bls; struct bl *bl = NULL; gzFile gzf; if (cgetent(&buf, db_array, name) != 0) err(1, "Can't find \"%s\" in spamd config", name); buf = fix_quoted_colons(buf); if (cgetcap(buf, "black", ':') != NULL) { /* use new list */ black = 1; blc = blistnew->blc; bls = blistnew->bls; bl = blistnew->bl; } else if (cgetcap(buf, "white", ':') != NULL) { /* apply to most recent blacklist */ black = 0; blc = blist->blc; bls = blist->bls; bl = blist->bl; } else errx(1, "Must have \"black\" or \"white\" in %s", name); switch (cgetstr(buf, "msg", &message)) { case -1: if (black) errx(1, "No msg for blacklist \"%s\"", name); break; case -2: err(1, NULL); } switch (cgetstr(buf, "method", &method)) { case -1: method = NULL; break; case -2: err(1, NULL); } switch (cgetstr(buf, "file", &file)) { case -1: errx(1, "No file given for %slist %s", black ? "black" : "white", name); case -2: err(1, NULL); default: fd = open_file(method, file); if (fd == -1) err(1, "Can't open %s by %s method", file, method ? method : "file"); free(method); free(file); gzf = gzdopen(fd, "r"); if (gzf == NULL) errx(1, "gzdopen"); } free(buf); bl = add_blacklist(bl, &blc, &bls, gzf, !black); serror = errno; gzclose(gzf); if (bl == NULL) { errno = serror; warn("Could not add %slist %s", black ? "black" : "white", name); return (0); } if (black) { if (debug) fprintf(stderr, "blacklist %s %zu entries\n", name, blc / 2); blistnew->message = message; blistnew->name = name; blistnew->black = black; blistnew->bl = bl; blistnew->blc = blc; blistnew->bls = bls; } else { /* whitelist applied to last active blacklist */ if (debug) fprintf(stderr, "whitelist %s %zu entries\n", name, (blc - blist->blc) / 2); blist->bl = bl; blist->blc = blc; blist->bls = bls; } return (black); } void send_blacklist(struct blacklist *blist, in_port_t port) { struct cidr *cidrs; u_int clc; if (blist->blc > 0) { cidrs = collapse_blacklist(blist->bl, blist->blc, &clc); if (cidrs == NULL) err(1, NULL); if (!dryrun) { if (configure_spamd(port, blist->name, blist->message, cidrs, clc) == -1) err(1, "Can't connect to spamd on port %d", port); if (!greyonly && configure_pf(cidrs) == -1) err(1, "pfctl failed"); } free(cidrs); free(blist->bl); } } __dead void usage(void) { fprintf(stderr, "usage: %s [-bDdn]\n", __progname); exit(1); } int main(int argc, char *argv[]) { size_t blc, bls, black, white; char *db_array[2], *buf, *name; struct blacklist *blists; struct servent *ent; int daemonize = 0, ch; struct passwd *pw; while ((ch = getopt(argc, argv, "bdDn")) != -1) { switch (ch) { case 'n': dryrun = 1; break; case 'd': debug = 1; break; case 'b': greyonly = 0; break; case 'D': daemonize = 1; break; default: usage(); break; } } argc -= optind; argv += optind; if (argc != 0) usage(); if ((pw = getpwnam(SPAMD_USER)) == NULL) errx(1, "cannot find user %s", SPAMD_USER); spamd_uid = pw->pw_uid; spamd_gid = pw->pw_gid; if (pledge("stdio rpath inet proc exec id", NULL) == -1) err(1, "pledge"); if (daemonize) daemon(0, 0); else if (chdir("/") != 0) err(1, "chdir(\"/\")"); if ((ent = getservbyname("spamd-cfg", "tcp")) == NULL) errx(1, "cannot find service \"spamd-cfg\" in /etc/services"); ent->s_port = ntohs(ent->s_port); db_array[0] = PATH_SPAMD_CONF; db_array[1] = NULL; if (cgetent(&buf, db_array, "all") != 0) err(1, "Can't find \"all\" in spamd config"); name = strsep(&buf, ": \t"); /* skip "all" at start */ blists = NULL; blc = bls = 0; while ((name = strsep(&buf, ": \t")) != NULL) { if (*name) { /* extract config in order specified in "all" tag */ if (blc == bls) { struct blacklist *tmp; bls += 32; tmp = reallocarray(blists, bls, sizeof(struct blacklist)); if (tmp == NULL) err(1, NULL); blists = tmp; } if (blc == 0) black = white = 0; else { white = blc - 1; black = blc; } memset(&blists[black], 0, sizeof(struct blacklist)); black = getlist(db_array, name, &blists[white], &blists[black]); if (black && blc > 0) { /* collapse and free previous blacklist */ send_blacklist(&blists[blc - 1], ent->s_port); } blc += black; } } /* collapse and free last blacklist */ if (blc > 0) send_blacklist(&blists[blc - 1], ent->s_port); return (0); }