Simple Email Blacklist Using Spamassassin Plugin And RBLDNSD

This is not new idea, actually. someone at spamassassin plugin developers have been made before. basically, the idea was put email addresses in RBLDNSD zone dnset format, ie:

Email
user@example.com

RBLDNSD
user.example.com

So, we replace @ sign into dot (.) sign. that way, we can save the email addresses into the RBLDNSD dnset zone.

RBLDNSD part:

create emailbl zone, meta information

zone

:127.0.0.4:DNSBL. $ - Not receiving email right now.
example.user.gmail.com
example.user.rediffmail.com

meta

$NS 1w ns.example.com ns.example.com
$SOA 1w ns.example.com admin.example.com 0 2h 2h 1w 1h
$DATASET generic @
@ A 1.2.3.4
@ MX 10 mx.example.com
@ TXT "example email blocklist"

in /etc/sysconfig/rbldnsd

RBLDNSD="dsbl -r/var/lib/rbldns/dsbl -t 300 -b 1.2.3.4 \
emailbl.example.com:combined:meta,emailbl
"

ofcourse we should delegate the subdomain emailbl.example.com in example.com authoritative nameserver

; subdomain delegation
emailbl.example.com.	IN NS ns.example.com.
ns.example.com.			IN A 1.2.3.4

start rbldnsd service

service rbldnsd start

Nginx As imap4/pop3 Proxy Using Apache As Auth Server Backend

It’s been a long times since i wrote my last article, i’ve been bussy with real life things.
As usual, I’ll get right to the subject of how to configure nginx as POP3/IMAP proxy server.

Nginx IP                 = 192.168.1.1
Postfix User Database IP = 192.168.1.5 (postfix + courier server + apache backend)

nginx server configuration

mail {
    server_name mail-proxy.example.com;
    # apache external backend
    auth_http  192.168.1.5:8081/auth.php;
    proxy  on;
    proxy_pass_error_message on;

    imap_capabilities "IMAP4rev1" "UIDPLUS" "IDLE" "LITERAL +" "QUOTA";

    pop3_auth plain apop cram-md5;
    pop3_capabilities "LAST" "TOP" "USER" "PIPELINING" "UIDL";

    ssl_certificate /etc/nginx/ssl_keys/db.mail-proxy.crt;
    ssl_certificate_key /etc/nginx/ssl_keys/db.mail-proxy.key;
    ssl_session_timeout 5m;
    ssl_protocols SSLv2 SSLv3 TLSv1;
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
    ssl_prefer_server_ciphers on;

    server {
      listen      143;
      protocol    imap;
      starttls    on;
      auth_http_header X-Auth-Port 143;
      auth_http_header User-Agent "Nginx POP3/IMAP4 proxy";
    }

    server {
      protocol    pop3;
      listen      110;
      starttls    on;
      pop3_auth   plain;
      auth_http_header X-Auth-Port 110;
      auth_http_header User-Agent "Nginx POP3/IMAP4 proxy";
    }

    server {
      listen      993;
      ssl         on;
      protocol    imap;
      auth_http_header X-Auth-Port 993;
      auth_http_header User-Agent "Nginx POP3/IMAP4 proxy";
    }

    server {
      protocol    pop3;
      listen      995;
      ssl         on;
      pop3_auth   plain;
      auth_http_header X-Auth-Port 995;
      auth_http_header User-Agent "Nginx POP3/IMAP4 proxy";
    }
}

Postfix Changing Outgoing IP By Time Interval Using TCP_TABLE And Perl

Someone asked me if i can make a perl scripts that can change the ip address based on time interval, say he want ip address 1.2.3.4 used within one hour, if done next ip address will be used within next one hour..and so on. when it came to highest number of ip address in array, they will be reset back to the start. first i suggest him to look at the articles i wrote. But then i decide to write Perl script which was made for the purposes mention above.

here we are..

Postfix section:

master.cf
127.0.0.1:2527 inet  n       n       n       -       0      spawn
          user=nobody argv=/etc/postfix/ip_by_time.pl

ip1  unix -       -       n       -       -       smtp
          -o syslog_name=postfix-ip1
          -o smtp_helo_name=smtp1.example.com
          -o smtp_bind_address=1.2.3.1

ip2  unix -       -       n       -       -       smtp
          -o syslog_name=postfix-ip2
          -o smtp_helo_name=smtp2.example.com
          -o smtp_bind_address=1.2.3.2

