Skip to content

Instantly share code, notes, and snippets.

@bmatthewshea
Last active March 28, 2024 17:26
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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.

@ShamimIslam
Copy link

Can you run it on the cli? Does the geo-ip file run manually?

@ShamimIslam
Copy link

If all else fails, set postfix to log more verbosely. It should tell you the problem.

@ShamimIslam
Copy link

ufw runs on everything and lets you manage the firewall easily. Are you running selinux?

@ShamimIslam
Copy link

ShamimIslam commented Feb 14, 2024

What happens when you type /usr/local/bin/geo-ip-reject.pl (or whatever you called the script) and hit enter on the command line in your ssh session.

@FoulFoot
Copy link

I'm running AlmaLinux.

Running from cli, it complained about a Regexp module. So I installed that. Now it just hangs at the cli, which I assume is a good thing, as it probably needs an IP address parameter or something.

I'm going to try and activate it now, and see if it's working.

@FoulFoot
Copy link

Nope, still refusing connection.

@ShamimIslam
Copy link

ShamimIslam commented Feb 14, 2024

It's supposed to hang. Post your master.cf. (last part of it.)

Also, send a ls -al of your /usr/local/bin

Also, locate the user and group postfix is running as.

Did you install the Geo::IP module as well?

@FoulFoot
Copy link

ls -al of /etc/postfix/scripts: -rwxr-x--- 1 root root 2998 Feb 13 18:46 postfix-geoip.pl

Postfix runs as root/root.

master.cf:

smtp inet n - n - - smtpd -o smtpd_sasl_auth_enable=yes -o smtpd_tls_security_level=may -v

pickup unix n - n 60 1 pickup
cleanup unix n - n - 0 cleanup
qmgr unix n - n 300 1 qmgr

tlsmgr unix - - n 1000? 1 tlsmgr
rewrite unix - - n - - trivial-rewrite
bounce unix - - n - 0 bounce
defer unix - - n - 0 bounce
trace unix - - n - 0 bounce
verify unix - - n - 1 verify
flush unix n - n 1000? 0 flush
proxymap unix - - n - - proxymap
proxywrite unix - - n - 1 proxymap
smtp unix - - n - - smtp
relay unix - - n - - smtp
-o syslog_name=postfix/$service_name

showq unix n - n - - showq
error unix - - n - - error
retry unix - - n - - error
discard unix - - n - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - n - - lmtp
anvil unix - - n - 1 anvil
scache unix - - n - 1 scache

submission inet n - n - - smtpd
-o smtpd_sasl_auth_enable=yes
-o smtpd_tls_security_level=may
-o smtp_tls_mandatory_protocols=TLSv1
spamassassin unix - n n - - pipe flags=R user=spamd argv=/usr/bin/spamc -e /usr/sbin/sendmail -oi -f ${sender} ${recipient}
smtps inet n - n - - smtpd -o smtpd_sasl_auth_enable=yes -o smtpd_tls_security_level=may -o smtpd_tls_wrappermode=yes

postlog unix-dgram n - n - 1 postlogd

127.0.0.1:2528 inet n n n - 0 spawn
user=nobody argv=/etc/postfix/scripts/postfix-geoip.pl

@FoulFoot
Copy link

Yes, Geo::IP is installed.

@ShamimIslam
Copy link

ShamimIslam commented Feb 14, 2024

Your perl file is owned by root but is attempted to be run by nobody/nogroup.

chown nobody.nogroup /etc/postfix/scripts/postfix-geoip.pl

or

chown nobody.nobody /etc/postfix/scripts/postfix-geoip.pl
ls -al of /etc/postfix/scripts: 
-rwxr-x--- 1 root root 2998 Feb 13 18:46 postfix-geoip.pl

The user nobody is unable to access a file that is owned by user root and group root where the permissions are: rwxr-x---

So you are getting "connection refused" because the program is failing to spawn.

DO NOT run the perl program as root.
Type:
id nobody
That should tell you the user and group you need to use for your chown command.

@FoulFoot
Copy link

I tried the above and still connection refused.

Looking a little deeper, it appears I may not actually have Geo::IP installed. I have perl-Geo-IP installed which apparently is an older version. In trying to install Geo::IP using CPAN, I ran into a host of other errors that I'm not able to figure out. I've decided to stop now before I break something.

Thank you very much for your efforts!

@bmatthewshea
Copy link
Author

bmatthewshea commented Feb 14, 2024

Hi @FoulFoot

Your problem isn't that big of problem where you would mess anything up. Just don't do anything not mentioned here..

As @ShamimIslam said, the permissions on the file are probably the issue and the script is never spawned on the 2528 port in master.cf.

Therefore there is nothing behind the port listening and you get a refused.

I don't think I read if you checked the port to see if it's open?

