A Perl script to send Caller ID popups from Asterisk to computers running Growl under OS X on a Mac or Growl for Windows

 

Important
This is an edited version of a post that originally appeared on a blog called The Michigan Telephone Blog, which was written by a friend before he decided to stop blogging. It is reposted with his permission. Comments dated before the year 2013 were originally posted to his blog.
Notice
EDIT March, 2014 and August 2020: If you are running OS X Mavericks or later, or any version of MacOS we recommend that you do NOT use the script shown here, but instead send notifications to a XMPP/Jabber account and use either Apple’s Messages app (formerly iChat) or a third party messaging program such as Adium to receive them, since the message will then display in the Notifications Center and you do not need Growl. See How to send various types of notifications on an incoming call in FreePBX for more information. You may also find this thread on the RasPBX forum useful.

What follows will probably not work on ANY currently supported version of MacOS and is left here as a historical reference only.

Quite some time ago, I wrote a post explaining how you could poll a Linksys or Sipura VoIP adapter or phone once per second, and whenever there was an incoming call, generate a notification popup on your computer, if you have the Growl notification service installed.  However, that method doesn’t work if you’re not using a Linksys or Sipura phone or device.

If you are running Asterisk, there’s another way to do it, and that’s to get Asterisk to send the notifications directly. In order for this to work, the computer on which you want to receive the notifications has to be running Growl (under Mac OS X) or Growl for Windows. You must also configure Growl to receive network notifications. I will note here that if you are using a Mac and have never done that before, you may want to make sure that Growl network notifications work before proceeding, because it appears that under OS X, it’s pretty much a crap shoot whether Growl network notifications will work at all, and when they don’t the Growl folks apparently have no clue as to why they don’t. It seems to be a machine-specific thing – on some Macs they work fine, while on others they don’t work at all.

You must have the Perl language installed on your Asterisk server, and you must have the Net::Growl and Asterisk::AGI modules installed (I’m going to assume you know how to install a Perl module from the CPAN repository – if you have Webmin installed, it can be done from within Webmin). Chances are you already have Asterisk::AGI installed, unless you built your Asterisk server “from scratch” and never installed it, but if you’ve never installed Net::Growl you’ll need to do that first.

Next you want to copy and paste the following Perl script to the filename /var/lib/asterisk/agi-bin/growlsend.agi on your Asterisk server (to create a non-existent file, you can use the touch command, and after that you can edit it in Midnight Commander or by using the text editor of your choice). If this code looks somewhat familiar, it’s because it’s adapted from some code that originally appeared in a FreePBX How-To, which I modified.

#!/usr/bin/perl
use strict;
use warnings;
use Net::Growl;
use Asterisk::AGI;
my $agi = new Asterisk::AGI;
my %input = $agi->ReadParse();
my $num = $input{'callerid'};
my $name = $input{'calleridname'};
my $ext = $input{'extension'};
my $ip = $ARGV[0];

if ( $ip =~ /^([0-9a-f]{2}(:|$)){6}$/i ) {
    $ip = $agi->database_get('growlsend',uc($ip));
}

unless ( $ip =~ /^(d+).(d+).(d+).(d+)$/ ) {
    exit;
}

open STDOUT, '>/dev/null';
fork and exit;

if ( $ARGV[2] ne "" ) {
    $ext = $ARGV[2];
}

# Define months and weekdays in English

my @months = (
    "January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December"
);
my @weekdays = (
    "Sunday", "Monday", "Tuesday", "Wednesday",
    "Thursday", "Friday", "Saturday"
);

# Construct date/time string

my (
    $sec, $min, $hour, $mday, $mon,
    $year, $wday, $yday, $isdst
) = localtime(time);
my $ampm = "AM";
if ( $hour > 12 ) {
    $ampm = "PM";
    $hour = ( $hour - 12 );
}
elsif ( $hour eq 12 ) { $ampm = "PM"; }
elsif ( $hour eq 0 ) { $hour = "12"; }
if ( $min < 10 ) { $min = "0" . $min; }
$year += 1900;

