1 /* $OpenBSD: spamlogd.c,v 1.32 2021/07/12 15:09:18 beck Exp $ */
2
3 /*
4 * Copyright (c) 2006 Henning Brauer <henning@openbsd.org>
5 * Copyright (c) 2006 Berk D. Demir.
6 * Copyright (c) 2004-2007 Bob Beck.
7 * Copyright (c) 2001 Theo de Raadt.
8 * Copyright (c) 2001 Can Erkin Acar.
9 * All rights reserved
10 *
11 * Permission to use, copy, modify, and distribute this software for any
12 * purpose with or without fee is hereby granted, provided that the above
13 * copyright notice and this permission notice appear in all copies.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
16 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
17 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
18 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
20 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
21 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22 */
23
24 /* watch pf log for mail connections, update whitelist entries. */
25
26 #include <sys/types.h>
27 #include <sys/socket.h>
28 #include <sys/ioctl.h>
29 #include <sys/signal.h>
30
31 #include <net/if.h>
32
33 #include <netinet/in.h>
34 #include <netinet/ip.h>
35 #include <arpa/inet.h>
36
37 #include <net/pfvar.h>
38 #include <net/if_pflog.h>
39
40 #include <db.h>
41 #include <err.h>
42 #include <errno.h>
43 #include <fcntl.h>
44 #include <netdb.h>
45 #include <pwd.h>
46 #include <stdio.h>
47 #include <stdarg.h>
48 #include <stdlib.h>
49 #include <syslog.h>
50 #include <string.h>
51 #include <unistd.h>
52 #include <pcap-int.h>
53 #include <pcap.h>
54
55 #include "grey.h"
56 #include "sync.h"
57
58 #define MIN_PFLOG_HDRLEN 45
59 #define PCAPSNAP 512
60 #define PCAPTIMO 500 /* ms */
61 #define PCAPOPTZ 1 /* optimize filter */
62 #define PCAPFSIZ 512 /* pcap filter string size */
63
64 #define SPAMD_USER "_spamd"
65
66 int debug = 1;
67 int greylist = 1;
68 FILE *grey = NULL;
69
70 u_short sync_port;
71 int syncsend;
72 u_int8_t flag_debug = 0;
73 u_int8_t flag_inbound = 0;
74 char *networkif = NULL;
75 char *pflogif = "pflog0";
76 char errbuf[PCAP_ERRBUF_SIZE];
77 pcap_t *hpcap = NULL;
78 struct syslog_data sdata = SYSLOG_DATA_INIT;
79 time_t whiteexp = WHITEEXP;
80 extern char *__progname;
81
82 void logmsg(int , const char *, ...);
83 void sighandler_close(int);
84 int init_pcap(void);
85 void logpkt_handler(u_char *, const struct pcap_pkthdr *, const u_char *);
86 int dbupdate(char *, char *);
87 __dead void usage(void);
88
89 void
logmsg(int pri,const char * msg,...)90 logmsg(int pri, const char *msg, ...)
91 {
92 va_list ap;
93 va_start(ap, msg);
94
95 if (flag_debug) {
96 vfprintf(stderr, msg, ap);
97 fprintf(stderr, "\n");
98 } else
99 vsyslog_r(pri, &sdata, msg, ap);
100
101 va_end(ap);
102 }
103
104 void
sighandler_close(int signal)105 sighandler_close(int signal)
106 {
107 if (hpcap != NULL)
108 pcap_breakloop(hpcap); /* sighdlr safe */
109 }
110
111 pcap_t *
pflog_read_live(const char * source,int slen,int promisc,int to_ms,char * ebuf)112 pflog_read_live(const char *source, int slen, int promisc, int to_ms,
113 char *ebuf)
114 {
115 int fd;
116 struct bpf_version bv;
117 struct ifreq ifr;
118 u_int v, dlt = DLT_PFLOG;
119 pcap_t *p;
120
121 if (source == NULL || slen <= 0)
122 return (NULL);
123
124 p = pcap_create(source, ebuf);
125 if (p == NULL)
126 return (NULL);
127
128 /* Open bpf(4) read only */
129 if ((fd = open("/dev/bpf", O_RDONLY)) == -1)
130 return (NULL);
131
132 if (ioctl(fd, BIOCVERSION, &bv) == -1) {
133 snprintf(ebuf, PCAP_ERRBUF_SIZE, "BIOCVERSION: %s",
134 pcap_strerror(errno));
135 goto bad;
136 }
137
138 if (bv.bv_major != BPF_MAJOR_VERSION ||
139 bv.bv_minor < BPF_MINOR_VERSION) {
140 snprintf(ebuf, PCAP_ERRBUF_SIZE,
141 "kernel bpf filter out of date");
142 goto bad;
143 }
144
145 strlcpy(ifr.ifr_name, source, sizeof(ifr.ifr_name));
146 if (ioctl(fd, BIOCSETIF, &ifr) == -1) {
147 snprintf(ebuf, PCAP_ERRBUF_SIZE, "BIOCSETIF: %s",
148 pcap_strerror(errno));
149 goto bad;
150 }
151
152 if (dlt != (u_int) -1 && ioctl(fd, BIOCSDLT, &dlt)) {
153 snprintf(ebuf, PCAP_ERRBUF_SIZE, "BIOCSDLT: %s",
154 pcap_strerror(errno));
155 goto bad;
156 }
157
158 p->fd = fd;
159 p->snapshot = slen;
160 p->linktype = dlt;
161
162 /* set timeout */
163 if (to_ms != 0) {
164 struct timeval to;
165 to.tv_sec = to_ms / 1000;
166 to.tv_usec = (to_ms * 1000) % 1000000;
167 if (ioctl(p->fd, BIOCSRTIMEOUT, &to) == -1) {
168 snprintf(ebuf, PCAP_ERRBUF_SIZE, "BIOCSRTIMEOUT: %s",
169 pcap_strerror(errno));
170 goto bad;
171 }
172 }
173 if (promisc)
174 /* this is allowed to fail */
175 ioctl(fd, BIOCPROMISC, NULL);
176
177 if (ioctl(fd, BIOCGBLEN, &v) == -1) {
178 snprintf(ebuf, PCAP_ERRBUF_SIZE, "BIOCGBLEN: %s",
179 pcap_strerror(errno));
180 goto bad;
181 }
182 p->bufsize = v;
183 p->buffer = malloc(p->bufsize);
184 if (p->buffer == NULL) {
185 snprintf(ebuf, PCAP_ERRBUF_SIZE, "malloc: %s",
186 pcap_strerror(errno));
187 goto bad;
188 }
189 p->activated = 1;
190 return (p);
191
192 bad:
193 pcap_close(p);
194 return (NULL);
195 }
196
197 int
init_pcap(void)198 init_pcap(void)
199 {
200 struct bpf_program bpfp;
201 char filter[PCAPFSIZ] = "ip and port 25 and action pass "
202 "and tcp[13]&0x12=0x2";
203
204 if ((hpcap = pflog_read_live(pflogif, PCAPSNAP, 1, PCAPTIMO,
205 errbuf)) == NULL) {
206 logmsg(LOG_ERR, "Failed to initialize: %s", errbuf);
207 return (-1);
208 }
209
210 if (pcap_datalink(hpcap) != DLT_PFLOG) {
211 logmsg(LOG_ERR, "Invalid datalink type");
212 pcap_close(hpcap);
213 hpcap = NULL;
214 return (-1);
215 }
216
217 if (networkif != NULL) {
218 strlcat(filter, " and on ", PCAPFSIZ);
219 strlcat(filter, networkif, PCAPFSIZ);
220 }
221
222 if (pcap_compile(hpcap, &bpfp, filter, PCAPOPTZ, 0) == -1 ||
223 pcap_setfilter(hpcap, &bpfp) == -1) {
224 logmsg(LOG_ERR, "%s", pcap_geterr(hpcap));
225 return (-1);
226 }
227
228 pcap_freecode(&bpfp);
229
230 if (ioctl(pcap_fileno(hpcap), BIOCLOCK) == -1) {
231 logmsg(LOG_ERR, "BIOCLOCK: %s", strerror(errno));
232 return (-1);
233 }
234
235 return (0);
236 }
237
238 void
logpkt_handler(u_char * user,const struct pcap_pkthdr * h,const u_char * sp)239 logpkt_handler(u_char *user, const struct pcap_pkthdr *h, const u_char *sp)
240 {
241 sa_family_t af;
242 u_int8_t hdrlen;
243 u_int32_t caplen = h->caplen;
244 const struct ip *ip = NULL;
245 const struct pfloghdr *hdr;
246 char ipstraddr[40] = { '\0' };
247
248 hdr = (const struct pfloghdr *)sp;
249 if (hdr->length < MIN_PFLOG_HDRLEN) {
250 logmsg(LOG_WARNING, "invalid pflog header length (%u/%u). "
251 "packet dropped.", hdr->length, MIN_PFLOG_HDRLEN);
252 return;
253 }
254 hdrlen = BPF_WORDALIGN(hdr->length);
255
256 if (caplen < hdrlen) {
257 logmsg(LOG_WARNING, "pflog header larger than caplen (%u/%u). "
258 "packet dropped.", hdrlen, caplen);
259 return;
260 }
261
262 /* We're interested in passed packets */
263 if (hdr->action != PF_PASS)
264 return;
265
266 af = hdr->af;
267 if (af == AF_INET) {
268 ip = (const struct ip *)(sp + hdrlen);
269 if (hdr->dir == PF_IN)
270 inet_ntop(af, &ip->ip_src, ipstraddr,
271 sizeof(ipstraddr));
272 else if (hdr->dir == PF_OUT && !flag_inbound)
273 inet_ntop(af, &ip->ip_dst, ipstraddr,
274 sizeof(ipstraddr));
275 }
276
277 if (ipstraddr[0] != '\0') {
278 if (hdr->dir == PF_IN)
279 logmsg(LOG_DEBUG,"inbound %s", ipstraddr);
280 else
281 logmsg(LOG_DEBUG,"outbound %s", ipstraddr);
282 dbupdate(PATH_SPAMD_DB, ipstraddr);
283 }
284 }
285
286 int
dbupdate(char * dbname,char * ip)287 dbupdate(char *dbname, char *ip)
288 {
289 HASHINFO hashinfo;
290 DBT dbk, dbd;
291 DB *db;
292 struct gdata gd;
293 time_t now;
294 int r;
295 struct in_addr ia;
296
297 now = time(NULL);
298 memset(&hashinfo, 0, sizeof(hashinfo));
299 db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_HASH, &hashinfo);
300 if (db == NULL) {
301 logmsg(LOG_ERR, "Can not open db %s: %s", dbname,
302 strerror(errno));
303 return (-1);
304 }
305 if (inet_pton(AF_INET, ip, &ia) != 1) {
306 logmsg(LOG_NOTICE, "Invalid IP address %s", ip);
307 goto bad;
308 }
309 memset(&dbk, 0, sizeof(dbk));
310 dbk.size = strlen(ip);
311 dbk.data = ip;
312 memset(&dbd, 0, sizeof(dbd));
313
314 /* add or update whitelist entry */
315 r = db->get(db, &dbk, &dbd, 0);
316 if (r == -1) {
317 logmsg(LOG_NOTICE, "db->get failed (%m)");
318 goto bad;
319 }
320
321 if (r) {
322 /* new entry */
323 memset(&gd, 0, sizeof(gd));
324 gd.first = now;
325 gd.bcount = 1;
326 gd.pass = now;
327 gd.expire = now + whiteexp;
328 memset(&dbk, 0, sizeof(dbk));
329 dbk.size = strlen(ip);
330 dbk.data = ip;
331 memset(&dbd, 0, sizeof(dbd));
332 dbd.size = sizeof(gd);
333 dbd.data = &gd;
334 r = db->put(db, &dbk, &dbd, 0);
335 if (r) {
336 logmsg(LOG_NOTICE, "db->put failed (%m)");
337 goto bad;
338 }
339 } else {
340 /* XXX - backwards compat */
341 if (gdcopyin(&dbd, &gd) == -1) {
342 /* whatever this is, it doesn't belong */
343 db->del(db, &dbk, 0);
344 goto bad;
345 }
346 gd.pcount++;
347 gd.expire = now + whiteexp;
348 memset(&dbk, 0, sizeof(dbk));
349 dbk.size = strlen(ip);
350 dbk.data = ip;
351 memset(&dbd, 0, sizeof(dbd));
352 dbd.size = sizeof(gd);
353 dbd.data = &gd;
354 r = db->put(db, &dbk, &dbd, 0);
355 if (r) {
356 logmsg(LOG_NOTICE, "db->put failed (%m)");
357 goto bad;
358 }
359 }
360 db->close(db);
361 db = NULL;
362 if (syncsend)
363 sync_white(now, now + whiteexp, ip);
364 return (0);
365 bad:
366 db->close(db);
367 db = NULL;
368 return (-1);
369 }
370
371 void
usage(void)372 usage(void)
373 {
374 fprintf(stderr,
375 "usage: %s [-DI] [-i interface] [-l pflog_interface] "
376 "[-W whiteexp] [-Y synctarget]\n",
377 __progname);
378 exit(1);
379 }
380
381 int
main(int argc,char ** argv)382 main(int argc, char **argv)
383 {
384 int ch;
385 struct passwd *pw;
386 pcap_handler phandler = logpkt_handler;
387 int syncfd = 0;
388 struct servent *ent;
389 char *sync_iface = NULL;
390 char *sync_baddr = NULL;
391 const char *errstr;
392
393 if (geteuid())
394 errx(1, "need root privileges");
395
396 if ((ent = getservbyname("spamd-sync", "udp")) == NULL)
397 errx(1, "Can't find service \"spamd-sync\" in /etc/services");
398 sync_port = ntohs(ent->s_port);
399
400 while ((ch = getopt(argc, argv, "DIi:l:W:Y:")) != -1) {
401 switch (ch) {
402 case 'D':
403 flag_debug = 1;
404 break;
405 case 'I':
406 flag_inbound = 1;
407 break;
408 case 'i':
409 networkif = optarg;
410 break;
411 case 'l':
412 pflogif = optarg;
413 break;
414 case 'W':
415 /* limit whiteexp to 2160 hours (90 days) */
416 whiteexp = strtonum(optarg, 1, (24 * 90), &errstr);
417 if (errstr)
418 usage();
419 /* convert to seconds from hours */
420 whiteexp *= (60 * 60);
421 break;
422 case 'Y':
423 if (sync_addhost(optarg, sync_port) != 0)
424 sync_iface = optarg;
425 syncsend++;
426 break;
427 default:
428 usage();
429 }
430 }
431
432 signal(SIGINT , sighandler_close);
433 signal(SIGQUIT, sighandler_close);
434 signal(SIGTERM, sighandler_close);
435
436 logmsg(LOG_DEBUG, "Listening on %s for %s %s", pflogif,
437 (networkif == NULL) ? "all interfaces." : networkif,
438 (flag_inbound) ? "Inbound direction only." : "");
439
440 if (init_pcap() == -1)
441 err(1, "couldn't initialize pcap");
442
443 if (syncsend) {
444 syncfd = sync_init(sync_iface, sync_baddr, sync_port);
445 if (syncfd == -1)
446 err(1, "sync init");
447 }
448
449 /* privdrop */
450 if ((pw = getpwnam(SPAMD_USER)) == NULL)
451 errx(1, "no such user %s", SPAMD_USER);
452
453 if (setgroups(1, &pw->pw_gid) ||
454 setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
455 setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
456 err(1, "failed to drop privs");
457
458 if (!flag_debug) {
459 if (daemon(0, 0) == -1)
460 err(1, "daemon");
461 tzset();
462 openlog_r("spamlogd", LOG_PID | LOG_NDELAY, LOG_DAEMON, &sdata);
463 }
464
465 if (unveil(PATH_SPAMD_DB, "rw") == -1)
466 err(1, "unveil %s", PATH_SPAMD_DB);
467 if (syncsend) {
468 if (pledge("stdio rpath wpath inet flock", NULL) == -1)
469 err(1, "pledge");
470 } else {
471 if (pledge("stdio rpath wpath flock", NULL) == -1)
472 err(1, "pledge");
473 }
474
475 pcap_loop(hpcap, -1, phandler, NULL);
476
477 logmsg(LOG_NOTICE, "exiting");
478 if (!flag_debug)
479 closelog_r(&sdata);
480
481 exit(0);
482 }
483