There’s a handy way to knock all distributed SMTP AUTH attacks on the head by switching off the advertising of SMTP AUTH to all but specified IP addresses and IP address ranges. It works perfectly. For details on how to achieve this on a WHM/cPanel server, see here:
http://sysadmintips.in/advanced/csf/exim
Great!
However there’s a problem. Many many people like to use Google Mail (Gmail, Inbox) as their email client and they also like to ‘Send Mail As’ some email address other than their Google Mail address. For example, you might use Gmail as ‘jeff @ gmail.com’ but want to send email as ‘jeff @ aetherweb.co.uk’ which looks more professional.
Google now insists that in order to do so, you must let Google connect to the SMTP server (‘aetherweb.co.uk’ in this case) directly.
No problem you think – I’ll just add Google’s IP ranges to my SMTP AUTH ‘approved’ IP list…. but wait, you can’t, Google’s IP ranges change all the time, unpredictably. Awkward.
The SMTP auth file on my WHM/cPanel Centos 6.6 server is:
/etc/csf/csf.smtpauth
To remedy this I’ve written the following code in PHP and set it to run every 10 minutes as a root cron task. It queries Google for the latest IP ranges and then updates them only as needed.
If an update is needed it then creates a file as a flag for a subsequent cron task (details further down).
Here’s the CRONTAB line to launch the code, and the subsequent restart if required:
In file /etc/crontab add: # Check and possibly update Google's SPF auth stuff 0,10,20,30,40,50 * * * * root php -f /path_to_script/update_google_spf.php # We might need, as a result, to restart CSF and LFD 1,11,21,31,41,51 * * * * root sh /path_to_script/restartcsflfd.sh
At the top of the CRONTAB file ensure to set an email recipient for reports.
And here’s the code:
update_google_spf.php
<?php // Grab current Google SPF IPs... $dns = dns_get_record('_spf.google.com', DNS_TXT); if (!$dns) { echo "FAILED TO RETRIEVE DNS RECORD \n"; exit; } // The variable in which to store the results $ranges = array(); // Of interest in particular to us is... $val = $dns[0]['txt']; preg_match_all("/include:[^\s]+\s/", $val, $matches); if (sizeof($matches[0]) <= 0) { echo "BAD DATA RECEIVED OR FAILED TO DECODE DATA \n"; exit; } foreach ($matches[0] as $match) { $match = trim($match); $domain = trim(preg_replace("/include\:/", "", $match)); // Now do it all again for this domain to get the IP range $dns = dns_get_record($domain, DNS_TXT); if (!$dns) { echo "DNS LOOKUP FAILURE AT PASS 2 \n"; exit; } $val = $dns[0]['txt']; preg_match_all("/ip\d:[^\s]+\s/", $val, $ips); if (sizeof($ips[0])<=0) { // At time of writing this is entirely possible as _netblocks3.google.com // currently holds NO IP ranges } else { foreach ($ips[0] as $ip) { $ip = trim($ip); if ($ip <> '') { $ip = preg_replace("/ip\d\:/", "", $ip); $ranges[] = $ip; } } } } // To be here means we made it without a major problem. Form the new IP range for // the smtp auth file (/etc/csf/csf.smtpauth) and compare with the existing. Update only if there has // been a change. Also update only if there are at least N ranges found. // When I wrote this there were 11 IPV4 ranges and 6 IPV6 ranges so setting // low limit to 10 $limit = 10; $filename = '/etc/csf/csf.smtpauth'; if (sizeof($ranges) < $limit) { echo "NOT UPDATING RANGES, TOO FEW DISCOVERED, PROBLEM?"; exit; } $filerange = "# GOOGLE SPF RESULTS START\n"; $filerange .= join("\n", $ranges); $filerange .= "\n# GOOGLE SPF RESULTS END"; // Read in existing conf file $econf = file_get_contents($filename); if (sizeof($econf)<=0) { echo "FAILED TO READ $filename \n"; exit; } // Extract the block if (!preg_match("/\# GOOGLE SPF RESULTS START.+\# GOOGLE SPF RESULTS END/s", $econf, $matches)) { echo "FAILED TO FIND EXISTING BLOCK. CORRUPT AUTH FILE? \n"; exit; } if ($filerange == $matches[0]) { // IT'S THE SAME DO NOT UPDATE IT!; exit; } // Replace the block entirely $econf = preg_replace("/\# GOOGLE SPF RESULTS START.+\# GOOGLE SPF RESULTS END/s", $filerange, $econf); // Write out the new file contents file_put_contents($filename, $econf); // Trigger a CSF/LFD restart by creating trigger file. touch("restartcsflfd"); ?>
restartcsflfd.sh
#!/bin/bash if [ -f /path_to_file/restartcsflfd ]; then csf -r /etc/init.d/lfd restart rm -f restartcsflfd echo "RE-STARTED CSF and LFD" fi