[Greylisting] - graymilter with DNS based whitelists support

graymilter is an excellent (and very simple) tool to implement Greylisting. But what if you want to have whitelists not based on IP lists, but on DNS instead? Say to allow all .amazon.com machines to connect to your mail server without having to know the IP addresses of Amazon's outgoing mail servers?

A FIRST ATTEMPT USING TCP_WRAPPERS

Here comes my patch on graymilter.c (version 1.23):


Tue Jun 14 16:54 [146] > diff graymilter.c graymilter.c-
76d75
< #include <tcpd.h>
543d541
<     struct request_info req;
582,593d579
<
<     /*
<      * Check if the hostname is allowed in /etc/hosts.allow.
<      * Note 1: We are searching only for ALLOW entries.  DENY entries
<      *         are ignored
<      * Note 2: The servicename in /etc/hosts.allow is graymilter
<      */
<     request_init(&req, RQ_DAEMON, "graymilter", RQ_CLIENT_NAME, connhost, 0);
<     fromhost(&req);
<     if (hosts_access(&req)) {
<         syslog( LOG_INFO, "%s is whitelisted by /etc/hosts.allow - accepting", connhost );
<         return SMFIS_ACCEPT;
<     }

Please remember to add -lwrap at LDFLAGS in your Makefile.

Use this hack with great care. An enty like:

graymilter: .gr : ALLOW
in /etc/hosts.allow would mean that any host in the .gr domain name space is whitelisted. This includes DSL and dialup customers of all the Greek ISPs! This is definately not what you wanted whitelisted.

Now, on a semi-busy mail server (meaning thousands of messages per hour) this could be a problem. Graymilter crashes because it opens far too many times /etc/hosts.allow for reading. I have implemented another hack for this situation:

A SECOND TRY

Here comes my patch on graymilter.c (version 1.26):


Fri Jan 13, 12:12:23 [503] # diff graymilter.c graymilter.c-
76d75
< #include "tee_check_domain.c"
680,692d678
<     if (tee_check_domain(connhost, ".example.com") == 0) {
<           syslog(LOG_INFO, "accepting host %s", connhost);
<           return SMFIS_ACCEPT;
<     }
<     if (tee_check_domain(connhost, ".example.org") == 0) {
<           syslog(LOG_INFO, "accepting host %s", connhost);
<           return SMFIS_ACCEPT;
<     }
<     if (tee_check_domain(connhost, ".example.net") == 0) {
<           syslog(LOG_INFO, "accepting host %s", connhost);
<           return SMFIS_ACCEPT;
<     }
<
Of course you can play arround with stdarg(3) and wrap multiple calls to tee_check_domain() like this:

#include <stdarg.h>
static int
tee_check_domain_list(char *x, ...)
{
va_list ap;
char *s;

        va_start(ap, s);
        while((s = va_arg(ap, char *)) != NULL) {
                if (tee_check_domain(x, s) == 0) {
                        return 0;
                }
        }
        va_end(ap);

        return 1;
}
and call once:

if (tee_check_domain_list(connhost, ".example.com", ".example.net", ".example.org", NULL) == 0) {
	syslog(LOG_INFO, "accepting host %s", connhost);
	return SMFIS_ACCEPT;
}
and tee_check_domain() is:

#ifndef _TEE_CHECK_DOMAIN_C_
#define _TEE_CHECK_DOMAIN_C_ 1

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Think of this as a rightmost strcmp() */

static int
tee_check_domain(char *x, char *y)
{
int xl, yl, l;
char *s;

        xl = strlen(x);
        yl = strlen(y);
        l = xl - yl;

        if (l < 0) {
                return l;
        } else {
                s = x + l;
                return (strncmp(s, y, yl));
        }
}

#endif /* _TEE_CHECK_DOMAIN_C_ */
A THIRD ALTERNATIVE

This requires you to have installed rbldnsd and lwres(3) (lwres comes with BIND9). Using this, you do not have to restart graymilter everytime you want to add or remove a domain name from your whitelist (as is needed with the second version). So what you have to do is:

  1. Decide a whitelist DNS zone name. For example, whitelist.tee.gr. Have rbldnsd serve either a dnset or a combined zone. My whitelist.tee.gr. zonefile looks like:
    ; It is declared as a combined zonefile when rbldnsd statrs
    $DATASET dnset: @
    .example.com
    .example.net
    .example.org
    
  2. For version 1.26:
    
    76d75
    < #include "tee_check_domain.c"
    680,684d678
    <     if (tee_check_domain(connhost, ".whitelist.tee.gr") == 0) {
    <           syslog(LOG_INFO, "accepting host %s", connhost);
    <           return SMFIS_ACCEPT;
    <     }
    <
    
  3. and tee_check_domain() becomes:
    
    #ifndef _TEE_CHECK_DOMAIN_
    #define _TEE_CHECK_DOMAIN_ 1
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <lwres/lwres.h>
    #include <lwres/netdb.h>
    
    static int
    tee_check_domain(char *host, char *whitelist)
    {
    char *name;
    int l, herr;
    struct hostent *he;
    
            l = strlen(host) + strlen(whitelist) + 1;
            name = (char *) malloc(l);
            if (name == NULL) {
                    syslog(LOG_INFO, "tee_check_domain(): malloc() error: aborting");
                    return 0;
            }
            memset(name, 0, l);
            sprintf(name, "%s%s", host, whitelist);
    
            he = lwres_getipnodebyname(name, AF_INET, 0, &herr);
            if (he == NULL) {
                    free(name);
                    if (herr == HOST_NOT_FOUND) {
                            return 1;
                    } else {
                            return 0;
                    }
            }
    
            lwres_freehostent(he);
            free(name);
            return 0;
    }
    
    #endif /* _TEE_CHECK_DOMAIN_ */
    
  4. After running ./configure and before running make please remember to add -llwres to the Makefile (right after -lmilter).
Please note the starting dot (.) of the second argument when calling tee_check_domain(). It must be there (after all this is a hack, right?). Maybe I'll fix it and have it check for (and insert if needed) this dot.

Special Thanks

  1. Jef Poskanzer, graymilter's author.
  2. Mart Pirita for being the first user of patch #2 and making me build patch #3

For those interested in what the tee_ prefix stands for, TEE.gr is my current employer.

adamo@dblab.ece.ntua.gr