1/*	$OpenBSD: spamd-setup.c,v 1.50 2017/07/07 00:10:15 djm Exp $ */
2
3/*
4 * Copyright (c) 2003 Bob Beck.  All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include <arpa/inet.h>
28#include <sys/socket.h>
29#include <sys/types.h>
30
31#include <err.h>
32#include <errno.h>
33#include <fcntl.h>
34#include <netdb.h>
35#include <pwd.h>
36#include <stdio.h>
37#include <stdlib.h>
38#include <string.h>
39#include <unistd.h>
40#include <zlib.h>
41
42#define PATH_FTP		"/usr/bin/ftp"
43#define PATH_PFCTL		"/sbin/pfctl"
44#define PATH_SPAMD_CONF		"/etc/mail/spamd.conf"
45#define SPAMD_ARG_MAX		256 /* max # of args to an exec */
46#define SPAMD_USER		"_spamd"
47
48struct cidr {
49	u_int32_t addr;
50	u_int8_t bits;
51};
52
53struct bl {
54	u_int32_t addr;
55	int8_t b;
56	int8_t w;
57};
58
59struct blacklist {
60	char *name;
61	char *message;
62	struct bl *bl;
63	size_t blc, bls;
64	u_int8_t black;
65};
66
67u_int32_t	 imask(u_int8_t);
68u_int8_t	 maxblock(u_int32_t, u_int8_t);
69u_int8_t	 maxdiff(u_int32_t, u_int32_t);
70struct cidr	*range2cidrlist(struct cidr *, u_int *, u_int *, u_int32_t,
71		     u_int32_t);
72void		 cidr2range(struct cidr, u_int32_t *, u_int32_t *);
73char		*atop(u_int32_t);
74int		 parse_netblock(char *, struct bl *, struct bl *, int);
75int		 open_child(char *, char **, int);
76int		 fileget(char *);
77int		 open_file(char *, char *);
78char		*fix_quoted_colons(char *);
79void		 do_message(FILE *, char *);
80struct bl	*add_blacklist(struct bl *, size_t *, size_t *, gzFile, int);
81int		 cmpbl(const void *, const void *);
82struct cidr	*collapse_blacklist(struct bl *, size_t, u_int *);
83int		 configure_spamd(u_short, char *, char *, struct cidr *, u_int);
84int		 configure_pf(struct cidr *);
85int		 getlist(char **, char *, struct blacklist *, struct blacklist *);
86__dead void	 usage(void);
87
88uid_t		  spamd_uid;
89gid_t		  spamd_gid;
90int		  debug;
91int		  dryrun;
92int		  greyonly = 1;
93
94extern char 	 *__progname;
95
96#define MAXIMUM(a,b) (((a)>(b))?(a):(b))
97
98u_int32_t
99imask(u_int8_t b)
100{
101	if (b == 0)
102		return (0);
103	return (0xffffffffU << (32 - b));
104}
105
106u_int8_t
107maxblock(u_int32_t addr, u_int8_t bits)
108{
109	u_int32_t m;
110
111	while (bits > 0) {
112		m = imask(bits - 1);
113
114		if ((addr & m) != addr)
115			return (bits);
116		bits--;
117	}
118	return (bits);
119}
120
121u_int8_t
122maxdiff(u_int32_t a, u_int32_t b)
123{
124	u_int8_t bits = 0;
125	u_int32_t m;
126
127	b++;
128	while (bits < 32) {
129		m = imask(bits);
130
131		if ((a & m) != (b & m))
132			return (bits);
133		bits++;
134	}
135	return (bits);
136}
137
138struct cidr *
139range2cidrlist(struct cidr *list, u_int *cli, u_int *cls, u_int32_t start,
140    u_int32_t end)
141{
142	u_int8_t maxsize, diff;
143	struct cidr *tmp;
144
145	while (end >= start) {
146		maxsize = maxblock(start, 32);
147		diff = maxdiff(start, end);
148
149		maxsize = MAXIMUM(maxsize, diff);
150		if (*cls <= *cli + 1) {		/* one extra for terminator */
151			tmp = reallocarray(list, *cls + 32,
152			    sizeof(struct cidr));
153			if (tmp == NULL)
154				err(1, NULL);
155			list = tmp;
156			*cls += 32;
157		}
158		list[*cli].addr = start;
159		list[*cli].bits = maxsize;
160		(*cli)++;
161		start = start + (1 << (32 - maxsize));
162	}
163	return (list);
164}
165
166void
167cidr2range(struct cidr cidr, u_int32_t *start, u_int32_t *end)
168{
169	*start = cidr.addr;
170	*end = cidr.addr + (1 << (32 - cidr.bits)) - 1;
171}
172
173char *
174atop(u_int32_t addr)
175{
176	struct in_addr in;
177
178	memset(&in, 0, sizeof(in));
179	in.s_addr = htonl(addr);
180	return (inet_ntoa(in));
181}
182
183int
184parse_netblock(char *buf, struct bl *start, struct bl *end, int white)
185{
186	char astring[16], astring2[16];
187	unsigned maskbits;
188	struct cidr c;
189
190	/* skip leading spaces */
191	while (*buf == ' ')
192		buf++;
193	/* bail if it's a comment */
194	if (*buf == '#')
195		return (0);
196	/* otherwise, look for a netblock of some sort */
197	if (sscanf(buf, "%15[^/]/%u", astring, &maskbits) == 2) {
198		/* looks like a cidr */
199		memset(&c.addr, 0, sizeof(c.addr));
200		if (inet_net_pton(AF_INET, astring, &c.addr, sizeof(c.addr))
201		    == -1)
202			return (0);
203		c.addr = ntohl(c.addr);
204		if (maskbits > 32)
205			return (0);
206		c.bits = maskbits;
207		cidr2range(c, &start->addr, &end->addr);
208		end->addr += 1;
209	} else if (sscanf(buf, "%15[0123456789.]%*[ -]%15[0123456789.]",
210	    astring, astring2) == 2) {
211		/* looks like start - end */
212		memset(&start->addr, 0, sizeof(start->addr));
213		memset(&end->addr, 0, sizeof(end->addr));
214		if (inet_net_pton(AF_INET, astring, &start->addr,
215		    sizeof(start->addr)) == -1)
216			return (0);
217		start->addr = ntohl(start->addr);
218		if (inet_net_pton(AF_INET, astring2, &end->addr,
219		    sizeof(end->addr)) == -1)
220			return (0);
221		end->addr = ntohl(end->addr) + 1;
222		if (start > end)
223			return (0);
224	} else if (sscanf(buf, "%15[0123456789.]", astring) == 1) {
225		/* just a single address */
226		memset(&start->addr, 0, sizeof(start->addr));
227		if (inet_net_pton(AF_INET, astring, &start->addr,
228		    sizeof(start->addr)) == -1)
229			return (0);
230		start->addr = ntohl(start->addr);
231		end->addr = start->addr + 1;
232	} else
233		return (0);
234
235	if (white) {
236		start->b = 0;
237		start->w = 1;
238		end->b = 0;
239		end->w = -1;
240	} else {
241		start->b = 1;
242		start->w = 0;
243		end->b = -1;
244		end->w = 0;
245	}
246	return (1);
247}
248
249void
250drop_privileges(void)
251{
252	if (setgroups(1, &spamd_gid) != 0)
253		err(1, "setgroups %ld", (long)spamd_gid);
254	if (setresgid(spamd_gid, spamd_gid, spamd_gid) != 0)
255		err(1, "setresgid %ld", (long)spamd_gid);
256	if (setresuid(spamd_uid, spamd_uid, spamd_uid) != 0)
257		err(1, "setresuid %ld", (long)spamd_uid);
258}
259
260int
261open_child(char *file, char **argv, int drop_privs)
262{
263	int pdes[2];
264
265	if (pipe(pdes) != 0)
266		return (-1);
267	switch (fork()) {
268	case -1:
269		close(pdes[0]);
270		close(pdes[1]);
271		return (-1);
272	case 0:
273		/* child */
274		close(pdes[0]);
275		if (pdes[1] != STDOUT_FILENO) {
276			dup2(pdes[1], STDOUT_FILENO);
277			close(pdes[1]);
278		}
279		if (drop_privs)
280			drop_privileges();
281		closefrom(STDERR_FILENO + 1);
282		execvp(file, argv);
283		_exit(1);
284	}
285
286	/* parent */
287	close(pdes[1]);
288	return (pdes[0]);
289}
290
291int
292fileget(char *url)
293{
294	char *argv[6];
295
296	argv[0] = "ftp";
297	argv[1] = "-V";
298	argv[2] = "-o";
299	argv[3] = "-";
300	argv[4] = url;
301	argv[5] = NULL;
302
303	if (debug)
304		fprintf(stderr, "Getting %s\n", url);
305
306	return (open_child(PATH_FTP, argv, 1));
307}
308
309int
310open_file(char *method, char *file)
311{
312	char *url;
313	char **ap, **argv;
314	int len, i, oerrno;
315
316	if ((method == NULL) || (strcmp(method, "file") == 0))
317		return (open(file, O_RDONLY));
318	if (strcmp(method, "http") == 0 || strcmp(method, "https") == 0 ||
319	    strcmp(method, "ftp") == 0) {
320		if (asprintf(&url, "%s://%s", method, file) == -1)
321			return (-1);
322		i = fileget(url);
323		free(url);
324		return (i);
325	} else if (strcmp(method, "exec") == 0) {
326		len = strlen(file);
327		argv = calloc(len, sizeof(char *));
328		if (argv == NULL)
329			return (-1);
330		for (ap = argv; ap < &argv[len - 1] &&
331		    (*ap = strsep(&file, " \t")) != NULL;) {
332			if (**ap != '\0')
333				ap++;
334		}
335		*ap = NULL;
336		i = open_child(argv[0], argv, 0);
337		oerrno = errno;
338		free(argv);
339		errno = oerrno;
340		return (i);
341	}
342	errx(1, "Unknown method %s", method);
343	return (-1); /* NOTREACHED */
344}
345
346/*
347 * fix_quoted_colons walks through a buffer returned by cgetent.  We
348 * look for quoted strings, to escape colons (:) in quoted strings for
349 * getcap by replacing them with \C so cgetstr() deals with it correctly
350 * without having to see the \C bletchery in a configuration file that
351 * needs to have urls in it. Frees the buffer passed to it, passes back
352 * another larger one, with can be used with cgetxxx(), like the original
353 * buffer, it must be freed by the caller.
354 * This should really be a temporary fix until there is a sanctioned
355 * way to make getcap(3) handle quoted strings like this in a nicer
356 * way.
357 */
358char *
359fix_quoted_colons(char *buf)
360{
361	int in = 0;
362	size_t i, j = 0;
363	char *newbuf, last;
364
365	/* Allocate enough space for a buf of all colons (impossible). */
366	newbuf = malloc(2 * strlen(buf) + 1);
367	if (newbuf == NULL)
368		return (NULL);
369	last = '\0';
370	for (i = 0; i < strlen(buf); i++) {
371		switch (buf[i]) {
372		case ':':
373			if (in) {
374				newbuf[j++] = '\\';
375				newbuf[j++] = 'C';
376			} else
377				newbuf[j++] = buf[i];
378			break;
379		case '"':
380			if (last != '\\')
381				in = !in;
382			newbuf[j++] = buf[i];
383			break;
384		default:
385			newbuf[j++] = buf[i];
386		}
387		last = buf[i];
388	}
389	free(buf);
390	newbuf[j] = '\0';
391	return (newbuf);
392}
393
394void
395do_message(FILE *sdc, char *msg)
396{
397	size_t i, bs = 0, bu = 0, len;
398	ssize_t n;
399	char *buf = NULL, last, *tmp;
400	int fd;
401
402	len = strlen(msg);
403	if (msg[0] == '"' && msg[len - 1] == '"') {
404		/* quoted msg, escape newlines and send it out */
405		msg[len - 1] = '\0';
406		buf = msg + 1;
407		bu = len - 2;
408		goto sendit;
409	} else {
410		/*
411		 * message isn't quoted - try to open a local
412		 * file and read the message from it.
413		 */
414		fd = open(msg, O_RDONLY);
415		if (fd == -1)
416			err(1, "Can't open message from %s", msg);
417		for (;;) {
418			if (bu == bs) {
419				tmp = realloc(buf, bs + 8192);
420				if (tmp == NULL)
421					err(1, NULL);
422				bs += 8192;
423				buf = tmp;
424			}
425
426			n = read(fd, buf + bu, bs - bu);
427			if (n == 0) {
428				goto sendit;
429			} else if (n == -1) {
430				err(1, "Can't read from %s", msg);
431			} else
432				bu += n;
433		}
434		buf[bu]='\0';
435	}
436 sendit:
437	fprintf(sdc, ";\"");
438	last = '\0';
439	for (i = 0; i < bu; i++) {
440		/* handle escaping the things spamd wants */
441		switch (buf[i]) {
442		case 'n':
443			if (last == '\\')
444				fprintf(sdc, "\\\\n");
445			else
446				fputc('n', sdc);
447			last = '\0';
448			break;
449		case '\n':
450			fprintf(sdc, "\\n");
451			last = '\0';
452			break;
453		case '"':
454			fputc('\\', sdc);
455			/* FALLTHROUGH */
456		default:
457			fputc(buf[i], sdc);
458			last = '\0';
459		}
460	}
461	fputc('"', sdc);
462	if (bs != 0)
463		free(buf);
464}
465
466/* retrieve a list from fd. add to blacklist bl */
467struct bl *
468add_blacklist(struct bl *bl, size_t *blc, size_t *bls, gzFile gzf, int white)
469{
470	int i, n, start, bu = 0, bs = 0, serrno = 0;
471	char *buf = NULL, *tmp;
472	struct bl *blt;
473
474	for (;;) {
475		/* read in gzf, then parse */
476		if (bu == bs) {
477			tmp = realloc(buf, bs + (1024 * 1024) + 1);
478			if (tmp == NULL) {
479				serrno = errno;
480				free(buf);
481				buf = NULL;
482				bs = 0;
483				goto bldone;
484			}
485			bs += 1024 * 1024;
486			buf = tmp;
487		}
488
489		n = gzread(gzf, buf + bu, bs - bu);
490		if (n == 0)
491			goto parse;
492		else if (n == -1) {
493			serrno = errno;
494			goto bldone;
495		} else
496			bu += n;
497	}
498 parse:
499	start = 0;
500	/* we assume that there is an IP for every 14 bytes */
501	if (*blc + bu / 7 >= *bls) {
502		*bls += bu / 7;
503		blt = reallocarray(bl, *bls, sizeof(struct bl));
504		if (blt == NULL) {
505			*bls -= bu / 7;
506			serrno = errno;
507			goto bldone;
508		}
509		bl = blt;
510	}
511	for (i = 0; i <= bu; i++) {
512		if (*blc + 1 >= *bls) {
513			*bls += 1024;
514			blt = reallocarray(bl, *bls, sizeof(struct bl));
515			if (blt == NULL) {
516				*bls -= 1024;
517				serrno = errno;
518				goto bldone;
519			}
520			bl = blt;
521		}
522		if (i == bu || buf[i] == '\n') {
523			buf[i] = '\0';
524			if (parse_netblock(buf + start,
525			    bl + *blc, bl + *blc + 1, white))
526				*blc += 2;
527			start = i + 1;
528		}
529	}
530	if (bu == 0)
531		errno = EIO;
532 bldone:
533	free(buf);
534	if (serrno)
535		errno = serrno;
536	return (bl);
537}
538
539int
540cmpbl(const void *a, const void *b)
541{
542	if (((struct bl *)a)->addr > ((struct bl *) b)->addr)
543		return (1);
544	if (((struct bl *)a)->addr < ((struct bl *) b)->addr)
545		return (-1);
546	return (0);
547}
548
549/*
550 * collapse_blacklist takes blacklist/whitelist entries sorts, removes
551 * overlaps and whitelist portions, and returns netblocks to blacklist
552 * as lists of nonoverlapping cidr blocks suitable for feeding in
553 * printable form to pfctl or spamd.
554 */
555struct cidr *
556collapse_blacklist(struct bl *bl, size_t blc, u_int *clc)
557{
558	int bs = 0, ws = 0, state=0;
559	u_int cli, cls, i;
560	u_int32_t bstart = 0;
561	struct cidr *cl;
562	int laststate;
563	u_int32_t addr;
564
565	if (blc == 0)
566		return (NULL);
567
568	/*
569	 * Overallocate by 10% to avoid excessive realloc due to white
570	 * entries splitting up CIDR blocks.
571	 */
572	cli = 0;
573	cls = (blc / 2) + (blc / 20) + 1;
574	cl = reallocarray(NULL, cls, sizeof(struct cidr));
575	if (cl == NULL)
576		return (NULL);
577	qsort(bl, blc, sizeof(struct bl), cmpbl);
578	for (i = 0; i < blc;) {
579		laststate = state;
580		addr = bl[i].addr;
581
582		do {
583			bs += bl[i].b;
584			ws += bl[i].w;
585			i++;
586		} while (bl[i].addr == addr);
587		if (state == 1 && bs == 0)
588			state = 0;
589		else if (state == 0 && bs > 0)
590			state = 1;
591		if (ws > 0)
592			state = 0;
593		if (laststate == 0 && state == 1) {
594			/* start blacklist */
595			bstart = addr;
596		}
597		if (laststate == 1 && state == 0) {
598			/* end blacklist */
599			cl = range2cidrlist(cl, &cli, &cls, bstart, addr - 1);
600		}
601		laststate = state;
602	}
603	cl[cli].addr = 0;
604	*clc = cli;
605	return (cl);
606}
607
608int
609configure_spamd(u_short dport, char *name, char *message,
610    struct cidr *blacklists, u_int count)
611{
612	int lport = IPPORT_RESERVED - 1, s;
613	struct sockaddr_in sin;
614	FILE* sdc;
615
616	s = rresvport(&lport);
617	if (s == -1)
618		return (-1);
619	memset(&sin, 0, sizeof sin);
620	sin.sin_len = sizeof(sin);
621	sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
622	sin.sin_family = AF_INET;
623	sin.sin_port = htons(dport);
624	if (connect(s, (struct sockaddr *)&sin, sizeof sin) == -1)
625		return (-1);
626	sdc = fdopen(s, "w");
627	if (sdc == NULL) {
628		close(s);
629		return (-1);
630	}
631	fputs(name, sdc);
632	do_message(sdc, message);
633	fprintf(sdc, ";inet;%u", count);
634	while (blacklists->addr != 0) {
635		fprintf(sdc, ";%s/%u", atop(blacklists->addr),
636		    blacklists->bits);
637		blacklists++;
638	}
639	fputc('\n', sdc);
640	fclose(sdc);
641	close(s);
642	return (0);
643}
644
645
646int
647configure_pf(struct cidr *blacklists)
648{
649	char *argv[9]= {"pfctl", "-q", "-t", "spamd", "-T", "replace",
650	    "-f" "-", NULL};
651	static FILE *pf = NULL;
652	int pdes[2];
653
654	if (pf == NULL) {
655		if (pipe(pdes) != 0)
656			return (-1);
657		switch (fork()) {
658		case -1:
659			close(pdes[0]);
660			close(pdes[1]);
661			return (-1);
662		case 0:
663			/* child */
664			close(pdes[1]);
665			if (pdes[0] != STDIN_FILENO) {
666				dup2(pdes[0], STDIN_FILENO);
667				close(pdes[0]);
668			}
669			closefrom(STDERR_FILENO + 1);
670			execvp(PATH_PFCTL, argv);
671			_exit(1);
672		}
673
674		/* parent */
675		close(pdes[0]);
676		pf = fdopen(pdes[1], "w");
677		if (pf == NULL) {
678			close(pdes[1]);
679			return (-1);
680		}
681	}
682	while (blacklists->addr != 0) {
683		fprintf(pf, "%s/%u\n", atop(blacklists->addr),
684		    blacklists->bits);
685		blacklists++;
686	}
687	return (0);
688}
689
690int
691getlist(char ** db_array, char *name, struct blacklist *blist,
692    struct blacklist *blistnew)
693{
694	char *buf, *method, *file, *message;
695	int fd, black = 0, serror;
696	size_t blc, bls;
697	struct bl *bl = NULL;
698	gzFile gzf;
699
700	if (cgetent(&buf, db_array, name) != 0)
701		err(1, "Can't find \"%s\" in spamd config", name);
702	buf = fix_quoted_colons(buf);
703	if (cgetcap(buf, "black", ':') != NULL) {
704		/* use new list */
705		black = 1;
706		blc = blistnew->blc;
707		bls = blistnew->bls;
708		bl = blistnew->bl;
709	} else if (cgetcap(buf, "white", ':') != NULL) {
710		/* apply to most recent blacklist */
711		black = 0;
712		blc = blist->blc;
713		bls = blist->bls;
714		bl = blist->bl;
715	} else
716		errx(1, "Must have \"black\" or \"white\" in %s", name);
717
718	switch (cgetstr(buf, "msg", &message)) {
719	case -1:
720		if (black)
721			errx(1, "No msg for blacklist \"%s\"", name);
722		break;
723	case -2:
724		err(1, NULL);
725	}
726
727	switch (cgetstr(buf, "method", &method)) {
728	case -1:
729		method = NULL;
730		break;
731	case -2:
732		err(1, NULL);
733	}
734
735	switch (cgetstr(buf, "file", &file)) {
736	case -1:
737		errx(1, "No file given for %slist %s",
738		    black ? "black" : "white", name);
739	case -2:
740		err(1, NULL);
741	default:
742		fd = open_file(method, file);
743		if (fd == -1)
744			err(1, "Can't open %s by %s method",
745			    file, method ? method : "file");
746		free(method);
747		free(file);
748		gzf = gzdopen(fd, "r");
749		if (gzf == NULL)
750			errx(1, "gzdopen");
751	}
752	free(buf);
753	bl = add_blacklist(bl, &blc, &bls, gzf, !black);
754	serror = errno;
755	gzclose(gzf);
756	if (bl == NULL) {
757		errno = serror;
758		warn("Could not add %slist %s", black ? "black" : "white",
759		    name);
760		return (0);
761	}
762	if (black) {
763		if (debug)
764			fprintf(stderr, "blacklist %s %zu entries\n",
765			    name, blc / 2);
766		blistnew->message = message;
767		blistnew->name = name;
768		blistnew->black = black;
769		blistnew->bl = bl;
770		blistnew->blc = blc;
771		blistnew->bls = bls;
772	} else {
773		/* whitelist applied to last active blacklist */
774		if (debug)
775			fprintf(stderr, "whitelist %s %zu entries\n",
776			    name, (blc - blist->blc) / 2);
777		blist->bl = bl;
778		blist->blc = blc;
779		blist->bls = bls;
780	}
781	return (black);
782}
783
784void
785send_blacklist(struct blacklist *blist, in_port_t port)
786{
787	struct cidr *cidrs;
788	u_int clc;
789
790	if (blist->blc > 0) {
791		cidrs = collapse_blacklist(blist->bl, blist->blc, &clc);
792		if (cidrs == NULL)
793			err(1, NULL);
794		if (!dryrun) {
795			if (configure_spamd(port, blist->name,
796			    blist->message, cidrs, clc) == -1)
797				err(1, "Can't connect to spamd on port %d",
798				    port);
799			if (!greyonly && configure_pf(cidrs) == -1)
800				err(1, "pfctl failed");
801		}
802		free(cidrs);
803		free(blist->bl);
804	}
805}
806
807__dead void
808usage(void)
809{
810
811	fprintf(stderr, "usage: %s [-bDdn]\n", __progname);
812	exit(1);
813}
814
815int
816main(int argc, char *argv[])
817{
818	size_t blc, bls, black, white;
819	char *db_array[2], *buf, *name;
820	struct blacklist *blists;
821	struct servent *ent;
822	int daemonize = 0, ch;
823	struct passwd *pw;
824
825	while ((ch = getopt(argc, argv, "bdDn")) != -1) {
826		switch (ch) {
827		case 'n':
828			dryrun = 1;
829			break;
830		case 'd':
831			debug = 1;
832			break;
833		case 'b':
834			greyonly = 0;
835			break;
836		case 'D':
837			daemonize = 1;
838			break;
839		default:
840			usage();
841			break;
842		}
843	}
844	argc -= optind;
845	argv += optind;
846	if (argc != 0)
847		usage();
848
849	if ((pw = getpwnam(SPAMD_USER)) == NULL)
850		errx(1, "cannot find user %s", SPAMD_USER);
851	spamd_uid = pw->pw_uid;
852	spamd_gid = pw->pw_gid;
853
854	if (pledge("stdio rpath inet proc exec id", NULL) == -1)
855		err(1, "pledge");
856
857	if (daemonize)
858		daemon(0, 0);
859	else if (chdir("/") != 0)
860		err(1, "chdir(\"/\")");
861
862	if ((ent = getservbyname("spamd-cfg", "tcp")) == NULL)
863		errx(1, "cannot find service \"spamd-cfg\" in /etc/services");
864	ent->s_port = ntohs(ent->s_port);
865
866	db_array[0] = PATH_SPAMD_CONF;
867	db_array[1] = NULL;
868
869	if (cgetent(&buf, db_array, "all") != 0)
870		err(1, "Can't find \"all\" in spamd config");
871	name = strsep(&buf, ": \t"); /* skip "all" at start */
872	blists = NULL;
873	blc = bls = 0;
874	while ((name = strsep(&buf, ": \t")) != NULL) {
875		if (*name) {
876			/* extract config in order specified in "all" tag */
877			if (blc == bls) {
878				struct blacklist *tmp;
879
880				bls += 32;
881				tmp = reallocarray(blists, bls,
882				    sizeof(struct blacklist));
883				if (tmp == NULL)
884					err(1, NULL);
885				blists = tmp;
886			}
887			if (blc == 0)
888				black = white = 0;
889			else {
890				white = blc - 1;
891				black = blc;
892			}
893			memset(&blists[black], 0, sizeof(struct blacklist));
894			black = getlist(db_array, name, &blists[white],
895			    &blists[black]);
896			if (black && blc > 0) {
897				/* collapse and free previous blacklist */
898				send_blacklist(&blists[blc - 1], ent->s_port);
899			}
900			blc += black;
901		}
902	}
903	/* collapse and free last blacklist */
904	if (blc > 0)
905		send_blacklist(&blists[blc - 1], ent->s_port);
906	return (0);
907}
908