If you run a mailserver you for sure know the more or less creative attempts of spammers to deliver to nonexistent email addresses: guessing local parts, changing (deliberately or by using broken scripts) harvested local parts, or just sending to random addresses. Some examples from yesterday:
2busenet-0402@comodo.priv.at (was gregor+usenet ...) 87.29.61.85@comodo.priv.at a48ff091@comodo.priv.at gregor.herrmannnn@comodo.priv.at (so many n's ...) thanksgiving@comodo.priv.at
At some point we (as in "the spam & mail departments of the
CUG", IOW: Bernd &
me) thought that we actually don't want to accept mails from
machines (mostly trojaned clients in botnets) which send to such
random addresses (or to explicit spamtraps).
Thesis: a machine that send mails to this kind of addresses is not
a legitimate MTA, probably has a very bad ham/spam ratio, & is
better blocked at the RCPT TO stage in the first place.
After some attempts with scripts that ran daily against the maillog & created plain text blacklist for exim we came up with a solution that
Our database looks like this:
CREATE TABLE cugrbl_host (
ip inet NOT NULL
);
CREATE TABLE cugrbl_incident (
id integer NOT NULL,
ip inet NOT NULL,
recipient character varying(128) NOT NULL,
sender character varying(128) NOT NULL,
datetime timestamp without time zone DEFAULT now() NOT NULL
);
CREATE SEQUENCE cugrbl_indicent_id_seq
INCREMENT BY 1
NO MAXVALUE
NO MINVALUE
CACHE 1;
CREATE TABLE cugrbl_pattern (
id integer NOT NULL,
condition character varying(256) NOT NULL
);
CREATE SEQUENCE cugrbl_pattern_id_seq
INCREMENT BY 1
NO MAXVALUE
NO MINVALUE
CACHE 1;
ALTER TABLE cugrbl_incident ALTER COLUMN id SET DEFAULT nextval('cugrbl_indicent_id_seq'::regclass);
ALTER TABLE cugrbl_pattern ALTER COLUMN id SET DEFAULT nextval('cugrbl_pattern_id_seq'::regclass);
ALTER TABLE ONLY cugrbl_host
ADD CONSTRAINT cugrbl_host_pkey PRIMARY KEY (ip);
ALTER TABLE ONLY cugrbl_incident
ADD CONSTRAINT cugrbl_indicent_pkey PRIMARY KEY (id);
ALTER TABLE ONLY cugrbl_pattern
ADD CONSTRAINT cugrbl_pattern_pattern_key UNIQUE (condition);
ALTER TABLE ONLY cugrbl_pattern
ADD CONSTRAINT cugrbl_pattern_pkey PRIMARY KEY (id);
CREATE INDEX cugrbl_incident_datetime_index ON cugrbl_incident USING btree (datetime);
You may want to tweak the length of cugrbl_incident.{recipient,sender} by looking up the allowed length of email addresses in RfC 2821 or by using your own experience/log files.
cugrbl_pattern.conditions contains email addresses like the ones mentioned above & has to be filled manually. The field is later used with the LIKE operator in a WHERE clause & may therefore contain the % wildcard character. — For those who don't like either psql or phppgadmin the following shell script makes inserting more convenient:
#!/bin/sh
for a in "$@"; do
echo "trying $a ..."
echo "insert into cugrbl_pattern (condition) VALUES ('$a');" | \
psql -h localhost -U exim exim
done
Exim can "talk" directly to the postgresql database, & in our setup it:
First we set up the connection & define a few macros:
/etc/exim4/conf.d/main/10_exim4-config_cugrbl
# define our database connection
# local database accessed via a UNIX socket
hide pgsql_servers = (/var/run/postgresql/.s.PGSQL.5432)/exim/exim/PASSWORD
# local or remote database accessed via the network
# hide pgsql_servers = localhost/exim/exim/PASSWORD
SELECT_CONDITION = SELECT COUNT(condition) FROM cugrbl_pattern WHERE '${quote_pgsql:$local_part@$domain}' LIKE condition
SELECT_IP = SELECT COUNT(ip) FROM cugrbl_host WHERE ip='${quote_pgsql:$sender_host_address}'
INSERT_HOST = INSERT INTO cugrbl_host(ip) VALUES ('${quote_pgsql:$sender_host_address}')
INSERT_INCIDENT = INSERT INTO cugrbl_incident (ip, sender, recipient) VALUES ('${quote_pgsql:$sender_host_address}','${quote_pgsql:$sender_address}','${quote_pgsql:$local_part@$domain}')
Then comes the actual work of checking/writing/warning/rejecting:
/etc/exim4/conf.d/acl/30_exim4-config_check_rcpt
[..]
# get variables
warn set acl_m1 = ${lookup pgsql{SELECT_CONDITION}{$value}{2}}
warn set acl_m2 = ${lookup pgsql{SELECT_IP}{$value}{2}}
# cond & ip
warn message = SELECT-CONDITION & SELECT-IP - Spamtrap-Entry: IP ($sender_host_address) already known, SMTP-Tokens added, see https://info.colgarra.priv.at/cugrbl/
condition = ${if eq{$acl_m1}{1}}
condition = ${if eq{$acl_m2}{1}}
condition = ${lookup pgsql{INSERT_INCIDENT}{yes}{no}}
# cond & ! ip
warn message = SELECT-CONDITION & ! SELECT-IP - Spamtrap-Entry: IP ($sender_host_address) added to cUGrbl and SMTP-Tokens added, see https://info.colgarra.priv.at/cugrbl/
condition = ${if eq{$acl_m1}{1}}
condition = ${if eq{$acl_m2}{0}}
condition = ${lookup pgsql{INSERT_HOST}{yes}{no}}
condition = ${lookup pgsql{INSERT_INCIDENT}{yes}{no}}
# check ip - warn
warn message = X-Warning: $sender_host_address listed at CUGRBL (spamtrap), see https://info.colgarra.priv.at/cugrbl/
log_message = $sender_host_address listed at CUGRBL (spamtrap), see https://info.colgarra.priv.at/cugrbl/
condition = ${if eq{$acl_m2}{1}}
# check ip - reject
deny message = We don't accept mail from $sender_host_address, which is locally blacklisted due to a spamtrap. For further information contact postmaster@$domain or visit https://info.colgarra.priv.at/cugrbl/
condition = ${if eq{$acl_m2}{1}}
!senders = :
domains = CONFDIR/domains/deny_cugrbl
[..]
We don't want to fill the cugrbl_host table ad infinitum, & we want to give sending machines the chance to get out of the blacklist. The following perl script removes entries from this table after one month of no incident:
/etc/cron.daily/removeoldspamtrapips
#!/usr/bin/perl
use DBI;
use strict;
my $dbh = DBI->connect("dbi:Pg:dbname=exim", "exim", "PASSWORD");
my $sth = $dbh->do("DELETE FROM cugrbl_host WHERE ip NOT IN (SELECT DISTINCT ip from cugrbl_incident WHERE datetime > now() - interval '1 month');");
gregoa, 2007-12-25, 2008-01-09, 2008-03-06
Thanks to Bernd for his feedback on this documentation.
