Re: [exim] greylisting

Top Page
Delete this message
Reply to this message
Author: List
Date:  
To: exim-users
Subject: Re: [exim] greylisting
Le samedi 27 juin 2009 à 15:28:34, Dmitry Samersoff, vous avez écrit :
> David,
>
> I think SQL for graylist is totally overkill.

[...]

I used 'greylistd' python script ( using Exim socket ), then decided to
reimplement its in purely within Exim ${dlfunc.

In RCPT or DATA :

 defer condition      = ${if ...}{1}{0}}
       set acl_m_gris  = ${dlfunc{/usr/lib/exim/exim-gris.so}{gris}}
       condition          = ${if >{$acl_m_gris}{0}}
       message          = Greylisting, please try again later 
                ($acl_m_gris secondes)


This ${dlfunc use Exim functions for accessing Exim's hints database. This
module does not contain code for reading DBM files, all the relevant
functions are called as Exim macros (exim-4.69/src/dbstuff.h,
exim-4.69/src/dbfunctions.h). The DBM file has stored in Exim
spool_directory/db/greylisting

For maintaining greylisting database, this function auto remove obsolete data
(macro LAST_UPDATE 60*15).

To dump out the contents of the database:
% exim -be '${dlfunc{/usr/lib/exim/exim-gris.so}{dump}}'
#
# Greylisting Sat Jun 27 18:45:57 2009
#
{
key  = "131.111.8/exim-users@???/recipient@???"
        flag  = W
        last  = Thu Jun 25 13:29:04 2009
        first = Thu Jun 25 13:19:54 2009
        count = 1
}
[...]
# Total: 83 (White: 57|Grey: 25)


% gcc -O2 -Wall -Werror -shared -fPIC -g \
    -I./exim-4.69/build-Linux-i386/ \
    -L/usr/lib \
    -DLAST_UPDATE=60*15 \
    -DMAX_GREY=60*60*8 \
    -DRETRY_MAX=60*60*24*36 \
    -DRETRY_MIN=60*5 \
    -o /usr/lib/exim/exim-gris.so exim-gris.c \
    && strip /usr/lib/exim/exim-gris.so



-- I'm French. Sorry for my poor English.

--
Serge Demonchaux
/*
 * Copyright (C) 2009 by Serge Demonchaux
 *  <serge(DOT)d3(AT)free(DOT)fr>
 * $Id: exim-gris.c, v0.0.1 Jun 2009
 */
/**
- Build :
 % gcc -O2 -Wall -Werror -shared -fPIC -g \
    -I./exim-4.69/build-* \
    -L/usr/lib \
    -DLAST_UPDATE=60*15 \
    -DMAX_GREY=60*60*8 \
    -DRETRY_MAX=60*60*24*36 \
    -DRETRY_MIN=60*5 \
    -o /usr/lib/exim/exim-gris.so exim-gris.c \
    && strip /usr/lib/exim/exim-gris.so



- In RCPT or DATA :

 defer condition       = ${if ...}{1}{0}}
       set acl_m_gris  = ${dlfunc{/usr/lib/exim/exim-gris.so}{gris}}
       condition       = ${if >{$acl_m_gris}{0}}
       message         = Greylisting, please try again later 
                ($acl_m_gris secondes)



- To dump out the contents of the database :
% exim -be '${dlfunc{/usr/lib/exim/exim-gris.so}{dump}}'

- Or :
% perl -e 'use DB_File;tie my %h, "DB_File", $ARGV[0], 0, 0, $DB_HASH;print "total: ".keys(%h)."\n";untie %h;' /var/spool/exim/db/greylisting

*/

#include <string.h>
#include <errno.h>
#include <time.h>
#include <fcntl.h>
/* Exim */
#include "local_scan.h"
#include "dbstuff.h"
#include "dbfunctions.h"


#define K_LAST_UPDATE    "LAST_UPDATE" /* Nom de la clé du prochain */
                      /* temps de nétoyage */
#ifndef LAST_UPDATE
#  define LAST_UPDATE    60*15          /* Interval de l'auto nétoyage */
#endif
#ifndef MAX_GREY
#  define MAX_GREY    60*60*8          /* Temps max à l'état Grey avant */
#endif                      /* expiration */
#ifndef RETRY_MAX
#  define RETRY_MAX    60*60*24*36   /* Temps max à l'état White avant */
#endif                      /* expiration */
#ifndef RETRY_MIN
#  define RETRY_MIN    60*3          /* Temps iniciale du mécanisme */
#endif                      /* de grelistage */


#define DEBUG(x)    if ((debug_selector & (x)) != 0)


/* Structure des données enregistées
 * dans la base de données.
 * ( dans l'ordre ).
 * Cette structure reprend les consignes sur l'utilisation
 * des db avec Exim -> dbfunctions.h + dbstuff.h
 */
typedef struct {
    time_t       time_stamp;  /* timestamp dernier passage */
  /*************/
    time_t       first; /* timestamp enregistrement */
    unsigned int flag;  /* drapeau 'W' ou 'G' ( le drapeu
             * 'A' est réservé pour les clés
             * d'administration du programme */
    unsigned int count; /* Compteur des passages */ 
} GREY_CTX;



/* --- global --- */

/* temps des différents états */
static int retry_max = RETRY_MAX;
static int retry_min = RETRY_MIN;
static int max_grey = MAX_GREY;
/* db */
open_db dbblock, *dbm;
GREY_CTX *dbd;

int __greylisting_fn( unsigned char *recipient, char *relay_ip );
int __traverse_fn( int dump );


/* dump le contenu de la db dans un format humain
* de cette manière:
* exim -be '${dlfunc{/usr/lib/exim/exim-gris.so}{dump}}' > dump.txt
*/
int dump(void) {


    if ( ( dbm = dbfn_open( 
    US"greylisting", O_RDWR, &dbblock, TRUE ) ) == NULL ) {
    DEBUG(D_local_scan)
        fprintf( stderr, "greylisting database not available!\n");
    return -1;
    };


    /* Traverse la db et dump sur STDERR */
    if (  __traverse_fn( 1 ) != 0 )
    goto err;


    (void) dbfn_close( dbm );


    return 0;


    err: (void) dbfn_close( dbm );
    return -1;
}


/* defer condition     = ${if ...}{1}{0}}
 *       set acl_m_gris = ${dlfunc{/usr/lib/exim/exim-gris.so}{gris}}
 *       condition      = ${if >{$acl_m_gris}{0}}
 *       message        = Greylisting, please try again later \
 *                ($acl_m_gris secondes)
 */
int gris( unsigned char **yield, int argc, unsigned char *argv[] ) {


    char ipaddr[16];
    char *mask;
    int  delay = 0;



   /* ------ greylisting ------- */
    if ( ( dbm = dbfn_open( 
    US"greylisting", O_RDWR, &dbblock, TRUE ) ) == NULL ) {
    DEBUG(D_local_scan)
        debug_printf("greylisting database not available!\n");
    return OK;
    };


    DEBUG(D_local_scan) debug_printf("Using %s\n", EXIM_DBTYPE);


   /* --- construction de la requête ---
    * Elle prend la forme:
    * ip/sender/recipient.
    * L'ip est elle tronquée au dernier point,
    * par exemple : 192.168.0.10 est transformée en 192.168.0
    */
    strncpy( ipaddr, (sender_host_address)?
    (char*)sender_host_address : "127.0.0.1", sizeof( ipaddr ) );


    mask = ( strrchr( ipaddr, '.') ) ?
         strrchr( ipaddr, '.') :
        strrchr( ipaddr, ':');
    ipaddr[(int)(mask - ipaddr)] = '\0';


    /* acl_smtp_data */
    if ( expand_string(US"${if def:recipients}") != NULL ) {
    int i, ret;
    for ( i = 0; i < recipients_count; i++ ) {
        ret = __greylisting_fn( recipients_list[i].address, ipaddr );
        if ( ret > 0 )
        delay = ret;
    };
    } else {/* acl_smtp_rcpt */
    unsigned char *recipient;
    recipient = expand_string(US"$local_part@$domain");
    delay = __greylisting_fn( recipient, ipaddr );
    };


    (void) __traverse_fn( 0 );
    (void) dbfn_close( dbm );
    *yield = string_sprintf( "%d", delay );


    return OK;
}


/* clean db */
int __traverse_fn( int dump ) {

    EXIM_CURSOR *cursor;
    unsigned int total = 0, deleted = 0, white = 0, grey = 0;
    unsigned char *key;
    time_t now;
    int r;



    now = time( (time_t)NULL );
    key = (unsigned char*)K_LAST_UPDATE;


    if ( ( dbd = dbfn_read( dbm, key ) ) == NULL ) {
    dbd = store_get( sizeof( GREY_CTX ) );
    dbd->first = dbd->time_stamp = now;
    dbd->count = 0;
    dbd->flag  = 'A';
    };


    if ( dump == 0 && dbd->time_stamp + LAST_UPDATE > now )
    return 0;


    dbd->count += 1;
    if ( dbfn_write( dbm, key, dbd, sizeof(GREY_CTX) ) != 0 ) {
    DEBUG(D_local_scan)
        debug_printf("Failed: %s\n", strerror(errno));
    return -1;
    };


    if ( dump == 1 )
    fprintf( stderr, "#\n# Greylisting %s#\n", ctime(&now) );


    key = dbfn_scan( dbm, TRUE, &cursor);
    while ( key != NULL ) {


    dbd = dbfn_read( dbm, key );


    switch ( dbd->flag ) {
        case 'G': /* Grey */
        r = ( dbd->time_stamp + max_grey < now )? 1 : 0;
        break;
         case 'W': /* White */
        r = ( dbd->time_stamp + retry_max < now )? 1 : 0;
        break;
         case 'A': /* Admin key */
        r = 0;
        break;
         default: /* Inconnu */
        r = 1;
    };


    if ( r != 0 ) {
        (void) dbfn_delete( dbm, key );
        ++deleted;
    };
    if ( dump == 1 && r == 0 ) {
        white += (dbd->flag == 'W')? 1 : 0;
        grey  += (dbd->flag == 'G')? 1 : 0;
        fprintf( stderr, "{\nkey  = \"%s\"\n", key );
        fprintf( stderr, "\tflag  = %c\n", dbd->flag );
        fprintf( stderr, "\tlast  = %s", ctime(&dbd->time_stamp));
        fprintf( stderr, "\tfirst = %s", ctime(&dbd->first));
        fprintf( stderr, "\tcount = %u\n}\n", dbd->count );
    };
    ++total;
    key = dbfn_scan( dbm, FALSE, &cursor );
    };


    DEBUG(D_local_scan)
    debug_printf("total: %u deleted %u/%u  (White: %u|Grey: %u)\n", total-deleted, deleted, total, white, grey );


    if ( dump == 1 )
    fprintf( stderr, "\n# Total: %u (White: %u|Grey: %u)", total-deleted,  white, grey);


    return 0;
}


int __greylisting_fn( unsigned char *recipient, char *relay_ip ) {

    unsigned char query[256];
    time_t now;
    int delay;
    int r;



    now = time( (time_t)NULL );


    /* key: 'ip/sender/recipient' */
    if ( sender_address != NULL ) {
    (void) snprintf( (char*)query, sizeof( query ),
        "%s/%s/%s",
        relay_ip,
        sender_address,
        recipient );
    } else {
    (void) snprintf( (char*)query, sizeof( query ),
        "%s/MAILER-DAEMON@%s/%s",
        relay_ip,
        sender_host_name,
        recipient );
    };


    dbd = dbfn_read( dbm, query );
    /*  Validation des informations récupérées directement
     *  dans la structure GREY_CTX de 'g'
     */
    if ( dbd == NULL ) {/* initializing new key's data */
    dbd = store_get( sizeof( GREY_CTX ) );
    r = -1;
    } else if ( dbd->flag == 'W' ) {
    r = (dbd->time_stamp + retry_max < now)? -1 : 0;
    } else {//if ( dbd->flag == 'G' ) {
     r = (dbd->first + retry_min > now)? 1 ://encore grey
         (dbd->time_stamp + max_grey < now)? -1 : 0;//expiré
    };


    switch ( r ) {
    case 0:
        dbd->flag  = 'W';
        delay = 0;
        break;
    case 1:
        dbd->flag  = 'G';
        delay = (dbd->first + retry_min) - now;
        break;
    default:
        dbd->time_stamp = dbd->first = now;
        dbd->flag  = 'G';
        dbd->count = 0;
        delay = retry_min;
        DEBUG(D_local_scan)
        debug_printf("greylisting initializing new key's\n");
    };
    /* compteur */
    dbd->count += 1;


    if ( dbfn_write( dbm, query, dbd, sizeof(GREY_CTX)) != 0 ) {
    DEBUG(D_local_scan)
        debug_printf("Failed: %s\n", strerror(errno));
    } else {
    DEBUG(D_local_scan)
        debug_printf("greylisting db updated\n");
    };



    if ( delay > 0 )
    log_write(0, LOG_MAIN, "${dlfunc greylisting "
        "relay_ip=%s sender=<%s> recipient=<%s> delay=%dsecs",
        relay_ip,
        (sender_address)? (char*)sender_address : "MAILER-DAEMON",
        recipient, delay );


    return delay;
};