From 24ee74f4b98c3773a4fc64225f6f9e719f9c1ac8 Mon Sep 17 00:00:00 2001 From: dakkar Date: Thu, 27 Dec 2007 14:37:02 +0000 Subject: r3307@rfc-1918: dakkar | 2007-12-27 15:36:13 +0100 almost finished first draft --- article.rest.txt | 310 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) diff --git a/article.rest.txt b/article.rest.txt index bcf69f5..ffa0d5a 100644 --- a/article.rest.txt +++ b/article.rest.txt @@ -1,6 +1,316 @@ +.. -*- mode: rst; coding: utf-8 -*- + =============================================== ``Graylister`` - proviamo a fermare lo "SPAM" =============================================== :Author: Gianni Ceccarelli :Id: $Id $ +Un po' di storia +================ + +Non provo neppure a spiegare cosa si intende con "spam" parlando di +posta elettronica. Se non lo sapete, vuol dire che non avete usato la +posta elettronica negli ultimi anni, per cui questo articolo +non vi interessa ``:-)``. + +Ci sono svariati metodi per limitare la quantità di spazzatura che +arriva nella nostra casella postale. Molti, tra cui SpamAssassin_ che +è scritto in Perl ed è molto diffuso, analizzano il contenuto di +ciascun messaggio, cercando di indovinare la probabilità che sia da +cestinare; funzionano piuttosto bene, ma sono un po' pesanti dal punto +di vista computazionale: finché avete un solo account di cui +preoccuparvi, vanno bene, ma se dovete gestire un intero dominio, +potrebbe essere il caso di aggiungere qualche altro sistema di +filtraggio. + +Un sistema che ha avuto discreta popolarità fino a non molto tempo fa +è il `black listing`: si prendono delle liste di indirizzi IP +considerati "inaffidabili", e si rifiutano tutti i messaggi +provenienti da essi. Semplice, ma un po' troppo drastico, specialmente +perché è molto più facile finire per sbaglio in una di quelle liste +che farsi togliere. + +Di recente è stato inventato il `gray listing`, come forma meno +drastica del `black listing`: invece che rifiutare il messaggio, si +segnala un errore temporaneo, e se il mittente riprova l'invio, si +accetta. Il senso può non essere molto ovvio, per cui è bene spiegare +meglio. + +Un server di posta serio gestisce una coda di messaggi da inviare. Per +ciascun messaggio in coda, tenta l'invio contattando il gestore posta +responsabile per l'indirizzo del destinatario. Se tale gestore accetta +il messaggio, bene, fine del lavoro. Se lo rifiuta, si può segnalare +il problema al mittente. Ma, in alcuni casi, il gestore destinatario +potrebbe essere non raggiungibile, o segnalare qualche errore +temporaneo (ovvero che, nell'opinione del gestore destinatario, +dovrebbe risolversi tra un po' tempo). In questi ultimi casi, il +gestore mittente rimette il messaggio in coda, e ritenta l'invio dopo +qualche tempo. Però (e qui sta tutto il "succo" del `gray listing`) +molti spammer non usano gestori di posta seri, ma usano programmini +specializzati nell'inviare il maggior numero possibile di messaggi nel +minor tempo possibile. Questi sistemi non gestiscono una coda, per cui +trattano un errore temporaneo come un errore permanente: ignorando il +problema, e non ritentando l'invio. + +Per cui, se il nostro gestore destinatario costringe ciascun mittente +a tentare l'invio due volte, non accetterà mai messaggi da sistemi di +invio stupidi, e eliminerà così la maggior parte dello spam. D'altra +parte, se qualcuno ha un gestore posta serio che esce da un indirizzo +"sospetto" secondo qualche `black list`, un sistema di `gray listing` +accetterà i suoi messaggi (l'utilità di ciò è discutibile, ma almeno +abbiamo la possibilità di scegliere come trattare ciascun caso). + +Il contorno +=========== + +Avendo di recente spostato il mio dominio su un server dedicato, mi +sono trovato a dover configurare il server di posta elettronica (oltre +a tutto il resto). Ho scelto netqmail_, principalmente perché l'avevo +già usato e quindi ho un'idea di come si configuri. Dopo aver notato +la non trascurabile quantità di spam che entrava, mi sono messo a +cercare un sistema di `gray listing` da incastrare nel server. Ne ho +trovati parecchi, ma nessuno faceva proprio quel che volevo, per cui +mi sono ispirato alle caratteristiche migliori di ciascuno, e ne ho +scritto uno a modo mio (ah, il bello del software libero!). + +Per incastrarlo dentro netqmail_, ho usato il meccanismo detto +`qmail-spp`_, che permette di chiedere a ``qmail-smtpd`` di invocare un +programma esterno in certe fasi del protocollo SMTP_. In particolare, +mi farebbe comodo agire con più informazioni possibile; siccome non +voglio leggermi l'intero messaggio, mi limiterò a usare le +informazioni presenti sulla "busta": indirizzo IP della macchina +mittente, indirizzo di posta del mittente, e indirizzo di posta del +destinatario. + +A modo mio +========== + +Ma cos'è, esattamente, che voglio ottenere? + +1) `Gray listing` sulla maggior parte dei messaggi +2) Se un server si dimostra capace di passare il `gray listing` e + *non* compare in alcuna `black list`, viene aggiunto a una `white + list` e non subirà più filtraggio + +Semplice, no? Come lo spieghiamo alla macchina? Questa è la subroutine +principale del mia "graylister":: + + sub check { + my ($host,$from,$to)=@_; + + remove_old_attempts(); + if (is_whitelisted($host)) { + accept_message();return; + } + if (is_second_attempt($host,$from,$to)) { + if (is_blacklisted($host)) { + cleanup_attempt($host,$from,$to); + accept_message();return; + } + else { + add_to_whitelist($host); + accept_message();return; + } + } + record_first_attempt($host,$from,$to); + reject_temporarily($from); + return; + } + +Andiamo riga per riga. La sub prende 3 parametri: l'indirizzo IP del +server che sta cercando di mandarci un messaggio, l'indirizzo di posta +da cui dice di provenire il messaggio, e l'indirizzo di posta a cui +sarebbe destinato:: + + sub check { + my ($host,$from,$to)=@_; + +``remove_old_attempts`` serve per tenere pulito il piccolo database +che uso per tenere traccia dei tentativi di invio. + +Se il server mittente è nella `white list`, accetto il messaggio e +termino l'elaborazione:: + + if (is_whitelisted($host)) { + accept_message();return; + } + +Se è la seconda volta che ricevo lo stesso messaggio, e il server +mittente sta in qualche `black list`, pulisco la traccia del tentativo +e accetto il messaggio; se il server non sta in nessuna `black list`, +lo considero definitivamente affidabile, e comunque accetto il +messaggio:: + + if (is_second_attempt($host,$from,$to)) { + if (is_blacklisted($host)) { + cleanup_attempt($host,$from,$to); + accept_message();return; + } + else { + add_to_whitelist($host); + accept_message();return; + } + } + +Se arrivo a questo punto, vuol dire che il messaggio è un primo +tentativo da parte di un server non dichiarato affidabile: tengo +traccia del tentativo, e segnalo errore temporaneo:: + + record_first_attempt($host,$from,$to); + reject_temporarily($from); + return; + } + +Semplice, no? + +I dettagli +========== + +Ovviamente quella subroutine, da sola, non ha speranza di +funzionare. Pur senza mostrare tutto il codice, è opportuno scendere +un po' nei dettagli delle varie funzioni usate. + +Il database +----------- + +Per tenere traccia dei tentativi, e della `white list`, ho usato un +database SQLite_, tramite DBI_ e `DBD::SQLite`_. Contiene 3 tabelle: + +``version(version integer)`` + non strettamente necessaria, ma la metto per segnare la versione del + programma che ha creato il database, in modo da poter prevedere + cambiamenti strutturali in future versioni con aggiornamento + automatico dei dati + +``whitelist(host varchar)`` + la lista dei server dichiarati affidabili; ogni record contiene + un indirizzo IP in forma "dotted decimal" + +``attempts(host varchar,smtpfrom varchar,smtprcpt varchar,time integer)`` + in questa tabella vengono inseriti i tentativi di invio; il + timestamp viene usato per pulire i tentativi vecchi per cui abbiamo + perso ogni speranza (ovvero, se dopo un giorno non ci ha riprovato, + non ci riproverà più) + +Le funzioni ``add_to_whitelist``, ``is_whitelisted``, +``record_first_attempt``, ``is_second_attempt``, ``cleanup_attempt`` e +``remove_old_attempts`` usano semplici comandi SQL per manipolare il +database. Per rendere trasparente l'uso del database alla funzione +chiamante, ciascuna di queste funzioni invoca ``_init_dbh``, la quale +si preoccupa di aprire il database (se non è già stato fatto in questa +sessione) e eventualmente crearvi le tabelle (ovviamente solo al +momento della creazione, usando ``_prepare_db``). + +``cleanup_attempt`` ha una riga di logica in più: invece di cancellare +il record del tentativo di invio, si limita ad aggiornare il +timestamp. In questo modo, se il server mittente (sospetto) spedisce +spesso per la stessa coppia mittente-destinatario, si evita di +rallentare tutti gli invii (nota: questo potrebbe però aiutare gli +spammer, forse eliminerò questo comportamento). In alcuni casi il +mittente "sulla busta" potrebbe essere vuoto (in caso di bounce, ad +esempio): in questi casi si cancella il record del tentativo, in +quanto la terna ```` non identifica bene un messaggio. + +Le `black list` +--------------- + +Per controllare se l'indirizzo IP del mittente stia in qualche `black +list` ho usato il modulo `Net::DNSBLLookup`_. Siccome però ha qualche +stranezza (l'elenco delle liste non è molto aggiornato, non +restituisce tutte le informazioni che potrebbe), ne ridefinisco al +volo alcune parti (sì, prima o poi manderò una patch all'autore). + +Interazioni con l'esterno +------------------------- + +Infine, dobbiamo preoccuparci dell'input e dell'output del programma +rispetto a netqmail_. + +Per quanto riguarda l'output, abbiamo due funzioni: + +``accept_message`` + non fa niente: se il programma non scrive nulla su ``STDOUT``, + ``qmail-smtpd`` accetterà il messaggio + +``reject_temporarily`` + chiede a ``qmail-smtpd`` di rispondere con codice 451 ("temporary + failure", appunto); lo fa subito nella maggior parte dei casi, ma se + il mittente è vuoto o comincia per ``postmaster@`` (ovvero, se + sembra essere un altro server di posta), segnala l'errore solo dopo + aver ricevuto il messaggio (altrimenti il mittente potrebbe + offendersi e farci finire in qualche `black list`). + +Per l'input, basterebbe leggere qualche variabile di ambiente (vedi +``get_from_env``): + +``TCPREMOTEIP`` + indirizzo IP del server mittente, impostato da ``tcpserver`` + +``SMTPMAILFROM`` + indirizzo di posta del mittente sulla busta, impostato da + ``qmail-smtpd`` + +``SMTPRCPTTO`` + indirizzo di posta del destinatario sulla busta, impostato da + ``qmail-smtpd`` + +``DAKGL_DBNAME`` + path al database da usare, impostato in qualche modo (nel mio caso, + da ``tcpserver`` tramite apposita configurazione) + +Se però si leggono con attenzione altri programmi di `gray listing`, +si scopre che serve un minimo di codice in più: EZMLM_, noto e diffuso +programma per la gestione delle `mailing list`, cambia il mittente ad +ogni invio, inserendoci un numero variabile (usato per scopi +interni). Siccome non vogliamo ritardare ciascun messaggio di una +`mailing list` (specie se, come me, siete iscritti a parecchie), +sostituiamo il numero con un marcatore costante (in +``_cleanup_data``). + +Gli script +---------- + +Per invocare il tutto, ho scritto un piccolo script:: + + #!/usr/bin/perl + use DAKKAR::Graylister; + + exit 0 unless (defined $ENV{GRAYLISTING}) and (!defined $ENV{RELAYCLIENT}); + + DAKKAR::Graylister::check(DAKKAR::Graylister::get_from_env()); + +La riga che comincia con ``exit`` permette di escludere l'elaborazione +tramite apposite variabili di ambiente. In particolare, +``GRAYLISTING`` deve essere definita, e il server mittente non deve +essere già abilitato al `relay` (ovvero a inviare posta a chiunque, +non solo agli indirizzi gestiti direttamente da questo server): se è +abilitato al `relay`, si suppone che sia totalmente fidato, per cui è +inutile filtrarlo (se permettete il `relay` a macchine non fidate, +avete ben altri problemi, e meritate tutto il male che ve ne può +derivare). + +Siccome, per questioni di pulizia e robustezza, dedico a ciascuna +applicazione Perl la sua directory con i moduli che servono installati +appositamente, serve un altro script che imposti ``PERL5LIB`` per +permettere al compilatore di trovare i moduli:: + + #!/bin/bash + + export PERL5LIB='/usr/local/graylist/lib/perl5:/usr/local/graylist/lib/perl5/x86_64-linux-thread-multi' + + exec /usr/local/graylist/bin/dakkar-graylister + +Per costruire queste "librerie dedicate", consiglio l'uso di +`local::lib`_. + +.. _SpamAssassin: http://spamassassin.apache.org/ +.. _EZMLM: http://www.ezmlm.org/ +.. _`qmail-spp`: http://qmail-spp.sourceforge.net/ +.. _SMTP: http://www.faqs.org/rfcs/rfc2821.html +.. _SQLite: http://www.sqlite.org/ +.. _DBI: http://search.cpan.org/~timb/DBI/ +.. _`DBD::SQLite`: http://search.cpan.org/~msergeant/DBD-SQLite/ +.. _`Net::DNSBLLookup`: http://search.cpan.org/~tjmather/Net-DNSBLLookup/ +.. _netqmail: http://netqmail.org/ +.. _`local::lib`: http://search.cpan.org/~apeiron/local-lib/ -- cgit v1.2.3