.. -*- 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`_. I sorgenti ---------- Potete guardare i sorgenti sul mio Trac_: http://thenautilus.dyndns.org/trac/browser/Graylister/trunk/ .. _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/ .. _Trac: http://trac.edgewall.com/