ip3  unix -       -       n       -       -       smtp
          -o syslog_name=postfix-ip3
          -o smtp_helo_name=smtp3.example.com
          -o smtp_bind_address=1.2.3.3

ip4  unix -       -       n       -       -       smtp
          -o syslog_name=postfix-ip4
          -o smtp_helo_name=smtp4.example.com
          -o smtp_bind_address=1.2.3.4
....
....

main.cf

transport_maps = tcp:[127.0.0.1]:2527
127.0.0.1:2527_time_limit = 3600s

Postfix, Rate Limiting Inbound Emails Using SenderScore And Memcache

I received email from someone fiew days ago, he directed me to an article about senderscore and and asked if I could make it usable. Actually, I’m not very familiar with how senderscore work. I’ve read the article and see the FAQ at https://senderscore.org/. I have found that senderscore can be queried with a format like this:

reversed.ip.address.score.senderscore.com

Ie, I want to know the score value of ip address 202.127.97.97, the format of the query would be like this:

$ dig a 97.97.127.202.score.senderscore.com +short
127.0.4.75

Look at the answers given by senderscore’s NS. last octet is the score of the ip address 202.127.97.97, which scored 75.

Excerpts from senderscore faq:

All scores are based on a scale of 0 to 100, where 0 is the worst, and 100 is the best possible score. A score represents that IP address’s rank as measured against other IP addresses, much like a percentile ranking.

Now back to the article, The authors make a perl module that can perform queries to senderscore ns, put a “reputation score” into memcache, at the same time, calculating how many times an ip address connected to our smtp.

Let’s begin, first of all download Policy::Memcache from this git repository 
Create a working directory, and extract the tarball.

