On 2016-12-16 Clint Pachl <pa...@ecentryx.com> wrote:

[...]
> What would be
> best is if we could blacklist these spammers upon first connection

I also wanted to just-in-time decisions, but with dnswl lookups.
I wrote a program to intercept incoming, unknown smtp connections and
do a dnswl lookup to whitelist them just in time. You could do the same
for blacklisting, but only for lookups based on ip because the program
looks only at the initial syn packet.
For me this helped a lot to deliver mails faster which would otherwise
be delayed in the greytrap, or even get stuck, because they come from
smtp pools.


here are the pf rules:
pass in on egress inet proto tcp to (self) port smtp flags S/SA no state
divert-packet port 25
pass in on egress inet proto tcp from <dnswl-grey> to (self) port smtp keep
state rdr-to 127.0.0.1 port spamd
pass in log (to pflog1) on egress proto tcp from {<spamd-white> <dnswl-white>}
to port smtp keep state

and here's the C program. It still has lots of dead debugging code.:

#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();

void enlist(struct state *state, int white);

int dnssock, pfdev;

const char *const whitelists[] = {
    "list.dnswl.org",
    "swl.spamhaus.org",
};

int main(int argc, char *argv[])
{
    int i, ret;
    time_t t;
    struct sockaddr_in sin4;
    struct sockaddr_in6 sin6;
    struct group *group;
    struct passwd *passwd;
    struct pollfd fds[3];

    tzset();

    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");

    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 = -1;
        for (i=0; i < NSTATES; i++)
            if (states[i].af != 0 &&
                    (ret == -1 ||
                     timespeccmp(&states[i].timeout, &states[ret].timeout, <)))
                ret = i;
        if (ret == -1)
            ret = ppoll(fds, 3, NULL, NULL);
        else {
            if (clock_gettime(CLOCK_MONOTONIC, &timestamp) == -1) err(1,
"clock_gettime");
            timespecsub(&states[ret].timeout, &timestamp, &timestamp);
            if (timestamp.tv_sec < 0) timestamp.tv_sec = timestamp.tv_nsec = 0;
            ret = ppoll(fds, 3, &timestamp, NULL);
        }
        if (ret == -1) err(1, "poll");
        if (clock_gettime(CLOCK_MONOTONIC, &timestamp) == -1) err(1,
"clock_gettime");

#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], 0);
        }

        /* 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);
            fprintf(stderr, "%.19s: %s -> %s\n", ctime(&t), src, dst);

            ret = -1;
            for (i=0; i < NSTATES; i++) {
                if (states[i].addr.in4.s_addr == ip->ip_src.s_addr) {
                    ret = -2;
                    break;
                }
                if (states[i].af == 0) ret = i;
            }
            if (ret == -1)
                warnx("State table full");
            else if (ret == -2)
                warnx("Already seen");
            else {
                struct timespec timeout = { 0, 900000000 }; /* 0,9 s */
                states[ret].af = AF_INET;
                states[ret].addr.in4 = ip->ip_src;
                timespecadd(&timestamp, &timeout, &states[ret].timeout);

                /* queue dns */
                fds[0].events |= POLLOUT;

#if DEBUG
                fprintf(stderr, "Activated state %d for %s\n", ret, 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);
            fprintf(stderr, "%.19s: %s -> %s\n", ctime(&t), src, dst);

            ret = -1;
            for (i=0; i < NSTATES; i++) {
                if (!memcmp(&states[i].addr.in6, &ip6->ip6_src,
                            sizeof(ip6->ip6_src))) {
                    ret = -2;
                    break;
                }
                if (states[i].af == 0) ret = i;
            }
            if (ret == -1)
                warnx("State table full");
            else if (ret == -2)
                warnx("Already seen");
            else {
                states[ret].af = AF_INET6;
                states[ret].addr.in6 = ip6->ip6_src;
                states[ret].timeout = timestamp;
                states[ret].timeout.tv_sec++; /* 1s timeout */

                /* queue dns */
                fds[0].events |= POLLOUT;

#if DEBUG
                fprintf(stderr, "Activated state %d for %s\n", ret, 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

    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)) {
                enlist(&states[i], 1);
            }
    }
    else if ((ntohs(head->flags) & RCODE_MASK) >> RCODE_SHIFT == 3)
        /* fprintf(stderr, "No entry found\n") */;
    else
        warnx("DNS response code not understood.");

    p = msg + sizeof(struct dns_header);