my $fulldate =
"$hour:$min $ampm on $weekdays[$wday], $months[$mon] $mday, $year";

# Next two lines normalize NANP numbers, probably not wanted outside of U.S.A./Canada/other NANP places
$num =~ s/^([2-9])(d{2})([2-9])(d{2})(d{4})$/$1$2-$3$4-$5/;
$num =~ s/^(1)([2-9])(d{2})([2-9])(d{2})(d{4})$/$1-$2$3-$4$5-$6/;

register(host => "$ip",
    application=>"Incoming Call",
    password=>"$ARGV[1]", );
notify(host => "$ip",
    application=>"Incoming Call",
    title=>"$name",
    description=>"$numnfor $extn$fulldate",
    priority=>1,
    sticky=>'True',
    password=>"$ARGV[1]",
    );

Also, if you want to be able to specify computers that you wish to send notifications to using MAC addresses rather than IP addresses (in case computers on your network get their addresses via DHCP, and therefore the IP address of the target computer can change from time to time), then you must in addition install the following Perl script. It requires a command-line utility caller arp-scan so install that if you need to – I used to use nmap for this but they changed the output format, making it harder to parse, and arp-scan is much faster anyway. Call it /var/lib/asterisk/agi-bin/gshelper.agi and note that there are two references to 192.168.0… within it that you may need to change to reflect the scope of your local network, if your network’s IP addresses don’t start with 192.168.0.:

#!/usr/bin/perl
use strict;
use warnings;
my @mac;
# Change the following lines to reflect the scope of your local network, if necessary
my @arp = `arp-scan --quiet --interface=eth0 192.168.0.0/24`;
foreach (@arp) {
        if (index($_, "192.168.0.") == 0) {
                @mac = split(" ");
                `/usr/sbin/asterisk -rx "database put growlsend \U$mac[1] $mac[0]"`;
        }
}

Make sure to modify the permissions on both scripts to make them the same as other scripts in that directory (owner and group should be asterisk, and the file should be executable), and also, if you use the gshelper script, make sure to set up a cron job to run it every so often (I would suggest once per hour, but it’s up to you).

Now go to this page and search for the paragraph starting with, “After you have created that file, check the ownership and permissions” (it’s right under a code block, just a bit more than halfway down the page) and if you are using FreePBX follow the instructions from there on out (if you are not using FreePBX then just read that section of the page so you understand how this works, and in any case ignore the top half of the page, it’s talking about a different notification system entirely).  But note that if you use the above code and have the gshelper.agi program running as a cron job, then after the first time it has run while the computer to receive the notifications is online you should be able to use a computer’s MAC address instead of the IP address.  This only works if you’ve used the modified script on this page, not the one shown in the FreePBX How-To.  As an example, instead of

exten => ****525,1,AGI(growlsend.agi,192.168.0.123,GrowlPassWord,525)

as shown in the example there, you could use

exten => ****525,1,AGI(growlsend.agi,01:23:45:AB:CD:EF,GrowlPassWord,525)

