I forgot to attach my prototype. Here it is. On 2016-03-29 Bob Beck <b...@openbsd.org> wrote: > No. DNS based whitelisting does not belong in there. because it is > slow and DOS'able > > spamd is designed to be high speed low drag. If you want to do a DNS > based whitelist, write a little co-thing that spits one > into a file or into your nospamd table that then spamd *does not even > see*.
That's kind of what my prototype implementation does. > In short *spamd* is the wrong place to do this. put your dns based > whitelist in a table periodically I put the ips into the table on demand since I cannot simply download a dnswl. I already suspected relayd might be a better place to do this. Or keep it in a separate program. Sadly I cannot reinject the diverted SYN into pf. > On Tue, Mar 29, 2016 at 1:11 PM, Christopher Zimmermann > <chr...@openbsd.org> wrote: > > Hi, > > > > I want to use a DNS white list to skip greylisting delays for known > > good addresses, which would pass the greylist anyway. > > To do this with spamd and OpenSMTPd I wrote a prototype which > > intercepts the initial SYN packet from any non-whitelisted ip. It > > then queries DNS whitelists and on any positive reply it whitelists > > the ip. The SYN packet is dropped. Any sane smtp server will very > > shortly resend the SYN and get through to OpenSMTPd. > > This program is only a proof-of-concept. I think the same > > functionality could be integrated into spamd or as transparent > > relay into relayd. Is this a sensible approach? > > > > Christopher > > > > > > On 2016-03-15 Stuart Henderson <s...@spacehopper.org> wrote: > >> On 2016/03/15 12:55, Craig Skinner wrote: > >> > Generally, everything has changed from file feeds to DNS. > >> > >> Yep, because for the more actively maintained ones 1) new entries > >> show up more quickly than any sane rsync interval, this is quite > >> important for good blocking these days 2) DNS is less resource > >> intensive and more easily distributed than rsync, and 3) > >> importantly for the rbl providers, it gives additional input to > >> them about new mail sources (if an rbl suddenly starts seeing > >> queries from all over the world for a previously unseen address, > >> it's probably worth investigation - I am sure this is why some of > >> the commercial antispam operators provide free DNS-based lookups > >> for smaller orgs). > >> > >> A more flexible approach would be to skip the PF table integration > >> completely and do DNS lookups in spamd (or, uh, relayd, or > >> something new) and based on that it could choose whether to > >> tarpit, greylist or transparent-forward the connection to the real > >> mail server. This would also give a way to use dnswl.org's > >> whitelist to avoid greylisting for those hosts where it just > >> doesn't work well (gmail, office365 etc). #include <sys/types.h> #include <sys/time.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <sys/fcntl.h> #include <sys/wait.h> #include <netinet/in.h> #include <netinet/ip.h> #include <netinet/ip6.h> #include <netinet/tcp.h> #include <net/if.h> #include <net/pfvar.h> #include <arpa/inet.h> #include <arpa/nameser.h> #include <resolv.h> #include <poll.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <time.h> #include <pwd.h> #include <grp.h> #include <err.h> #include <assert.h> #define DEBUG 0 #define DIVERT_PORT 25 #define NSTATES 10 struct dns_header { uint16_t id; uint16_t flags; #define QR 0x8000 #define OPCODE_MASK 0x7800 #define OPCODE_SHIFT 11 #define AA 0x0400 #define TC 0x0200 #define RD 0x0100 #define RA 0x0080 #define AD 0x0020 #define CD 0x0010 #define RCODE_MASK 0x000f #define RCODE_SHIFT 0 uint16_t qdcount; uint16_t ancount; uint16_t nscount; uint16_t arcount; }; struct dns_record { uint16_t type; uint16_t class; uint32_t ttl; uint16_t length; }; struct state { union { struct in_addr in4; struct in6_addr in6; uint8_t octets[sizeof(struct in6_addr)]; } addr; struct timespec timeout; int af; uint16_t dnskey; } states[NSTATES]; void send_query(struct state *state, const char *question); void process_response(); enum color { white, grey }; void enlist(struct state *state, enum color color); int dnssock, pfdev; const char *const whitelists[] = { "list.dnswl.org", "swl.spamhaus.org" }; int main(int argc, char *argv[]) { int i, ret, timeout; time_t t; struct sockaddr_in sin4; struct sockaddr_in6 sin6; struct group *group; struct passwd *passwd; struct pollfd fds[3]; pfdev = open("/dev/pf", O_RDWR); if (pfdev == -1) err(1, "open(\"/dev/pf\") failed"); ret = IPPROTO_DIVERT_INIT; setsockopt(fds[1].fd, IPPROTO_IP, IP_DIVERTFL, &ret, sizeof(ret)); setsockopt(fds[2].fd, IPPROTO_IPV6, IP_DIVERTFL, &ret, sizeof(ret)); /* DNS */ if (res_init() == -1) err(1, "res_init"); assert(_res_ext.nsaddr_list[0].ss_family != 0); fds[0].fd = dnssock = socket(_res_ext.nsaddr_list[0].ss_family, SOCK_DGRAM | SOCK_DNS, 0); if (fds[0].fd == -1) err(1, "socket"); if (connect(fds[0].fd, (struct sockaddr *)&_res_ext.nsaddr_list[0], _res_ext.nsaddr_list[0].ss_len) != 0) err(1, "connect"); /* IPv4 divert */ memset(&sin4, 0, sizeof(sin4)); sin4.sin_family = AF_INET; sin4.sin_port = htons(DIVERT_PORT); sin4.sin_addr.s_addr = INADDR_ANY; fds[1].fd = socket(AF_INET, SOCK_RAW, IPPROTO_DIVERT); if (fds[1].fd == -1) err(1, "socket"); if (bind(fds[1].fd, (struct sockaddr *) &sin4, sizeof(sin4)) != 0) err(1, "bind"); /* IPv6 divert */ memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6; sin6.sin6_port = htons(DIVERT_PORT); sin6.sin6_addr = in6addr_any; fds[2].fd = socket(AF_INET6, SOCK_RAW, IPPROTO_DIVERT); if (fds[2].fd == -1) err(1, "socket"); if (bind(fds[2].fd, (struct sockaddr *) &sin6, sizeof(sin6)) != 0) err(1, "bind"); group = getgrnam("_spamd"); if (group == NULL) err(1, "getgrnam"); endgrent(); passwd = getpwnam("_spamd"); if (passwd == NULL) err(1, "getpwnam"); /*if (chroot("/var/empty") != 0) err(1, "chroot");*/ if (setgroups(0, NULL) != 0) err(1, "setgroups"); if (setgid(group->gr_gid) != 0) err(1, "setgid"); if (setuid(passwd->pw_uid) != 0) err(1, "setuid"); timeout = INFTIM; fds[0].events = POLLIN; fds[1].events = POLLIN; fds[2].events = POLLIN; #if 0 states[0].af = AF_INET; clock_gettime(CLOCK_MONOTONIC, &states[0].timeout); states[0].timeout.tv_sec++; states[0].addr.in4.s_addr = inet_addr("217.72.192.73"); fds[0].events |= POLLOUT; #endif while (1) { char src[48], dst[48]; struct timespec timestamp; #if DEBUG for(i=0; i < 3; i++) fprintf(stderr, "%d: fd:%d events:%hd revents:%hd\n", i, fds[i].fd, fds[i].events, fds[i].revents); fprintf(stderr, "Polling"); #endif ret = poll(fds, 3, timeout); if (ret == -1) err(1, "poll"); if (clock_gettime(CLOCK_MONOTONIC, ×tamp) == -1) err(1, "clock_gettime"); /*timeout = 5000;*/ #if DEBUG for(i=0; i < 3; i++) fprintf(stderr, "%d: fd:%d events:%hd revents:%hd\n", i, fds[i].fd, fds[i].events, fds[i].revents); #endif /* first check for DNS replies and timeouts to free up states. */ if (fds[0].revents & POLLIN) process_response(); /* timeouts */ for (i=0; i < NSTATES; i++) { if (states[i].af != 0 && timespeccmp(&states[i].timeout, ×tamp, <)) { enlist(&states[i], grey); memset(&states[i], 0, sizeof(states[i])); } } /* send DNS queries ? */ if (fds[0].revents & POLLOUT) { fds[0].events &= ~POLLOUT; for (i=0; i < NSTATES; i++) { if (states[i].af == 0) continue; if (states[i].dnskey == 0) { arc4random_buf(&states[i].dnskey, sizeof(states[i].dnskey)); for (int j = 0; j < sizeof(whitelists) / sizeof(whitelists[0]); j++) { send_query(&states[i], whitelists[j]); } } if (states[i].dnskey == 0) { fds[0].events |= POLLOUT; break; } } } /* Then accept next smtp connects */ if (fds[1].revents & POLLIN) { /* IPv4 */; char packet[IP_MAXPACKET]; const struct ip * const ip = (struct ip *) packet; ret = recv(fds[1].fd, packet, sizeof(packet), MSG_DONTWAIT); if (ret == -1) err(1, "recv"); if (ret < sizeof(struct ip)) { warnx("packet is too short"); continue; } if (inet_ntop(AF_INET, &ip->ip_src, src, sizeof(src)) == NULL) (void)strlcpy(src, "?", sizeof(src)); if (inet_ntop(AF_INET, &ip->ip_dst, dst, sizeof(dst)) == NULL) (void)strlcpy(dst, "?", sizeof(dst)); t = time(NULL); printf("%.19s: %s -> %s\n", ctime(&t), src, dst); for (i=0; i < NSTATES && states[i].af != 0; i++); if (i >= NSTATES) warnx("State table full"); else { states[i].af = AF_INET; states[i].addr.in4 = ip->ip_src; states[i].timeout = timestamp; states[i].timeout.tv_sec++; /* 1s timeout */ /* queue dns */ fds[0].events |= POLLOUT; #if DEBUG fprintf(stderr, "Activated state %d for %s\n", i, name); #endif } } else if (fds[2].revents & POLLIN) { /* IPv6 */; char packet[IPV6_MAXPACKET]; const struct ip6_hdr * const ip6 = (struct ip6_hdr *) packet; ret = recv(fds[2].fd, packet, sizeof(packet), MSG_DONTWAIT); if (ret == -1) err(1, "recv"); if (ret < sizeof(struct ip6_hdr)) { warnx("packet is too short"); continue; } if (inet_ntop(AF_INET6, &ip6->ip6_src, src, sizeof(src)) == NULL) (void)strlcpy(src, "?", sizeof(src)); if (inet_ntop(AF_INET6, &ip6->ip6_dst, dst, sizeof(dst)) == NULL) (void)strlcpy(dst, "?", sizeof(dst)); t = time(NULL); printf("%.19s: %s -> %s\n", ctime(&t), src, dst); for (i=0; i < NSTATES && states[i].af != 0; i++); if (i >= NSTATES) warnx("State table full"); else { states[i].af = AF_INET; states[i].addr.in6 = ip6->ip6_src; states[i].timeout = timestamp; states[i].timeout.tv_sec++; /* 1s timeout */ /* queue dns */ fds[0].events |= POLLOUT; #if DEBUG fprintf(stderr, "Activated state %d for %s\n", i, name); #endif } } } } void send_query(struct state *state, const char *question) { int ret; uint8_t msg[512]; uint8_t *p = msg + sizeof(struct dns_header); struct dns_header *head = (struct dns_header *)msg; struct dns_record *record; char name[HOST_NAME_MAX]; memset(msg, 0, sizeof(msg)); head->id = htons(state->dnskey); head->flags = htons(RD); /* In practise only one question is supported by nameservers. */ head->qdcount = htons(1); ret = snprintf(name, sizeof(name), "%hhu.%hhu.%hhu.%hhu.%s", state->addr.octets[3], state->addr.octets[2], state->addr.octets[1], state->addr.octets[0], question); if (ret >= sizeof(name)) errx(1, "truncated domain name"); ret = dn_comp(name, p, sizeof(msg) - (p-msg), NULL, NULL); if (ret == -1) errx(1, "dn_comp"); p += ret; record = (struct dns_record *)p; p += 4; /* no ttl or length in the question section */ if (p - msg > sizeof(msg)) errx(1, "buffer too small"); record->type = htons(1); record->class = htons(1); ret = send(dnssock, msg, p - msg, MSG_DONTWAIT); /* TODO: use poll */ if (ret == -1) err(1, "send"); if (ret != p - msg) err(1, "sent short datagram"); } void process_response() { int ret; uint8_t msg[512]; const uint8_t *p; struct dns_header *head = (struct dns_header *)msg; #if DEBUG struct dns_record *record; char name[HOST_NAME_MAX]; #endif char address[48]; memset(msg, 0, sizeof(msg)); ret = recv(dnssock, msg, sizeof(msg), MSG_DONTWAIT); if (ret == -1) err(1, "recv"); if (ret > 1023) warn("Datagram truncated."); msg[sizeof(msg) - 1] = '\0'; #if DEBUG fprintf(stderr, "Received DNS: id %#.4hx flags %#.4hx qdcount%hu ancount%hu nscount%hu arcount%hu\n", ntohs(head->id), ntohs(head->flags), ntohs(head->qdcount), ntohs(head->ancount), ntohs(head->nscount), ntohs(head->arcount)); #endif if ((ntohs(head->flags) & (QR|RCODE_MASK)) == QR) { /* lookup successful */ for (int i=0; i <= NSTATES; i++) if (states[i].dnskey == ntohs(head->id)) { if (states[i].af == AF_INET) p = (void *)inet_ntop(AF_INET, &states[i].addr.in4, address, sizeof(address)); else p = (void *)inet_ntop(AF_INET6, &states[i].addr.in6, address, sizeof(address)); if (p == NULL) err(1, "inet_ntop"); fprintf(stderr, "Found %s on whitelist\n", address); enlist(&states[i], white); } } else if ((ntohs(head->flags) & RCODE_MASK) >> RCODE_SHIFT == 3) fprintf(stderr, "No entry found\n"); p = msg + sizeof(struct dns_header); } void enlist(struct state *state, enum color color) { int ret; pid_t pid; struct pfioc_table pfioc; struct pfr_addr pfaddr; char address[48]; if (inet_ntop(state->af, &state->addr, address, sizeof(address)) == NULL) err(1, "inet_ntop"); /* add to spamd-white/grey table */ bzero(&pfioc, sizeof(pfioc)); bzero(&pfaddr, sizeof(pfaddr)); strlcpy(pfioc.pfrio_table.pfrt_name, color == white ? "spamd-white" : "spamd-grey", sizeof(pfioc.pfrio_table.pfrt_name)); pfioc.pfrio_buffer = &pfaddr; pfioc.pfrio_esize = sizeof(pfaddr); pfioc.pfrio_size = 1; pfaddr.pfra_af = state->af; switch (state->af) { case AF_INET: pfaddr.pfra_ip4addr = state->addr.in4; pfaddr.pfra_net = 32; break; case AF_INET6: pfaddr.pfra_ip6addr = state->addr.in6; pfaddr.pfra_net = 128; break; default: errx(1, "unknown address family %d", state->af); } if (ioctl(pfdev, DIOCRADDADDRS, &pfioc) == -1) err(1, "cannot add address to table %s", pfioc.pfrio_table.pfrt_name); else warnx("added address to table %s", pfioc.pfrio_table.pfrt_name); /* add to spamdb database by running the spamdb command. */ if (color == white) { pid = fork(); if (pid == -1) err(1, "fork"); else if (pid == 0) { execle("/usr/sbin/spamdb", "spamdb", "-a", address, NULL, NULL); err(1, "execle"); } do { pid_t pid2; pid2 = waitpid(pid, &ret, 0); if (pid2 == -1) err(1, "waitpid"); assert(pid2 == pid); } while (! WIFEXITED(ret) ); if (WEXITSTATUS(ret) != 0) warnx("spamdb -a %s failed with %d", address, WEXITSTATUS(ret)); } memset(state, 0, sizeof(*state)); }
pgpQ00RjzIRog.pgp
Description: OpenPGP digital signature