summaryrefslogtreecommitdiff
path: root/article.rest.txt
blob: 430e499254e94e620d30bd0ae113a7ee7c0b73a1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
.. -*- 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 <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 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 ``<host,'',to>`` 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/