The first place to start troubleshooting if you get a connection refused:
sudo lsof -i | grep 2528 (or u can use sudo netstat -ltn | grep :2528 if you don't have lsof)

You should see it bound to localhost or 127.0.0.1. Either one is fine. If you don't, the script was never spawned in master.cf on the port.

Assuming the script is actually located where you are saying it is, this could mean it can't read the script due to ownership, or permissions (as mentioned). That amounts to the same thing: It is never used. This "error" master.cf entry should be in your mail.log if you turn up verbosity (as mentioned).

To test if it works, do this (make it 'world executable'):
sudo chmod 755 /etc/postfix/scripts/postfix-geoip.pl

Restart Postfix.

Then try this again:
sudo lsof -i | grep 2528

If you see it bound to the port, try:
telnet 127.0.0.1 2528

(If it connects via telnet, use CTRL-] and then type 'quit' to exit.)
It works if it connects.

If it is working, you can try to reduce the permissions.
Or, you can leave as-is: It's only a minor security concern if you are the only system user.
If script works, do this if you want to make it more secure - be careful - copy and paste the FULL line:

sudo chown -R nobody:nogroup /etc/postfix/scripts/ && sudo chmod 750 /etc/postfix/scripts/postfix-geoip.pl

/scripts/ area should look like this when you do that (my filename is different BTW):

Image-2024-02-14_124854

(restart Postfix and re-test port)

If you still get a refused, the script was never spawn to the port via master.cf.
More than likely, you still have permission problem, or script can't be found.
If still getting a 'refused' after trying above, let me know.

@ShamimIslam
Copy link

ShamimIslam commented Feb 14, 2024 via email

@FoulFoot
Copy link

Hi @bmatthewshea,

First, thanks for the detailed reply! Though you underestimate my ability to break things. :)

I can't get any response on 2528 with netstat. I confirmed the 755 permissions set on the script directory, and the script itself, so Postfix should definitely be able to access it. If the script was accessed but failed to run, would it also give a connection refused?

My master.cf line is actually this:

127.0.0.1:2528 inet n n n - 0 spawn
user=nobody argv=perl /etc/postfix/scripts/postfix-geoip.pl

... on AlmaLinux / CentOS, perl scripts can't be run natively, they need to be an argument of the perl command. So I don't know if the space in there would cause an issue -- I'd think not. Other than that, if Geo::IP is not installed (which I don't believe it is), the script should also fail (or hang), causing a "connection refused" too?

I tried completely disabling the firewall, but still no dice. I wonder if there's a setting somewhere preventing the port connection on loopback. In main.cf I do have inet_interfaces = all, and I tried uncommenting mynetworks = 127.0.0.0/8, but no effect.

@ShamimIslam,

Yeah, I don't know about that. There's well over a hundred perl modules installed on my system right now, and if I yank them all off to install the CPAN thing, that WILL break everything. CPAN itself appears to install fine, but using it to then install the Geo::IP module hangs: "Fetching with LWP: http://www.cpan.org/authors/01mailrc.txt.gz..." . I suspect that my system is blocking a download from an http: site, which in 2024 is no-bueno (indeed, trying to download it on my Windows browser also results in the file being blocked). Googling this issue doesn't result in anything useful that I could find.

@bmatthewshea
Copy link
Author

bmatthewshea commented Feb 14, 2024

@FoulFoot

Hi,
No problem. Have you tried to test the script directly to rule it out?

sudo /etc/postfix/scripts/postfix-geoip.pl
(then lookup something:)
get mx3.insidetheslice.com

You should see this output if blocking this country code (and GeoIP lib working) - it should also be in your defined mail.log/maillog:
200 REJECT no connections accepted from country code RO

(hit ctrl-c to exit script - if script exits by itself, there is definitely an error /or dep missing.. it should tell you)

Post back with results..


Re: "Run natively":
The space shouldn't be a problem in the master.cf, but -
You have Perl installed on same machine as Postfix, correct? Then you can run it "natively".
If you mean you cannot run the script directly in shell, it is probably because Perl is in a different area of your path.

The first line of script (above) defines the command interpreter/"language": #!/usr/bin/perl

Is Perl located there for you? (ls /usr/bin/perl)?
If not, you cannot run script directly till you update that location in first line of script.
see here - https://unix.stackexchange.com/a/87600
To find it try: which perl and replace the path in first line with where yours is.. I don't have CentOS installed right now so I can't tell you.

@bmatthewshea
Copy link
Author

bmatthewshea commented Feb 14, 2024

@ShamimIslam Thanks for the support/help above! 👍 🥇

@ShamimIslam
Copy link

ShamimIslam commented Feb 15, 2024

@FoulFoot Also, if your user is "root" but the program runs as "nobody", you have to
check that every step in the path is ALSO accessible. A single entry that
prevents it, will cause an issue.

sudo -u nobody

