Skip to content

Instantly share code, notes, and snippets.

@bmatthewshea
Last active August 16, 2025 01:19
Show Gist options
  • Save bmatthewshea/1c1a4d5ee8a07912abe62b18ae240d12 to your computer and use it in GitHub Desktop.
Save bmatthewshea/1c1a4d5ee8a07912abe62b18ae240d12 to your computer and use it in GitHub Desktop.
Postfix GeoIP Blocking
#!/usr/bin/perl
# Brady Shea Feb 25 2019
# starting point:
# http://web.archive.org/web/20151128083440/https://www.kutukupret.com/2011/05/29/postfix-geoip-based-rejections/
use strict;
use warnings;
use Sys::Syslog qw(:DEFAULT setlogsock);
use Geo::IP;
use Regexp::Common;
#my $gi = Geo::IP->new(GEOIP_INDEX_CACHE); #should probably use this one? works. New spawns after 3600s, though.
my $gi = Geo::IP->new(GEOIP_STANDARD);
my $country_code = "";
my $client = "";
# Country codes to reject
my @geo_map = (
'BR' ,
'PW' ,
'NL' ,
'JP' ,
'UA' ,
'LR' ,
'KR' ,
'RO' ,
'IN' ,
'CN' ,
'HK' ,
'TW' ,
'RU'
);
#
# Initalize and open syslog.
#
openlog('postfix/geoip','pid','mail');
#
# Autoflush standard output.
#
select STDOUT; $|++;
while (<>) {
if ($_ =~ /\w\s\w/) # if two words..
{
chomp;
$_ =~ s/^\S+\s*//; #Remove "get " (first word)
$client = $_;
#check tld sent first to avoid geoip call if possible (example mailer.domain.ru = .ru/RU)
$country_code = uc tld_check($client);
if ( grep /$country_code/, @geo_map )
{
reject_country();
$country_code=""; # remove
next;
}
#geoip tests
if ( $client =~ m/$RE{net}{IPv4}/ ){
$country_code = $gi->country_code_by_addr($client); # is IP - normally NOT sent by POSTFIX "check_client_access tcp:" - sends 'unknown'
} else {
$country_code = $gi->country_code_by_name($client); # is DNS name
}
if (defined $country_code)
{
if ( grep /$country_code/, @geo_map )
{
reject_country();
} else {
approve_country();
}
} else {
unknown_country();
}
next;
}
#incomplete lookup (was the "get sub.domain.tld" missing the 'get'? The string is always 2 parts.
lookup_error();
}
# SUBS
#quick check on TLD to see if it matches our banned tlds. This should avoid calling GEOIP too much
sub tld_check {
my @pieces = split /\./,$_;
my $tld = $pieces[-1];
chomp($tld);
return $tld;
}
sub reject_country {
print "200 REJECT no connections accepted from country code $country_code\n";
syslog("info","Client: %s REJECTED because of country code match %s", $client, $country_code);
return
}
sub approve_country {
print "200 DUNNO passed geoip check\n";
syslog("info","Client: %s PASSED geoip from country code: %s", $client, $country_code);
return
}
sub unknown_country {
print "200 DUNNO geoip country not found\n";
syslog("info","Client: %s PASSED geoip - no country found", $client);
return
}
# This next one should probably be switched to REJECT if I start seeing many of them.
# POSTFIX should ALWAYS send a two part string.
sub lookup_error {
print "200 DUNNO geoip skipped\n";
syslog("info","Incomplete DATA received from MTA to do a geoip check");
return;
}

Installation:

The starting point for this script was from here:
http://web.archive.org/web/20151128083440/https://www.kutukupret.com/2011/05/29/postfix-geoip-based-rejections/

You need:

  • Linux machine with-
  • Perl
  • Perl Geo::IP module
  • and of course "Postfix" (MTA)

  1. You will need to add the script above somewhere on your system. /etc/postfix/scripts/postfix-geoip.pl would probably be a good place. It doesn't really matter where it is placed, though. Keep in mind the permissions & owner will need to be correct no matter where you put it.

    Once placed, make sure it's owned by root and can be run by the "nobody" user. (It should be owned by root to avoid postfix warnings):

    sudo chown root: /etc/postfix/scripts /etc/postfix/scripts/postfix-geoip.pl
    sudo chmod 755 /etc/postfix/scripts/postfix-geoip.pl
    
  2. Once the script is owned correctly and executable on the Postfix system, you will need to edit the Postfix configuration.

    Edit sudo nano /etc/postfix/main.cf and find smtpd_client_restrictions = and add a 'check_client_access' directive under it (just make sure it has a comma on end and is above the final 'permit') Leave any other directives you may see (the dots '...') in place.:

    smtpd_client_restrictions =
    ...
    check_client_access tcp:[127.0.0.1]:2528,
    ...
    permit
    

    Example:

    SHEA99-2022-06-22_093904

    NOTE: It may be a better idea to place this under smtpd_helo_restrictions since this is the very first check. If it's a bad IP, it should go no further. Less system resources would be used to check and 'block' a connected IP under HELO hypothetically. I used smtpd_client_restrictions for my own reasons. Either area should work. I haven't tested it under helo restrictions, though.

  3. Next, edit the /etc/postfix/master.cf file and put this bit at the very bottom of this file:

    127.0.0.1:2528 inet  n       n       n       -       0      spawn
             user=nobody argv=/etc/postfix/scripts/postfix-geoip.pl
    
  4. Next install GeoIP system wide. Debian/Ubuntu apt example:

    sudo apt update -y && sudo apt install libgeo-ip-perl
    

    OR: If using cpan to install the module:

    sudo cpan install Geo::IP
    