#if DEBUG
    /* Questions */
    fprintf(stderr, "QUESTIONS section:\n"); fflush(stderr);
    for (int i = 0; i < ntohs(head->qdcount); i++) {
        ret = dn_expand(msg, msg + sizeof(msg), p, name, sizeof(name));
        if (ret == -1) errx(1, "dn_expand");
        p += ret;

        record = (struct dns_record *)p;
        p += 4; /* no ttl or length in the question section */
        if (p - msg > sizeof(msg)) {
            warnx("end of buffer reached.");
            break;
        }

        record->type = htons(1);
        record->class = htons(1);

        fprintf(stderr, "QNAME <%s> QTYPE %hu QCLASS %hu\n",
                name,
                ntohs(record->type), /* QTYPE */
                ntohs(record->class)); /* QCLASS */
    }

    /* Answers */
    fprintf(stderr, "ANSWERS section:\n"); fflush(stderr);
    for (int i = 0; i < ntohs(head->ancount); i++) {
        ret = dn_expand(msg, msg + sizeof(msg), p, name, sizeof(name));
        if (ret == -1) errx(1, "dn_expand");
        p += ret;

        record = (struct dns_record *)p;
        p += 10;
        if (p - msg > sizeof(msg)) {
            warnx("end of buffer reached.");
            break;
        }

        fprintf(stderr, "QNAME <%s> TYPE %hu CLASS %hu TTL %u length %hu data 
<%s> as
IPv4 address: %s\n",
                name,
                ntohs(record->type), /* QTYPE */
                ntohs(record->class), /* QCLASS */
                ntohl(record->ttl), /* TTL */
                ntohs(record->length), /* RDATA length */
                p, inet_ntoa(*(struct in_addr *)p));

        p += ntohs(record->length);
    }

    /* Authorities */
    fprintf(stderr, "AUTHORITIES section:\n"); fflush(stderr);
    for (int i = 0; i < ntohs(head->nscount); i++) {
        ret = dn_expand(msg, msg + sizeof(msg), p, name, sizeof(name));
        if (ret == -1) errx(1, "dn_expand");
        p += ret;

        record = (struct dns_record *)p;
        p += 10;
        if (p - msg > sizeof(msg)) {
            warnx("end of buffer reached.");
            break;
        }

        if (ntohs(record->type) == 1 && ntohs(record->class) == 1 &&
            ntohs(record->length) == 4) {
            if (inet_ntop(AF_INET, p, address, sizeof(address)) == NULL)
                err(1, "inet_ntop");
        }

        fprintf(stderr, "QNAME <%s> TYPE %hu CLASS %hu TTL %u length %hu data 
<%s> as
IPv4 address: %s\n",
                name,
                ntohs(record->type), /* QTYPE */
                ntohs(record->class), /* QCLASS */
                ntohl(record->ttl), /* TTL */
                ntohs(record->length), /* RDATA length */
                p, inet_ntoa(*(struct in_addr *)p));

        p += ntohs(record->length);
    }

    /* Additional */
    fprintf(stderr, "ADDITIONAL section:\n"); fflush(stderr);
    for (int i = 0; i < ntohs(head->arcount); i++) {
        ret = dn_expand(msg, msg + sizeof(msg), p, name, sizeof(name));
        if (ret == -1) errx(1, "dn_expand");
        p += ret;

        record = (struct dns_record *)p;
        p += 10;
        if (p - msg > sizeof(msg)) {
            warnx("end of buffer reached.");
            break;
        }

        if (ntohs(record->type) == 1 && ntohs(record->class) == 1 &&
            ntohs(record->length) == 4) {
            if (inet_ntop(AF_INET, p, address, sizeof(address)) == NULL)
                err(1, "inet_ntop");
        }

        fprintf(stderr, "QNAME <%s> TYPE %hu CLASS %hu TTL %u length %hu data 
<%s> as
IPv4 address: %s\n",
                name,
                ntohs(record->type), /* QTYPE */
                ntohs(record->class), /* QCLASS */
                ntohl(record->ttl), /* TTL */
                ntohs(record->length), /* RDATA length */
                p, inet_ntoa(*(struct in_addr *)p));

        p += ntohs(record->length);
    }
#endif
}

void enlist(struct state *state, int white)
{
#if 0
    int ret;
    pid_t pid;
#endif
    time_t t;
    struct pfioc_table pfioc;
    struct pfr_addr pfaddr;
    char address[48];

    /* add to spamd-white/grey table */
    bzero(&pfioc, sizeof(pfioc));
    bzero(&pfaddr, sizeof(pfaddr));
    strlcpy(pfioc.pfrio_table.pfrt_name,
            white ? "dnswl-white" : "dnswl-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 (inet_ntop(pfaddr.pfra_af, &pfaddr.pfra_ip4addr, address,
sizeof(address)) == NULL)
        err(1, "inet_ntop");

    if (ioctl(pfdev, DIOCRADDADDRS, &pfioc) == -1)
        err(1, "cannot add %s to table %s", address, 
pfioc.pfrio_table.pfrt_name);
    else {
        t = time(NULL);
        fprintf(stderr, "%.19s: added %d %s to table %s\n",
                ctime(&t), pfioc.pfrio_nadd, address, 
pfioc.pfrio_table.pfrt_name);
    }

    /* add to spamdb database by running the spamdb command. */
#if 0
    if (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));
    }
#endif

    memset(state, 0, sizeof(*state));
}


--
http://gmerlin.de
OpenPGP: http://gmerlin.de/christopher.pub
2779 7F73 44FD 0736 B67A  C410 69EC 7922 34B4 2566

[demime 1.01d removed an attachment of type application/pgp-signature]

Reply via email to