@bmatthewshea Been using your library consistently for a couple of years now. I finally got the hang of it. It's been keeping the Nigerians and Russians out. LOL. So thank you for helping me when I first started. I'm just giving back.

P.S. I have 2 different versions running for 2 different parts of the postfix transaction.

P.P.S. Have you tried the new Postfix SNI (and the Dovecot SNI)? I just recently got started when I found the OLS SNI option. Now instead of a dozen servers, I can use 3. At least until traffic picks up. Then I can just move the instance to a larger machine.

@bmatthewshea
Copy link
Author

@ShamimIslam
Yep. Clearly some countries have zero business with my business clients other than delivering junk and/or account exploits to them. I have a few who only communicate business-wise with people in certain countries - and nowhere else, so in those cases it makes sense and limits a lot of junk/mal-email/etc (and server load & storage cost). It's not perfect, but anything that can limit some of the junk relaying is good in my book. :-)

No, I haven't tried anything labeled like that? I think I have a web server setup with ssl SNI (ones that need hardened encryption and/or 'http2'), not so sure it's even needed on Postfix/et al? And yeah - I mostly use a cloud provider for all my email servers/services/instances - many virtual domains on a server. Why would SNI help that/improve on it? Just have all your domains/people use one set of configs/one domain - no reason to use their own virtual domain for email client setups and only complicates your mail server configuration (I have done it - never again. lol).

@FoulFoot
Copy link

@bmatthewshea

The script does work -- received the 200 REJECT code. So obviously then the Geo::IP module is either somehow installed or the older iteration is working. So that's good.

Re: running natively. Perl is in /usr/bin, yes. Attempting to run the script directly results in "-bash: postfix-geoip.pl: command not found". I was in that scripts directory when I tried it, so not a pathing issue to the script.

@ShamimIslam

I've never used sudo -- on earlier CentOS versions it was not a valid command (I believe), though I do see that it's in AlmaLinux now. "sudo -u nobody" is invalid. "sudo -u nobody postfix-geoip.pl" results in "sudo: postfix-geoip.pl: command not found". Not sure what info you were looking for.

I tried switching the master.cf entry to user root, which should definitely be able to access it, and it didn't work.

@bmatthewshea
Copy link
Author

bmatthewshea commented Feb 15, 2024

@FoulFoot

So, as root when you run:
# /etc/postfix/scripts/postfix-geoip.pl
(Do not add perl to it)
It fails and says command not found?

Your perl under /usr/bin/perl should look like:

-rwxr-xr-x 2 root root 3478464 Nov 23 09:02 /usr/bin/perl

Correct? (Meaning the permissions are 755?)

What happens when you type perl --version? (What version does it say?)

As an alternative, you could try #!/usr/bin/env perl as your first line and see if it works, but I see no reason why it wouldn't work otherwise unless you are using some non-standard shell??

The fact that you say that 'shebang' line doesn't work previously makes wonder if this isn't the problem somehow..

My master.cf line is actually this:

127.0.0.1:2528 inet n n n - 0 spawn
user=nobody argv=perl /etc/postfix/scripts/postfix-geoip.pl

If you do it like that, be sure to include the full path for your perl:
. . . argv=/usr/bin/perl /etc/postfix/scripts/postfix-geoip.pl

@FoulFoot
Copy link

Yes, command not found.

-rwxr-xr-x 2 root root 12736 May 18 2023 perl

I notice that the filesize is way, way less than yours. Not small enough to be an alias or something, but definitely not the full-size executable.

"This is perl 5, version 26, subversion 3 (v5.26.3)"... copyrighted 2018.

I'd think my package manager would keep this up-to-date, and I don't think there's anything weird or non-standard on the install. Though this was an old CentOS 7 install that was upgraded in-place to AlmaLinux 8, so legacy components may not have correctly upgraded / transferred over.

Unclear what you mean by "#!/usr/bin/env perl as your first line"... I changed the first line of your script to that, but it didn't work -- the shell doesn't see it as a valid executable (which it isn't). In Windows we would say that the "default program" for the .pl extension needs to be set to Perl. Right now the shell doesn't know what to do with it.

@FoulFoot
Copy link

I just uninstalled and re-installed Perl, and got the same version (five years old). I then confirmed that the latest version of Perl for RHEL 8 is indeed 5.26, so that's not the issue.

@ShamimIslam
Copy link

@FoulFoot
The computer will only do exactly what you say.

sudo -u nobody <path to perl script>
That's what you need.

If you received a reject code from running the script with no data, there is a problem.

I've stopped using RHEL 8 and clones since CentOs went the way of the dodo.

Ubuntu server for everything now.

I feel like if we shared screens at some point the error would become obvious. :)

@bmatthewshea

SNI for consolidating services with a single server so that it uses the correct certificate based on FQDN even though the IP address is the same.

@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.)

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