/* $OpenBSD: spamlogd.c,v 1.32 2021/07/12 15:09:18 beck Exp $ */ /* * Copyright (c) 2006 Henning Brauer * Copyright (c) 2006 Berk D. Demir. * Copyright (c) 2004-2007 Bob Beck. * Copyright (c) 2001 Theo de Raadt. * Copyright (c) 2001 Can Erkin Acar. * All rights reserved * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /* watch pf log for mail connections, update whitelist entries. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "grey.h" #include "sync.h" #define MIN_PFLOG_HDRLEN 45 #define PCAPSNAP 512 #define PCAPTIMO 500 /* ms */ #define PCAPOPTZ 1 /* optimize filter */ #define PCAPFSIZ 512 /* pcap filter string size */ #define SPAMD_USER "_spamd" int debug = 1; int greylist = 1; FILE *grey = NULL; u_short sync_port; int syncsend; u_int8_t flag_debug = 0; u_int8_t flag_inbound = 0; char *networkif = NULL; char *pflogif = "pflog0"; char errbuf[PCAP_ERRBUF_SIZE]; pcap_t *hpcap = NULL; struct syslog_data sdata = SYSLOG_DATA_INIT; time_t whiteexp = WHITEEXP; extern char *__progname; void logmsg(int , const char *, ...); void sighandler_close(int); int init_pcap(void); void logpkt_handler(u_char *, const struct pcap_pkthdr *, const u_char *); int dbupdate(char *, char *); __dead void usage(void); void logmsg(int pri, const char *msg, ...) { va_list ap; va_start(ap, msg); if (flag_debug) { vfprintf(stderr, msg, ap); fprintf(stderr, "\n"); } else vsyslog_r(pri, &sdata, msg, ap); va_end(ap); } void sighandler_close(int signal) { if (hpcap != NULL) pcap_breakloop(hpcap); /* sighdlr safe */ } pcap_t * pflog_read_live(const char *source, int slen, int promisc, int to_ms, char *ebuf) { int fd; struct bpf_version bv; struct ifreq ifr; u_int v, dlt = DLT_PFLOG; pcap_t *p; if (source == NULL || slen <= 0) return (NULL); p = pcap_create(source, ebuf); if (p == NULL) return (NULL); /* Open bpf(4) read only */ if ((fd = open("/dev/bpf", O_RDONLY)) == -1) return (NULL); if (ioctl(fd, BIOCVERSION, &bv) == -1) { snprintf(ebuf, PCAP_ERRBUF_SIZE, "BIOCVERSION: %s", pcap_strerror(errno)); goto bad; } if (bv.bv_major != BPF_MAJOR_VERSION || bv.bv_minor < BPF_MINOR_VERSION) { snprintf(ebuf, PCAP_ERRBUF_SIZE, "kernel bpf filter out of date"); goto bad; } strlcpy(ifr.ifr_name, source, sizeof(ifr.ifr_name)); if (ioctl(fd, BIOCSETIF, &ifr) == -1) { snprintf(ebuf, PCAP_ERRBUF_SIZE, "BIOCSETIF: %s", pcap_strerror(errno)); goto bad; } if (dlt != (u_int) -1 && ioctl(fd, BIOCSDLT, &dlt)) { snprintf(ebuf, PCAP_ERRBUF_SIZE, "BIOCSDLT: %s", pcap_strerror(errno)); goto bad; } p->fd = fd; p->snapshot = slen; p->linktype = dlt; /* set timeout */ if (to_ms != 0) { struct timeval to; to.tv_sec = to_ms / 1000; to.tv_usec = (to_ms * 1000) % 1000000; if (ioctl(p->fd, BIOCSRTIMEOUT, &to) == -1) { snprintf(ebuf, PCAP_ERRBUF_SIZE, "BIOCSRTIMEOUT: %s", pcap_strerror(errno)); goto bad; } } if (promisc) /* this is allowed to fail */ ioctl(fd, BIOCPROMISC, NULL); if (ioctl(fd, BIOCGBLEN, &v) == -1) { snprintf(ebuf, PCAP_ERRBUF_SIZE, "BIOCGBLEN: %s", pcap_strerror(errno)); goto bad; } p->bufsize = v; p->buffer = malloc(p->bufsize); if (p->buffer == NULL) { snprintf(ebuf, PCAP_ERRBUF_SIZE, "malloc: %s", pcap_strerror(errno)); goto bad; } p->activated = 1; return (p); bad: pcap_close(p); return (NULL); } int init_pcap(void) { struct bpf_program bpfp; char filter[PCAPFSIZ] = "ip and port 25 and action pass " "and tcp[13]&0x12=0x2"; if ((hpcap = pflog_read_live(pflogif, PCAPSNAP, 1, PCAPTIMO, errbuf)) == NULL) { logmsg(LOG_ERR, "Failed to initialize: %s", errbuf); return (-1); } if (pcap_datalink(hpcap) != DLT_PFLOG) { logmsg(LOG_ERR, "Invalid datalink type"); pcap_close(hpcap); hpcap = NULL; return (-1); } if (networkif != NULL) { strlcat(filter, " and on ", PCAPFSIZ); strlcat(filter, networkif, PCAPFSIZ); } if (pcap_compile(hpcap, &bpfp, filter, PCAPOPTZ, 0) == -1 || pcap_setfilter(hpcap, &bpfp) == -1) { logmsg(LOG_ERR, "%s", pcap_geterr(hpcap)); return (-1); } pcap_freecode(&bpfp); if (ioctl(pcap_fileno(hpcap), BIOCLOCK) == -1) { logmsg(LOG_ERR, "BIOCLOCK: %s", strerror(errno)); return (-1); } return (0); } void logpkt_handler(u_char *user, const struct pcap_pkthdr *h, const u_char *sp) { sa_family_t af; u_int8_t hdrlen; u_int32_t caplen = h->caplen; const struct ip *ip = NULL; const struct pfloghdr *hdr; char ipstraddr[40] = { '\0' }; hdr = (const struct pfloghdr *)sp; if (hdr->length < MIN_PFLOG_HDRLEN) { logmsg(LOG_WARNING, "invalid pflog header length (%u/%u). " "packet dropped.", hdr->length, MIN_PFLOG_HDRLEN); return; } hdrlen = BPF_WORDALIGN(hdr->length); if (caplen < hdrlen) { logmsg(LOG_WARNING, "pflog header larger than caplen (%u/%u). " "packet dropped.", hdrlen, caplen); return; } /* We're interested in passed packets */ if (hdr->action != PF_PASS) return; af = hdr->af; if (af == AF_INET) { ip = (const struct ip *)(sp + hdrlen); if (hdr->dir == PF_IN) inet_ntop(af, &ip->ip_src, ipstraddr, sizeof(ipstraddr)); else if (hdr->dir == PF_OUT && !flag_inbound) inet_ntop(af, &ip->ip_dst, ipstraddr, sizeof(ipstraddr)); } if (ipstraddr[0] != '\0') { if (hdr->dir == PF_IN) logmsg(LOG_DEBUG,"inbound %s", ipstraddr); else logmsg(LOG_DEBUG,"outbound %s", ipstraddr); dbupdate(PATH_SPAMD_DB, ipstraddr); } } int dbupdate(char *dbname, char *ip) { HASHINFO hashinfo; DBT dbk, dbd; DB *db; struct gdata gd; time_t now; int r; struct in_addr ia; now = time(NULL); memset(&hashinfo, 0, sizeof(hashinfo)); db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_HASH, &hashinfo); if (db == NULL) { logmsg(LOG_ERR, "Can not open db %s: %s", dbname, strerror(errno)); return (-1); } if (inet_pton(AF_INET, ip, &ia) != 1) { logmsg(LOG_NOTICE, "Invalid IP address %s", ip); goto bad; } memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(ip); dbk.data = ip; memset(&dbd, 0, sizeof(dbd)); /* add or update whitelist entry */ r = db->get(db, &dbk, &dbd, 0); if (r == -1) { logmsg(LOG_NOTICE, "db->get failed (%m)"); goto bad; } if (r) { /* new entry */ memset(&gd, 0, sizeof(gd)); gd.first = now; gd.bcount = 1; gd.pass = now; gd.expire = now + whiteexp; memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(ip); dbk.data = ip; memset(&dbd, 0, sizeof(dbd)); dbd.size = sizeof(gd); dbd.data = &gd; r = db->put(db, &dbk, &dbd, 0); if (r) { logmsg(LOG_NOTICE, "db->put failed (%m)"); goto bad; } } else { /* XXX - backwards compat */ if (gdcopyin(&dbd, &gd) == -1) { /* whatever this is, it doesn't belong */ db->del(db, &dbk, 0); goto bad; } gd.pcount++; gd.expire = now + whiteexp; memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(ip); dbk.data = ip; memset(&dbd, 0, sizeof(dbd)); dbd.size = sizeof(gd); dbd.data = &gd; r = db->put(db, &dbk, &dbd, 0); if (r) { logmsg(LOG_NOTICE, "db->put failed (%m)"); goto bad; } } db->close(db); db = NULL; if (syncsend) sync_white(now, now + whiteexp, ip); return (0); bad: db->close(db); db = NULL; return (-1); } void usage(void) { fprintf(stderr, "usage: %s [-DI] [-i interface] [-l pflog_interface] " "[-W whiteexp] [-Y synctarget]\n", __progname); exit(1); } int main(int argc, char **argv) { int ch; struct passwd *pw; pcap_handler phandler = logpkt_handler; int syncfd = 0; struct servent *ent; char *sync_iface = NULL; char *sync_baddr = NULL; const char *errstr; if (geteuid()) errx(1, "need root privileges"); if ((ent = getservbyname("spamd-sync", "udp")) == NULL) errx(1, "Can't find service \"spamd-sync\" in /etc/services"); sync_port = ntohs(ent->s_port); while ((ch = getopt(argc, argv, "DIi:l:W:Y:")) != -1) { switch (ch) { case 'D': flag_debug = 1; break; case 'I': flag_inbound = 1; break; case 'i': networkif = optarg; break; case 'l': pflogif = optarg; break; case 'W': /* limit whiteexp to 2160 hours (90 days) */ whiteexp = strtonum(optarg, 1, (24 * 90), &errstr); if (errstr) usage(); /* convert to seconds from hours */ whiteexp *= (60 * 60); break; case 'Y': if (sync_addhost(optarg, sync_port) != 0) sync_iface = optarg; syncsend++; break; default: usage(); } } signal(SIGINT , sighandler_close); signal(SIGQUIT, sighandler_close); signal(SIGTERM, sighandler_close); logmsg(LOG_DEBUG, "Listening on %s for %s %s", pflogif, (networkif == NULL) ? "all interfaces." : networkif, (flag_inbound) ? "Inbound direction only." : ""); if (init_pcap() == -1) err(1, "couldn't initialize pcap"); if (syncsend) { syncfd = sync_init(sync_iface, sync_baddr, sync_port); if (syncfd == -1) err(1, "sync init"); } /* privdrop */ if ((pw = getpwnam(SPAMD_USER)) == NULL) errx(1, "no such user %s", SPAMD_USER); if (setgroups(1, &pw->pw_gid) || setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) err(1, "failed to drop privs"); if (!flag_debug) { if (daemon(0, 0) == -1) err(1, "daemon"); tzset(); openlog_r("spamlogd", LOG_PID | LOG_NDELAY, LOG_DAEMON, &sdata); } if (unveil(PATH_SPAMD_DB, "rw") == -1) err(1, "unveil %s", PATH_SPAMD_DB); if (syncsend) { if (pledge("stdio rpath wpath inet flock", NULL) == -1) err(1, "pledge"); } else { if (pledge("stdio rpath wpath flock", NULL) == -1) err(1, "pledge"); } pcap_loop(hpcap, -1, phandler, NULL); logmsg(LOG_NOTICE, "exiting"); if (!flag_debug) closelog_r(&sdata); exit(0); }