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, &timestamp) == -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, &timestamp, <)) {
                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));
}

Attachment: pgpQ00RjzIRog.pgp
Description: OpenPGP digital signature

Reply via email to