Configuration is complete. Restart Postfix:

sudo systemctl restart postfix

Test / check mail.log / etc.

@bmatthewshea
Copy link
Author

bmatthewshea commented Feb 15, 2024

@FoulFoot

  • The file size is fine. It's just dynamically linked in system which results in smaller base executable.
  • Your version of Perl is fine albeit old.
  • I installed Alma Linux 8 during lunch under VirtualBox.

Works fine for me / as it should:

OrWhGft 1

The script is clearly executing using the /usr/bin/perl executable because of the shebang on first line. (Just because I am missing Geo-IP is irrelevant to this test.)

I have no idea what is wrong with your environment, but something definitely is.
Creating a new email server is time consuming, but isn't hard. My recommendation at this point would be to do a fresh install of distro of your choice and just recreate what you have there.

The only thing I see wrong in your config other than the bash env being broken is this (I mentioned in previous comment, but you may have missed it):

user=nobody argv=perl /etc/postfix/scripts/postfix-geoip.pl

Use this instead if using perl command ( add "/usr/bin/perl" instead of just "perl" and try with and without quotes):
user=nobody argv="/usr/bin/perl /etc/postfix/scripts/postfix-geoip.pl"

@FoulFoot
Copy link

@bmatthewshea ,

I fixed the environment problem. There was an errant line terminator (CR-LF) in the script which I copied from my Windows browser. Correcting that made the script executable directly.

Still can't get Postfix to listen on port 2528, no matter what I try. The port is open in the firewall, but as pointed out, the loopback should be inside the firewall anyway. I tried moving the script to /usr/local/bin, but still no dice. The script runs fine from the CLI, it's just that Postfix can't execute it (or is being blocked from reaching it). Entering sudo in master.cf ("user=nobody argv=sudo -u nobody /etc/postfix/scripts/postfix-geoip.pl") doesn't fix it.

I don't know if it's helpful, but in main.cf there's some configuration lines for milters:

smtpd_milters = inet:127.0.0.1:8891,inet:127.0.0.1:8893,unix:/run/spamass-milter/spamass-milter.sock,inet:localhost:8891
non_smtpd_milters = inet:127.0.0.1:8891,inet:127.0.0.1:8893,unix:/run/spamass-milter/spamass-milter.sock,inet:localhost:8891

... showing a loopback connection on ports. I vaguely recall there was some additional configuration somewhere to make ports 8891 and 8893 work, but I could be mistaken. That's the only thing I can think of at this point.

@bmatthewshea
Copy link
Author

bmatthewshea commented Feb 15, 2024

I asked you to look closely at your shebang in prev posts. You didn't do that until now? We spent a lot of time on that for nothing.

And please stop messing with firewall and adding/removing statements:
I noted at beginning of this to only do things I say and you wouldn't hurt your system.

The port does NOT need to be open on your firewall !!
Make sure you reverse everything you try.
Leaving something wrong in place will just add another tangle on top of the problem you already have.

Anyway it's not your firewall (or milters):
You are running script on "127.0.0.1:2528" (local machine only via tcp) - Postfix is running on same machine, I hope? A firewall has no reason to be involved and shouldn't be! The same holds true for those milters you list. You do not need to open a firewall for localhost only.

Do not add sudo or anything I haven't mentioned (see below). It is already being started as root. Root starts it and runs it as 'nobody' to keep execution as safe as possible.

Did you try the master.cf line I asked you to try? I'll ask for a 3rd time:

The only thing I see wrong in your config other than the bash env being broken is this (I mentioned in previous comment, but you may have missed it):

user=nobody argv=perl /etc/postfix/scripts/postfix-geoip.pl

Use this instead if using perl command ( add "/usr/bin/perl" instead of just "perl" and try with and without quotes):
user=nobody argv="/usr/bin/perl /etc/postfix/scripts/postfix-geoip.pl"

You could also try using (..) argv=/etc/postfix/scripts/postfix-geoip.pl now that your shebang is fixed.

@FoulFoot
Copy link

The problem with the "shebang" was invisible, so cut me some slack.

I did try listing the full path to the perl command, but since the script is now directly executable, it's no longer needed.

I think we're going around in circles at this point, so thank you both for your assistance in trying to get this working.

@bmatthewshea
Copy link
Author

bmatthewshea commented Feb 15, 2024

We are running in circles mainly because you keep doing things not mentioned.
Firewalls, sudo, etc are not going to help - just hurt.
I feel like everything you need to troubleshoot it is in the thread above.
You're welcome, and I wish you luck..

@ShamimIslam
Copy link

ShamimIslam commented Feb 15, 2024 via email

@bmatthewshea
Copy link
Author

bmatthewshea commented Feb 16, 2024

If he wasn't aware of a "hidden space" in his script until just a few msgs ago, then it could really be anything at this point.
I wouldn't waste your time on it @ShamimIslam ..
He has everything he needs to troubleshoot and fix it above.
(PS: He already tested the script and it worked. And none of what you are mentioning is going to help him/her.)

@Simanova86
Copy link

Hi, i got "malformed reply: Can't locate Regexp/Common.pm" in the mail warn file

i figured out the libregexp-common-perl was missing.

Please consider to add it to your manual.

@ShamimIslam
Copy link

ShamimIslam commented Jun 16, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment