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/* .
At this point now we should have Policy directory containing Memcache.pm and SenderScore.pm and also t.pl (this used for test ge and increment). About the policy itself, since there is no standardization of how to implement the score from senderscore I will try to make our own policy class.
The idea is, we will restrict the number of emails from one ip address coming into our server within one hour.
Score 1 – 10, limited sending 100 emails per hour.
Score 11 – 20, limited sending 200 emails per hour.
Score 21 – 30, limited sending 300 emails per hour.
Score 31 – 40, limited sending 400 emails per hour.
Score 41 – 50, limited sending 500 emails per hour.
Score 51 – 60, limited sending 600 emails per hour.
…
…
Score 91 – 100, limited sending 1000 emails per hour.
Now, create a policy server, to implement our policy class, I modify the script a little bit that I downloaded from here
#!/usr/bin/perl # use strict; use warnings; use lib ( "." ); use Policy::Memcache; use IO::Socket; use threads; use Proc::Daemon; use Sys::Syslog qw(:DEFAULT setlogsock); use Data::Dumper; # Global config settings my $TC = 15; my $debug = 1; our $pidfile = "/var/run/policy.pid"; my $mc = Policy::Memcache->new; # Param1: Client socket # Param2: hash_ref sub parse_postfix_input( $$ ) { my ($socket,$hashref) = @_; while( my $line = <$socket> ){ return if $line =~ /^(\r|\n)*$/; #print "DEBUG: $line" if $debug; if( $line =~ /^(\w+?)=(.+)$/ ){ $hashref->{$1} = $2; $hashref->{$1} =~ s/\015?\012?$//; } } } sub process_policy_request( $ ){ my ($href) = @_; my $action = "DUNNO"; my $messages_limit; # Do something with the href that we've consumed... my $client_ip = $href->{client_address}; my ($messages_count,$ss) = $mc->get( $client_ip ); if ($ss == -1 ) { $action = 'DUNNO'; $messages_limit = -1; } $mc = Policy::Memcache->new( -policy => 'ss' ); $mc->increment( $client_ip ); # classifying senderscore value, 100 messages / 10 score # last rule is unlimmited which has senderscore 90 - 100 if (($ss >= 1) && ($ss <= 10)) { $action = ($messages_count <= 100) ? 'DUNNO' : 'REJECT'; $messages_limit = 100; } elsif (($ss >= 11) && ($ss <= 20)) { $action = ($messages_count <= 200) ? 'DUNNO' : 'REJECT'; $messages_limit = 200; } elsif (($ss >= 21) && ($ss <= 30)) { $action = ($messages_count <= 300) ? 'DUNNO' : 'REJECT'; $messages_limit = 300; } elsif (($ss >= 31) && ($ss <= 40)) { $action = ($messages_count <= 400) ? 'DUNNO' : 'REJECT'; $messages_limit = 400; } elsif (($ss >= 41) && ($ss <= 50)) { $action = ($messages_count <= 500) ? 'DUNNO' : 'REJECT'; $messages_limit = 500; } elsif (($ss >= 51) && ($ss <= 60)) { $action = ($messages_count <= 600) ? 'DUNNO' : 'REJECT'; $messages_limit = 600; } elsif (($ss >= 61) && ($ss <= 70)) { $action = ($messages_count <= 700) ? 'DUNNO' : 'REJECT'; $messages_limit = 700; } elsif (($ss >= 71) && ($ss <= 80)) { $action = ($messages_count <= 800) ? 'DUNNO' : 'REJECT'; $messages_limit = 800; } elsif (($ss >= 81) && ($ss <= 90)) { $action = ($messages_count <= 900) ? 'DUNNO' : 'REJECT'; $messages_limit = 900; } elsif (($ss >= 91) && ($ss <= 100)) { $action = ($messages_count <= 1000) ? 'DUNNO' : 'REJECT'; $messages_limit = 1000; } return ($action, $client_ip, $ss, $messages_count, $messages_limit); } sub process_client($){ my ($socket) = @_; ACCEPT: while( my $client = $socket->accept() ){ my $hash_ref = {}; parse_postfix_input( $client, $hash_ref ); #print "DEBUG: " . Dumper( $hash_ref ) . "\n"; my @res = process_policy_request( $hash_ref ); if( $res[0] eq 'REJECT' ){ syslog('info', "$res[0]: client ip: $res[1], sender score: $res[2], message count: $res[3], messages limit: $res[4]"); print $client "action=421 sender score = $res[2], you're limited to $res[4] messages/hours in our policy, try again later\n\n"; next ACCEPT; } print $client "action=dunno\n\n"; } } sub handle_sig_int { unlink( $pidfile ); exit(0); } openlog('policy', '', 'mail'); syslog('info', 'launching in daemon mode') if (defined($ARGV[0]) && $ARGV[0] eq 'quiet-quick-start'); Proc::Daemon::Init if (defined($ARGV[0]) && $ARGV[0] eq 'quiet-quick-start'); # Attempt to parse in the redirect config $SIG{INT} = \&handle_sig_int; # Ignore client disconnects $SIG{PIPE} = "IGNORE"; open PID, "+>", "$pidfile" or die("Cannot open $pidfile: $!\n"); print PID "$$"; close( PID ); my $server = IO::Socket::INET->new( LocalAddr => 'localhost', LocalPort => 1234, Type => SOCK_STREAM, Reuse => 1, Listen => 10 ) or die "Couldn't be a tcp server on port" . my $default_config->{serverport} . ": $@\n"; # Generate a number of listener threads my @threads = (); for( 1 .. $TC ){ my $thread = threads->create( \&process_client, $server ); push( @threads, $thread ); } foreach my $thread ( @threads ){ $thread->join(); } unlink( $pidfile ); closelog; exit( 0 );
Make the script executable and run it in the background
$ chmod 755 policy.pl $ sudo ./policy.pl quiet-quick-start
Simple test using telnel localhost 1234
$ telnet localhost 1234 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. request=smtpd_access_policy protocol_state=RCPT protocol_name=SMTP helo_name=some.domain.tld queue_id=8045F2AB23 sender=foo@bar.tld recipient=bar@foo.tld recipient_count=0 client_address=202.127.97.97 client_name=another.domain.tld reverse_client_name=another.domain.tld instance=123.456.7 action=dunno Connection closed by foreign host.
After several test via telnet session we’ll get the value as example:
$ perl t.pl 202.127.97.97 get 202.127.97.97:75 => 6
202.127.97.97 had a score of 75, that means in our policy class it is allowed to send upto 800 emails.
to speed up so that its value exceeds 800 I run this simple command:
$ for i in $(seq 1 801);do perl t.pl 202.127.97.97 increment;done $ perl t.pl 202.127.97.97 get 202.127.97.97:75 => 803
Now, its value exceeds 800, let’s try again using a telnet session
$ telnet localhost 1234 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. request=smtpd_access_policy protocol_state=RCPT protocol_name=SMTP helo_name=some.domain.tld queue_id=8045F2AB23 sender=foo@bar.tld recipient=bar@foo.tld recipient_count=0 client_address=202.127.97.97 client_name=another.domain.tld reverse_client_name=another.domain.tld instance=123.456.7 action=421 sender score = 75, you're limited to 800 messages/hours in our policy, try again later Connection closed by foreign host.
In main.cf
smtpd_end_of_data_restrictions = ... check_policy_service inet:127.0.0.1:1234 ...
When email exceeding its limit in policy class, it’ll be rejected as shown below
$ telnet xxx.xxx.xx.xxx 25 Trying xxx.xxx.xx.xxx... Connected to mx.example.com (xxx.xxx.xx.xxx). Escape character is '^]'. 220 mx.example.com ESMTP Postfix (2.9-20110706) ehlo mx.example.net 250-mx.example.com 250-PIPELINING 250-SIZE 52428800 250-ETRN 250-STARTTLS 250-AUTH LOGIN PLAIN 250-AUTH=LOGIN PLAIN 250-ENHANCEDSTATUSCODES 250-8BITMIME 250 DSN mail from:<foo@example.com> 250 2.1.0 Ok rcpt to:<bar@example.net> 250 2.1.5 Ok data 354 End data with <CR><LF>.<CR><LF> Subject: test From from@example.com To: bar@example dasdasd . 421 4.7.1 <END-OF-MESSAGE>: End-of-data rejected: sender score = 75, you're limited to 800 messages/hours in our policy, try again later Connection closed by foreign host.
The log shows:
Aug 15 09:45:42 fire postfix/smtpd[15215]: 8F9CA400D5: reject: END-OF-MESSAGE from mx.example.net[xxx.xxx.xx.xxx] 421 4.7.1 <END-OF-MESSAGE>: End-of-data rejected: sender score = 75, you're limited to 900 messages/hours in our policy, try again later; from=<foo@example.com> to=<bar@example.com> proto=ESMTP helo=<mx.example.com>
Ok, that’s it for now.
reference:
Hey, very cool! I quite enjoyed your post 🙂
finally, someone enjoyed my random ideas, LOL, thank you 🙂
I’ve tried your solution, but can’t get the senderscore.. I always get -1 and don’t know how to debug.
perl t.pl 202.127.97.98 get
202.127.97.98:-1 => 0
I already modify nameservers in SenderScore.pm with my own (default was 1.2.3.4)
But still no luck.
Can you help me please ?