$ mkdir pol-mem && cd pol-mem
$ tar --extract --file=petermblair-libemail-f73612c.tar.gz petermblair-libemail-f73612c/perl/senderscore/memcache/
$ mv petermblair-libemail-f73612c/perl/senderscore/memcache/* .

Postfix, Omar Kilani’s Memcache Patch Try-Out

I was rewrote Omar Kilani’s memcache patch couple of weeks ago. But that was not tested due to lack of time and unavailability of servers that can be used.

Now, i got chance to implement simple test. This is my configuration:
main.cf

smtpd_recipient_restrictions =
   ...
   ...
   check_recipient_access memcache:/etc/postfix/memcache.cf,
   ...
   ...

memcache.cf

servers = localhost:11211
key_format = %s

Entry on memcache

spam@example.com	REJECT	not allowed

Query using postmap

$ postmap -q "spam@example.com" memcache:/etc/postfix/memcache.cf
postmap: dict_memcache_lookup: using key_format '%s'
postmap: plmemcache_get: fetching key spam@example.com from memcache
postmap: plmemcache_get: key spam@example.com =>; REJECT not allowed
postmap: dict_memcache_lookup: spam@example.com returned REJECT not allowed
REJECT not allowed

A little bit too verbose i guess, but it can be adjusted by modifying source code.

Geo Location DNSBL Using Perl, Memcached And GeoIP

In my last article about the DNSBL and memcached, I wrote how to use memcached to store the data for the DNSBL. It led me to other new ideas to make the Geo Location DNSBL. In previous article I put data into memcached with the following format:

_prefix_ip 127.0.0.2-10
_prefix_ = _a_ or _txt_
ip = ip address to blacklist
Example:
_a_192.168.1.1	127.0.0.3
_txt_192.168.1.1	Blacklisted

Each ip address has a pair of _a_ and _txt_ record.
For This experiment records will be store in following format:

_prefix_country_code 127.0.0.2-10
_prefix_ = _a_ or _txt_
country_code = country code to blacklist
Example:
_a_ID	127.0.0.3
_txt_ID	Blacklisted

dnsbl-geo.pl Perl script

#!/usr/bin/perl
use Net::DNS::Nameserver;
use Cache::Memcached;
use Geo::IP;
use strict;
use warnings;

our $our_dnsbl = ".dnsbl.example.com";

# Configure the memcached server
my $memd = new Cache::Memcached {
            'servers' => [ '127.0.0.1:11211' ],
};

my $gi = Geo::IP->new(GEOIP_STANDARD);

#sub reverse_ipv4 {
#        my $ip = $_[0];
#        my ($a1, $a2, $a3, $a4) = split /\./, $ip;
#        my $reversed_ipv4 = join('.', $a4,$a3,$a2,$a1);
#        return $reversed_ipv4;
#}

sub reverse_ipv4 {
        my @ips = split /\./, $_[0];
        my @r;
        push @r, pop @ips while @ips;
        return join('.', @r);
}

sub strip_domain_part {
        my $strip_domain = $_[0];
        $strip_domain =~ s/$our_dnsbl//ig;
        return $strip_domain;
}

sub reply_handler {
        my ($qname, $qclass, $qtype, $peerhost,$query,$conn) = @_;
        my ($rcode, @ans, @auth, @add);
        my ($memc_a_val, $memc_txt_val);

        #print "Received query from $peerhost to ". $conn->{"sockhost"}. "\n";
        #$query->print;

        my $striped_domain = strip_domain_part($qname);
        my $reverse_striped_domain = reverse_ipv4($striped_domain);

        my $country_code = $gi->country_code_by_addr($reverse_striped_domain);

        if ($qtype eq "A" && $qname eq $striped_domain . $our_dnsbl) {
                my $vmemc_a_val = sprintf("_a_%s", lc($country_code));
                $memc_a_val = $memd->get($vmemc_a_val);
                if (defined($memc_a_val)) {
                        my ($ttl, $rdata) = (86400, $memc_a_val);
                        push @ans, Net::DNS::RR->new("$qname $ttl $qclass $qtype $rdata");
                        $rcode = "NOERROR";
                }
        } elsif ( $qname eq "dnsbl.example.com" ) {
                $rcode = "NOERROR";

        } else {
                $rcode = "NXDOMAIN";
        }

        if ($qtype eq "TXT" && $qname eq $striped_domain . $our_dnsbl) {
                my $vmemc_txt_val = sprintf("_txt_%s", lc($country_code));
                $memc_txt_val = $memd->get($vmemc_txt_val);
                if (defined($memc_txt_val)) {
                        my ($ttl, $rdata) = (86400, $memc_txt_val);
                        push @ans, Net::DNS::RR->new("$qname $ttl $qclass $qtype $rdata");
                        $rcode = "NOERROR";
                }
        } elsif ( $qname eq "dnsbl.example.com" ) {
                $rcode = "NOERROR";

        } else {
                $rcode = "NXDOMAIN";
        }

        # mark the answer as authoritive (by setting the 'aa' flag
        return ($rcode, \@ans, \@auth, \@add, { aa => 1 });
        $memd->disconnect_all();
}

my $ns = Net::DNS::Nameserver->new(
     LocalAddr    => "192.168.200.18",
     LocalPort    => 5353,
     ReplyHandler => \&reply_handler,
     Verbose      => 0,
) || die "couldn't create nameserver object\n";

$ns->main_loop;

DNSBL Using Perl And Memcached

DNSBL is a DNS based blackhole list, which can be used as countermeasure against unsolicited mail spam. One of the most efficient ways to block mail spam is to do it on SMTP conversation stage by denying incoming connects from spam sources, where the source machine is identified by its IP address which is checked against one or more DNSBLs on the fly.

I wrote a very simple perl script, which mimicked the way the original DNSBL works. However, I’ve created this script just respond to A and TXT records. But that’s the basic principle of how the DNSBL works.

Note: This is not recommended for real use. it’s volatile, the records will be vaporized upon server reboot. Think of this experiment just for learning purposes and fun only :mrgreen: .

DNSBL server perl scripts we called it dnsbl.pl (This script is lousy, fix it if necessary):

#!/usr/bin/perl
use Net::DNS::Nameserver;
use Net::CIDR 'addr2cidr';
use Cache::Memcached;
use strict;
use warnings;

our $our_dnsbl = ".dnsbl.example.com";

# Configure the memcached server
my $memd = new Cache::Memcached {
            'servers' => [ '127.0.0.1:11211' ],
};

sub reverse_ipv4 {
        my $ip = $_[0];
        my ($a1, $a2, $a3, $a4) = split /\./, $ip;
        my $reversed_ipv4 = join('.', $a4,$a3,$a2,$a1);
        return $reversed_ipv4;
}

#sub reverse_ipv4 {
#        my @ips = split /\./, $_[0];
#        my @r;
#        push @r, pop @ips while @ips;
#        return join('.', @r);
#}

sub strip_domain_part {
        my $strip_domain = $_[0];
        $strip_domain =~ s/$our_dnsbl//ig;
        return $strip_domain;
}

sub truncating_ipv4 {
        my $ip_addr = $_[0];
        my ($a1, $a2, $a3, $a4) = split /\./, $ip_addr;

        my $net_work_addr_ess = $ip_addr;
        my $net_work_addr = join('.', $a1,$a2,$a3);
        my $net_work = join('.', $a1,$a2);
        my $net = $a1;

        my @truncated_ipv4_lists = ($net_work_addr_ess,$net_work_addr,$net_work,$net);
        return @truncated_ipv4_lists;
}

sub test_cidr {
        my @cidr_list = Net::CIDR::addr2cidr($_[0]);
        return @cidr_list;
}

sub reply_handler {
        my ($qname, $qclass, $qtype, $peerhost,$query,$conn) = @_;
        my ($rcode, @ans, @auth, @add);
        my ($memc_a_val, $memc_txt_val, $memc_cidr_val, $memc_match_val);

        #print "Received query from $peerhost to ". $conn->{"sockhost"}. "\n";
        #$query->print;

        my $striped_domain = strip_domain_part($qname);
        my $reverse_striped_domain = reverse_ipv4($striped_domain);
        my @truncated_ipv4_lists = truncating_ipv4($reverse_striped_domain);
        my @cidr_lists = test_cidr($reverse_striped_domain);

        if ($qtype eq "A" && $qname eq $striped_domain . $our_dnsbl) {
                foreach my $truncated_ipv4_list (@truncated_ipv4_lists) {
                        $memc_a_val = $memd->get("_a_" . $truncated_ipv4_list);
                        last if(defined($memc_a_val));
                }

                foreach my $cidr_list (@cidr_lists) {
                        $memc_cidr_val = $memd->get("_a_" . $cidr_list);
                        last if(defined($memc_cidr_val));
                }

                for(;;) {
                        if (defined($memc_a_val)) {
                                $memc_match_val = $memc_a_val;
                                last;
                        }
                        if (defined($memc_cidr_val)) {
                                $memc_match_val = $memc_cidr_val;
                                last;
                        }
                }

                my ($ttl, $rdata) = (86400, $memc_match_val);
                push @ans, Net::DNS::RR->new("$qname $ttl $qclass $qtype $rdata");
                $rcode = "NOERROR";
        } elsif ( $qname eq "dnsbl.example.com" ) {
                $rcode = "NOERROR";

        } else {
                $rcode = "NXDOMAIN";
        }

        if ($qtype eq "TXT" && $qname eq $striped_domain . $our_dnsbl) {
                foreach my $truncated_ipv4_list (@truncated_ipv4_lists) {
                        $memc_txt_val = $memd->get("_txt_" . $truncated_ipv4_list);
                        last if(defined($memc_txt_val));
                }

                foreach my $cidr_list (@cidr_lists) {
                        $memc_cidr_val = $memd->get("_txt_" . $cidr_list);
                        last if(defined($memc_cidr_val));
                }

                for(;;) {
                        if (defined($memc_txt_val)) {
                                $memc_match_val = $memc_txt_val;
                                last;
                        }
                        if (defined($memc_cidr_val)) {
                                $memc_match_val = $memc_cidr_val;
                                last;
                        }
                }

                my ($ttl, $rdata) = (86400, $memc_match_val);
                push @ans, Net::DNS::RR->new("$qname $ttl $qclass $qtype $rdata");
                $rcode = "NOERROR";
        } elsif ( $qname eq "dnsbl.example.com" ) {
                $rcode = "NOERROR";

        } else {
                $rcode = "NXDOMAIN";
        }
        # mark the answer as authoritive (by setting the 'aa' flag
        return ($rcode, \@ans, \@auth, \@add, { aa => 1 });
        $memd->disconnect_all();
}

my $ns = Net::DNS::Nameserver->new(
     LocalAddr    => "192.168.200.18",
     LocalPort    => 5353,
     ReplyHandler => \&reply_handler,
     Verbose      => 0,
) || die "couldn't create nameserver object\n";

$ns->main_loop;

postfix, integrating memcache as a lookup table using tcp_table

I have not had time to do the test “postfix memcached patch” because there are no idle servers that can be used for the experiment. instead, I’ve made a tutorial how to integrate memcached as a “postfix lookup table” with the help of tcp_table and a simple perl script.

Indeed, tcp_table “table lookup protocol” is one of the most powerful tools as well as the regexp and pcre, in my opinion. although client-server connection is not protected and and the server is not authenticated.

yes, I did a lot of experiments using tcp_table and perl scripts. it made me realize that I can do almost everything I need and make postfix as my favorite MTA.

Things required:

OK, first we create a simple perl script that allows you to handle the protocols of tcp_table. let’s call it memc.pl

#!/usr/bin/perl
use strict;
use warnings;
use Sys::Syslog qw(:DEFAULT setlogsock);
use Cache::Memcached;

# Configure the memcached server
my $memd = new Cache::Memcached {
            'servers' => [ '127.0.0.1:11211' ],
};

#
# Initalize and open syslog.
#
openlog('postfix/memcached','pid','mail');

sub qrymemc {
        return unless /^get\s+(.+)/i;
        my $kmemc = lc($1);
        chomp($kmemc);
        trim($kmemc);
        my $vmemc = $memd->get($kmemc);
        if (defined $vmemc) {
                return ($kmemc,$vmemc);
        }
        return;
}

sub trim{
        $_[0]=~s/^\s+//;
        $_[0]=~s/\s+$//;
        return;
}

#
# Autoflush standard output.
#
select STDOUT; $|++;

while (<>) {
        chomp;
        if (/^get\s+(.+)/i) {
                my $data = lc($1);
                my @res = qrymemc($data);
                syslog("info","data: %s", $data);
                if (@res) {
                        chomp(@res);
                        print "200 $res[1]\n";
                        syslog("info","Found: key = %s, value = %s", $res[0], $res[1]);
                        next;
                }
        }
        print "200 DUNNO\n";
}

Postfix, old memcache lookup table patch

Yesterday, I was idly fiddling with the old patch postfix “memcached lookup table” created by Omar Kilani . unfortunately, patches can only be used for old postfix distributions (2.1.x – Released 2005-04-01, 2.2.x – Released 2005-04-01).

I rewrote the patch (code was not modified) so it can be applied against last postfix-2.9-20110706 snapshot.
This patches required memcached and libmemcache .

I was successfully compiled it, but not test it yet whether it will work or not. so it’s not recommended for use on production servers.

$ wget ftp://ftp.porcupine.org/mirrors/postfix-release/experimental/postfix-2.9-20110706.tar.gz
$ tar xzf postfix-2.9-20110706.tar.gz
$ cd postfix-2.9-20110706

Postfix memcache patch can be download here:
[download#41]

Patch postfix source distribution

$ patch -p1 < ../postfix-2.9-20110706-memcache.patch
patching file html/DATABASE_README.html
patching file html/Makefile.in
patching file html/MEMCACHE_README.html
patching file html/memcache_table.5.html
patching file man/Makefile.in
patching file man/man5/memcache_table.5
patching file proto/DATABASE_README.html
patching file proto/Makefile.in
patching file proto/MEMCACHE_README.html
patching file proto/memcache_table
patching file README_FILES/AAAREADME
patching file README_FILES/DATABASE_README
patching file README_FILES/MEMCACHE_README
patching file src/global/dict_memcache.c
patching file src/global/dict_memcache.h
patching file src/global/mail_dict.c
patching file src/global/Makefile.in

Postfix, Dynamic OverQuota User Map Script Using Bash And Inotifywait

I recently experimented with a simple bash script, inotifywait and  smtpd_recipient_restrictions (check_recipient_access) to map email users who have exceeded the quota.

Well, during testing, i’ve noticed when using hash/texthash lookup tables, it needed to be reloaded in order smtpd detect changes in table.so i’ve made quick test on mysql_tables it seem updating record on tables will immediately able to be queried

Mapping can be done as follows:
main.cf:

smtpd_recipient_restrictions =
    check_recipient_access mysql:/etc/postfix/mysql_quota_access.cf,
	...
	...

mysql_quota_access.cf

user = user
password = password
hosts = localhost
dbname = postfixdb
query = SELECT qaction FROM quota WHERE username='%s'

create mysql table called quota:

 CREATE TABLE quota (
 id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
 username VARCHAR(100),
 qaction VARCHAR(100)
 ) TYPE=innodb;

Here’s the idea, inotifywait will continuously monitor the maildir directory recursively, and updates “qaction” field on “quota” mysql table whenever new mail arrived or whenever there is email deleted from the maildir.

initial map, can be produced by retrieving user information from database.for example, username information in the database “postfixdb” with the table name “mailbox” and field “username”.

# for i in `mysql -u user -ppassword -D postfixdb -e 'SELECT  username FROM mailbox' | grep -v username`;do mysql -u user -ppassword -D postfixdb -e "INSERT INTO quota (username, qaction) VALUES ('$i', 'DUNNO')";

With this script,value of qaction field on mysql quota table will change continuously as the user’s maildir contents that keeps changing.