(the above is all one line) where 01:23:45:AB:CD:EF is the MAC address of the computer you want to send the notification to.  Once again, just in case you missed it the first time I said it, this won’t work until the gshelper.agi script has been run at least once while the computer to receive the notifications was online.  If for some reason it still doesn’t appear to work, run the nmap command including everything between the two backticks (`) directly from a Linux command prompt and see if it’s finding the computer (depending on the size of your network, it might be several seconds before you see any output, which is why I don’t try to run this in real time while a call is coming in).

If you are NOT running FreePBX, but instead writing your Asterisk dial plans by hand, then you will have to insert a line similar to one of the above examples into your dial plan, except that you don’t need the four asterisks (****) in front of the extension number, and if it’s not the first line in the context, you’ll probably want to use n rather than 1 for the line designator (and, you won’t be putting the line into extensions_custom.conf because you probably don’t have such a file; instead you’ll just put it right in the appropriate section of your dial plan).  In other words, something like this (using extension 525 as an example):

exten => 525,n,AGI(growlsend.agi,192.168.0.123,GrowlPassWord,525)

This line should go before the line that actually connects the call through to extension 525.  I do not write Asterisk dial plans by hand, so that’s about all the help I can give you. And if you don’t write your dial plans by hand, but you aren’t using FreePBX, then I’m afraid you’ll have to ask for help in whatever forum you use for advice on the particular software that you do use to generate dial plans, because I can’t tell you how to insert the above line (or something like it) into your dial plan.

Virtually everything in this article has already been published in one place or another, but I wanted to get it into an article with a relevant title and cut out some of the extraneous explanations and such.  There are links to all the original sources throughout the article, so feel free to follow those if you want more in-depth commentary.

Geolock — a Perl script for Asterisk or FreePBX users to enhance security

 

Important
This is an edited version of a post that originally appeared on a blog called The Michigan Telephone Blog, which was written by a friend before he decided to stop blogging. It is reposted with his permission. Comments dated before the year 2013 were originally posted to his blog.

I created the following Perl script and have been running it for a week or so, and it seems to be working well.  The idea is that this script runs once per minute, and whenever a SIP or IAX extension is registered with your Asterisk system the script looks at the IP address that the extension is registering from, and if that address is outside your home country (the United States by default), the IP address is immediately banned using IPtables.  So, your remote extensions could be anywhere in your home country and connect to your system, but if a hacker from some other nation penetrates your system and somehow guesses one of your passwords, they will (in theory) have less than a minute to do any damage before they are banned. And once they are banned, the rightful user of that extension should still have no difficulty connecting, as long as they are not coming in from outside your home country.

For those of you that don’t have any extensions connecting from outside your home country, consider this another tool in your arsenal of defenses against intrusions.  Combine it with strong passwords, and Fail2Ban with IPtables for additional security.

NOTE THAT THIS SCRIPT IS NOT GUARANTEED TO DO ANYTHING AT ALL, other than take up space on your computer.  IT SHOULD STILL BE CONSIDERED EXPERIMENTAL until there has been more testing on it.  I believe it works properly, but have no way to do extreme testing on it to see if or how it might break. THERE IS NO WARRANTY OF ANY KIND!!!

Prerequisite: Obviously, you must have IPtables installed and functioning properly, and you must install either the Geo::IP or Geo::IP::PurePerl Perl module (do NOT install both!). You can install one of these using Webmin (using Webmin’s Others | Perl modules page), or in any other way you usually install Perl modules (e.g. CPAN). The difference between the two was explained in my original article, as follows:

… there is a Perl module called Geo::IP, which calls the GeoIP C API. If you install that API (when downloading, I’d go into the test/ directory and get the latest beta) using the directions on the linked page, and then install the Perl module (you must do it in that order, or installation of the Perl module will fail), you could run a Perl script that shows the location that your off-site extensions are coming in from. If you don’t want to install the API, or can’t figure out how (not difficult if you follow the directions), you can use the Geo::IP::PurePerl Module which is slower, but does not require the additional C library. Just so you know, GeoIP puts its data file at /usr/local/share/GeoIP/GeoIPCity.dat and they suggest that you go to http://www.maxmind.com/download/geoip/database/ every month or so to grab the latest database (the full link for the country database is currently http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz or you can get a much larger city-level database at http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz — just make sure you don’t grab a CSV version by mistake!). If you need them, there are installation instructions for the database, although they are primarily for the city-level database. If you buy an account, they’ll give you automatic updates, though I can’t imagine it would be that hard to write a script to do that (or maybe Google could help you find one, or see this message thread).

If the above is confusing to you, I’d stick with the Geo::IP::PurePerl Module. That should at least get you going. You also need the Perl Sys::Syslog module (unless you want to omit all the syslog-related instructions), though chances are you may already have that one.

Now, here is the Perl script. I called it geolock.pl (although you are free to name it whatever you want), and I put it in the /var/lib/asterisk/agi-bin/ directory and made it executable. Note this is in a code block so the long lines will overflow the column width, so you need to copy and paste this into a text editor. Also there are certain lines you will need to change, as explained below:

#!/usr/bin/perl
use strict;
use warnings;
use Geo::IP;
use Sys::Syslog;
my $gi = Geo::IP->new(GEOIP_STANDARD);
my ($ext, @peerline, @extension, $ipaddress, $country, $shellcmd);
my $flag = 0;

my @sippeers = `asterisk -rx "sip show peers" | grep -v 192.168.0. | grep ^[1-9] | grep -v "(Unspecified)" | grep / | sort -n`;
# change grep statements in above line to match your local IP range (1st grep) and first digits of extensions (2nd grep)
foreach (@sippeers) {
@peerline = split(" ");
$ipaddress = $peerline[1];
$country = $gi->country_code_by_name($ipaddress);
@extension = split("/",$peerline[0]);
$ext = $extension[0];
#    print "Extension $ext has IP address $ipaddress which is in country $countryn";
if ($country && $country ne 'US') {
$shellcmd = `iptables -D INPUT -p ALL -s $ipaddress -j DROP 2>&1`;
system("iptables -A INPUT -p ALL -s $ipaddress -j DROP");
openlog($0,'pid','user');
syslog('notice', "Banning IP address $ipaddress in $country because Asterisk SIP Extension $ext is connecting from there");
closelog;
if (index($shellcmd, "Bad rule") >= 0) {
$shellcmd = 'echo "This is an automated message - please do not reply. IP address ' . $ipaddress . ' in country ' . $country . ' was banned in iptables because Asterisk SIP extension ' . $ext . ' was connecting from there." | mail -s "IP address banned on Asterisk box" root@localhost';
system($shellcmd);
$flag = 1;
}
}
}

### INCLUDE THIS NEXT SECTION ONLY IF YOU HAVE IAX2 EXTENSIONS ###
my @iaxpeers = `asterisk -rx "iax2 show peers" | grep -v 192.168.0. | grep ^[1-9] | grep -v "(Unspecified)" | grep -v "iax2 peers" | sort -n`;
# change grep statements in above line to match your local IP range (1st grep) and first digits of extensions (2nd grep)
foreach (@iaxpeers) {
@peerline = split(" ");
$ipaddress = $peerline[1];
$country = $gi->country_code_by_name($ipaddress);
$ext = $peerline[0];
#    print "Extension $ext has IP address $ipaddress which is in country $countryn";
if ($country && $country ne 'US') {
$shellcmd = `iptables -D INPUT -p ALL -s $ipaddress -j DROP 2>&1`;
system("iptables -A INPUT -p ALL -s $ipaddress -j DROP");
openlog($0,'pid','user');
syslog('notice', "Banning IP address $ipaddress in $country because Asterisk IAX2 Extension $ext is connecting from there");
closelog;
if (index($shellcmd, "Bad rule") >= 0) {
$shellcmd = 'echo "This is an automated message - please do not reply. IP address ' . $ipaddress . ' in country ' . $country . ' was banned in iptables because Asterisk IAX2 extension ' . $ext . ' was connecting from there." | mail -s "IP address banned on Asterisk box"
root@localhost';
system($shellcmd);
$flag = 1;
}
}
}
### END OF SECTION ONLY NEEDED IF YOU HAVE IAX2 EXTENSIONS ###

if ($flag == 1) {
`asterisk -rx "restart now"`;
}
else {
openlog($0,'pid','user');
syslog('info', "Completed normally");
closelog;
}

These are the things you need to change for your local installation:

  • If you used the Geo::IP::PurePerl module then be sure to change the two references to Geo::IP to Geo::IP::PurePerl.
  • Remove the optional section for IAX2 extensions if you don’t have any of those (but keep it if you have any IAX2 extensions, even if they are only internal ones).
  • In the line(s) “if ($country && $country ne ‘US’) {” change US to the code for your home country if you are somewhere else in the world. You could also create a more expansive conditional statement here to allow multiple countries, or a more restrictive ones if you want to block an IP that doesn’t resolve to any country (I consider that a database error, but maybe you don’t). If you have installed the city-level database, you could even (in theory) test for something other than country, such as a state or province, city, time zone, ISP, etc. This line appears in the SIP section and also the optional IAX2 section, so make sure you change both lines if necessary.
  • There are two instances of root@localhost in the above script (in the SIP section and also the optional IAX2 section), which you should change to a valid e-mail address if you want to receive e-mail notifications when an IP address is banned. If you don’t want to receive such e-mail notifications, then comment out or remove those two lines in their entirety (they start with: $shellcmd = ‘echo “This is an automated message …) and also remove the following system($shellcmd); line(s).

The following apply to the two lines that begin with "my @sippeers” and, in the optional IAX2 section, “my @iaxpeers”:

  • Change “grep -v 192.168.0.” to a regular expression or pattern that will match it IP address of all extensions on your local network.  The pattern as shown works if all your local extensions will be in the range 192.168.0.x. Since you can use a regular expression, you could do something like “grep -v 192.168.[1-5].” which would match any local address from 192.168.1.0 through 192.168.5.255.
  • Change grep ^[1-9] to match the first digit of your extensions – as shown, anything starting with a digit 1 through 9 would be considered an extension.  The idea here is that we only want to look at extensions, not trunks (which you can restrict using permit and deny statements, if necessary). Most trunks don’t begin with a number (when you do a “sip show peers” or “iax2 show peers” listing from the CLI), so this is what separates extensions from trunks.  You may have to get a bit more creative if you have a trunk that starts with a number that overlaps your extensions. If you run the entire section between the backticks (the ` characters) from a Linux command prompt,  it should show you all of your connected non-local (that is, not on your internal network) SIP or IAX extensions (depending on which line you run), but no trunks, local extensions, offline extensions, or header information.

