| |||
| © Perl Mongers Italia. Tutti i diritti riservati. | |||
/* :Author: David Goodger :Contact: goodger@users.sourceforge.net :Date: $Date: 2004/12/22 19:08:26 $ :Version: $Revision: 1.46 $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. */ /* "! important" is used here to override other .last { margin-bottom: 0 ! important } .hidden { display: none } a.toc-backref { text-decoration: none ; color: black } blockquote.epigraph { margin: 2em 5em ; } dl.docutils dd { margin-bottom: 0.5em } /* Uncomment (and remove this text!) to get bold-faced definition list terms dl.docutils dt { font-weight: bold } */ div.abstract { margin: 2em 5em } div.abstract p.topic-title { font-weight: bold ; text-align: center } div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { margin: 2em ; border: medium outset ; padding: 1em } div.admonition p.admonition-title, div.hint p.admonition-title, div.important p.admonition-title, div.note p.admonition-title, div.tip p.admonition-title { font-weight: bold ; font-family: sans-serif } div.attention p.admonition-title, div.caution p.admonition-title, div.danger p.admonition-title, div.error p.admonition-title, div.warning p.admonition-title { color: red ; font-weight: bold ; font-family: sans-serif } /* Uncomment (and remove this text!) to get reduced vertical space in compound paragraphs. div.compound .compound-first, div.compound .compound-middle { margin-bottom: 0.5em } div.compound .compound-last, div.compound .compound-middle { margin-top: 0.5em } */ div.dedication { margin: 2em 5em ; text-align: center ; font-style: italic } div.dedication p.topic-title { font-weight: bold ; font-style: normal } div.figure { margin-left: 2em } div.footer, div.header { font-size: smaller } div.line-block { display: block ; margin-top: 1em ; margin-bottom: 1em } div.line-block div.line-block { margin-top: 0 ; margin-bottom: 0 ; margin-left: 1.5em } div.sidebar { margin-left: 1em ; border: medium outset ; padding: 1em ; background-color: #ffffee ; width: 40% ; float: right ; clear: right } div.sidebar p.rubric { font-family: sans-serif ; font-size: medium } div.system-messages { margin: 5em } div.system-messages h1 { color: red } div.system-message { border: medium outset ; padding: 1em } div.system-message p.system-message-title { color: red ; font-weight: bold } div.topic { margin: 2em } h1.title { text-align: center } h2.subtitle { text-align: center } hr.docutils { width: 75% } ol.simple, ul.simple { margin-bottom: 1em } ol.arabic { list-style: decimal } ol.loweralpha { list-style: lower-alpha } ol.upperalpha { list-style: upper-alpha } ol.lowerroman { list-style: lower-roman } ol.upperroman { list-style: upper-roman } p.attribution { text-align: right ; margin-left: 50% } p.caption { font-style: italic } p.credits { font-style: italic ; font-size: smaller } p.label { white-space: nowrap } p.rubric { font-weight: bold ; font-size: larger ; color: maroon ; text-align: center } p.sidebar-title { font-family: sans-serif ; font-weight: bold ; font-size: larger } p.sidebar-subtitle { font-family: sans-serif ; font-weight: bold } p.topic-title { font-weight: bold } pre.address { margin-bottom: 0 ; margin-top: 0 ; font-family: serif ; font-size: 100% } pre.line-block { font-family: serif ; font-size: 100% } pre.literal-block, pre.doctest-block { margin-left: 2em ; margin-right: 2em ; background-color: #eeeeee } span.classifier { font-family: sans-serif ; font-style: oblique } span.classifier-delimiter { font-family: sans-serif ; font-weight: bold } span.interpreted { font-family: sans-serif } span.option { white-space: nowrap } span.option-argument { font-style: italic } span.pre { white-space: pre } span.problematic { color: red } table.citation { border-left: solid thin gray } table.docinfo { margin: 2em 4em } table.docutils { margin-top: 0.5em ; margin-bottom: 0.5em } table.footnote { border-left: solid thin black } table.docutils td, table.docutils th, table.docinfo td, table.docinfo th { padding-left: 0.5em ; padding-right: 0.5em ; vertical-align: top } th.docinfo-name, th.field-name { font-weight: bold ; text-align: left ; white-space: nowrap } h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { font-size: 100% } tt.docutils { background-color: #eeeeee } ul.auto-toc { list-style-type: none }
Un po' di storiaNon 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 <troppi> 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 contornoAvendo 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 mioMa cos'è, esattamente, che voglio ottenere?
Detto così sembra anche semplice, ma come lo spieghiamo alla macchina? Questa è la subroutine principale del mio "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; } Come si vede, con un minimo di occhio allo stile, si può scrivere codice Perl chiaro e leggibile. I dettagliOvviamente 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 databasePer tenere traccia dei tentativi, e della white list, ho usato un database SQLite, tramite DBI e DBD::SQLite. Contiene 3 tabelle:
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 <host,'',to> non identifica bene un messaggio. Le black listPer 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 ho fatto un branch locale, rinominandolo DAKKAR::Net::DNSBLLookup (sì, prima o poi manderò una patch all'autore). Interazioni con l'esternoInfine, dobbiamo preoccuparci dell'input e dell'output del programma rispetto a netqmail. Per quanto riguarda l'output, abbiamo due funzioni:
Per l'input, basterebbe leggere qualche variabile di ambiente (vedi get_from_env):
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 scriptPer 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 sorgentiPotete guardare i sorgenti sul mio Trac: http://thenautilus.dyndns.org/trac/browser/Graylister/trunk/ | |||