Sharp-eyed readers may observe that I first try to delete an iptables rule before creating it.  That’s because I don’t want to create the same rule multiple times (iptables happily accepts duplicates, unfortunately).  If I get an error when trying to delete the rule, then I know that what follows will be the first attempt to create it, and I should send an e-mail and restart Asterisk when we’re all finished.  Basically, it’s supposed to be a safety mechanism to keep from repeatedly sending the same e-mail, or restarting Asterisk once a minute if for some reason the iptables rule doesn’t “take” at first.

Again, don’t forget to make the Perl script executable, and run it manually a few times to watch the output (uncomment the commented-out “print” lines during initial testing – you can remove them once you are satisfied it’s working as it should be).  After you have run it a few times, from the Linux command prompt do iptables –list and make sure everything looks okay there.

One thing you should be aware of is that when this script detects an intrusion attempt (a connection from outside the United States), after it bans the IP address it restarts Asterisk, which will interrupt any calls in progress.  That’s deliberate; I assume you want to throw the hackers off your system right now, even if it means your users may have to re-dial their calls. Howerver, if for some reason you don’t want to do that, then you can change the line `asterisk -rx “restart now”`; to `asterisk -rx “restart when convenient”`;, which will wait until there is no usage on your system to restart Asterisk.  In that case, good luck to you if the hacker just placed a call to some $100-a-minute destination! In theory IPtables will interrupt the conversation (no audio will pass) BUT that does not mean Asterisk will tear the call down right away – when an extension “just disappears”, Asterisk tends to wait a LONG time to see if it will come back, and if it never does, well that’s what we call a “zombie” call — it just won’t die (at least not until the other end disconnects)! EDIT: If you don’t want to restart the whole system but do want to throw the hacker off NOW, see the modification by “Florent” in the comments below — I have NOT tested his changes personally, but they may do what you want.

The final step is to make this execute once a minute.  I used Webmin’s “System | Scheduled Cron Jobs” to set this up:

cron job setup using WebminBut if you are more comfortable creating a cron job from the command line, by all means, feel free to do so.

Finally, I always say that suggestions for improvement are welcome, and also, if you want to translate this into some other programming language, you have my blessing. Please be sure to test it thoroughly before relying on it, because if someone manages to hack through anyway, I’m not going to pay your phone bill! Once again, the above should be considered experimental code and is not guaranteed to do anything at all.