How to use the FreePBX [macro-dialout-trunk-predial-hook] macro and regular expressions to blacklist or whitelist outgoing calls on all trunks

 

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.

DISCLAIMER: Just because I do something a certain way doesn’t mean YOU should, at least not without extensive testing to make sure that it will do what you want. I do not warrant that anything shown here will in any way save you from getting a high phone bill. If that should happen, consider it a happy coincidence, but if it doesn’t work as you expected, don’t come crying to me. I only offer what I think might work, it’s up to YOU to test it on YOUR system.

This is a followup to my article, Did you know that Asterisk has the ability to evaluate Regular Expressions, though not in the same way as Perl or FreeSWITCH? If you have not read that article, you may want to do that first. Both that article and this one are subject to future revision, if I discover more on this subject.

The problem I have always had on the two systems I help administer is that we don’t allow international calls that cost money. In the past I had resorted to putting a complete list of valid area code patterns in each user’s Outbound Route, and since I had to include patterns for both 11 digit and 10 digit calls, it grew to be a pretty long list. Fortunately, when adding a new user, it was easy to copy the list from an existing user’s outbound account, bring it into a text editor to edit the /extension suffix, and paste it into the new user’s Outbound Route. That worked in FreePBX 2.7, and in versions 2.8 and 2.9 if you use the Swiss Army Knife module, but that module won’t work with FreePBX 2.10 and above.

Unfortunately, as you are very well aware if you have read many of my previous posts, starting in FreePBX 2.8 the FreePBX developers changed the entry method for Outbound Route dial patterns, changing it from an incredibly easy process to enter a list of patterns into a painful, totally stupid method involving individual text boxes for each individual part of each line of each Outbound Route dial pattern. That one change was enough to make me hate FreePBX. The FreePBX developers have been totally unresponsive to those who don’t care for the new method (they pretty much try to pretend such users don’t even exist).

One thing I am NOT going to EVER do is enter a long list of dial patterns using their input method! Someone once said of me that I would spend weeks looking for a way around having to re-type something, when it might only take me a few hours to re-type it, and that’s true. I have even abandoned whole articles because my system crashed and there was no backup, and there was no damn way I was retyping what I had already typed. So in that spirit, I started looking for ways to get around having to enter all those dial patterns for each Outbound Route. I’ve used various stopgap measures including weird things like custom trunks, but that was an ugly solution and leaves extra entries in the Call Detail Records. When I found out that Asterisk could use Regular Expressions, I figured out this method, which is actually much faster, and gives me another way to bypass the limitations of FreePBX.

This uses the [macro-dialout-trunk-predial-hook] macro which is already present, but commented out, in /etc/asterisk/extensions_custom.conf. The thing you need to understand about this macro is that it is called AFTER most of the dial plan associated with both the outbound route and the trunk have been applied. So if you change the called number in either the Outbound Route Dial Patterns or the Trunk Dialed Number Manipulation Rules, those transformations will already have been applied by the time this macro is called. Also, if the calling number has been changed (by setting the Outbound Caller ID in the Trunk, for example), that change have been made by the time this macro is called. And finally, this macro is called when a call is about to go out on ANY trunk in your system (at least I think it does, although I haven’t tested it with every possible trunk type. I’m reasonably sure it is called whenever a call is destined for a SIP trunk, but I am not sure that it gets called if you use one of the other available types of trunks. It does NOT seem to get called when a call is destined for an ENUM trunk). If you don’t want it to act on calls from a certain trunk, you need to test for that and bail out of the macro (I show an example of that below).

The interesting thing about this is that because this macro is invoked so late in the progress of an outgoing call, you can use it as a last chance to look for “undesirable things” or to send notifications of certain happenings. For example, although I don’t show exactly how to do it below, you could have it send an instant message or an e-mail notification when someone calls a specific number, such as 911.

What I am showing here is what I have set up on one of my systems. This assumes that the outbound routes only contain the following Dial Patterns (not all Outbound Routes will contain all of these, and I assume that each user has their own outbound route, or that groups of users may share outbound routes):

1NXXNXXXXXX/ext
NXXNXXXXXX/ext
1aaa+NXXXXXX/ext

…where aaa is the user’s local area code for 7 digit dialing , and ext is the user’s extension (which could be a pattern to allow a group of extensions). In addition, if the user’s service allows the *67 code to be used to block outgoing Caller ID, then these patterns might also be used (with the same substitutions):

*671NXXNXXXXXX/ext
*67NXXNXXXXXX/ext
*671aaa+*67|NXXXXXX/ext

The way I have set it up also ignores any number under 10 digits (allowing it to go through) so if you have dial patterns that allow users to dial provider service codes, or extensions on another connected server, or some such thing, those calls won’t be affected — only calls of 10 digits or longer are checked (this is one reason I prepend the area code to 7 digit calls in the Outbound Routes). Note that there are long lines in the following so if you want to see them in their entirety, you need to cut and paste this into a text editor:

[macro-dialout-trunk-predial-hook]
; this macro intentially left blank so it may be safely overwritten for any custom
; requirements that an installation may have.
;
; MACRO RETURN CODE: ${PREDIAL_HOOK_RET}
;                    if set to "BYPASS" then this trunk will be skipped
;
exten => s,1,NoOp(Trunk ${OUT_${DIAL_TRUNK}} selected)
exten => s,n,ExecIf($[${LEN(${OUTNUM})}<10]?MacroExit())
exten => s,n,ExecIf($["${OUT_${DIAL_TRUNK}}"="SIP/TollFree-Future9"]?MacroExit())
exten => s,n,Set(regx=^([*]67)?1?900[0-9]{7}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?premium)
exten => s,n,Set(regx=^([*]67)?1?8(00|22|33|44|55|66|77|88)[2-9][0-9]{6}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?tollfree)
exten => s,n,Set(regx=^([*]67)?1?[2-9][0-9]{2}976[0-9]{4}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?premium)
exten => s,n,Set(regx=^([*]67)?1?(201|202|203|205|206|207|208|209|210|212|213|214|215|216|217|218|219|220|223|224|225|228|229|231|234|239|240|248|251|252|253|254|256|260|262|267|269|270|272|274|276|279|281|301|302|303|304|305|307|308|309|310|312|313|314|315|316|317|318|319|320|321|323|325|327|330|331|332|334|336|337|339|346|347|351|352|360|361|364|380|385|386|401|402|404|405|406|407|408|409|410|412|413|414|415|417|419|423|424|425|430|432|434|435|440|442|443|445|458|463|469|470|475|478|479|480|484|501|502|503|504|505|507|508|509|510|512|513|515|516|517|518|520|530|534|539|540|541|551|559|561|562|563|564|567|570|571|573|574|575|580|585|586|601|602|603|605|606|607|608|609|610|612|614|615|616|617|618|619|620|623|626|628|629|630|631|636|641|646|650|651|657|660|661|662|667|669|678|680|681|682|701|702|703|704|706|707|708|712|713|714|715|716|717|718|719|720|724|725|726|727|731|732|734|737|740|743|747|754|757|760|762|763|765|769|770|772|773|774|775|779|781|785|786|801|802|803|804|805|806|808|810|812|813|814|815|816|817|818|828|830|831|832|838|843|845|847|848|850|854|856|857|858|859|860|862|863|864|865|870|872|878|901|903|904|906|907|908|909|910|912|913|914|915|916|917|918|919|920|925|928|929|930|931|934|936|937|938|940|941|947|949|951|952|954|956|959|970|971|972|973|978|979|980|984|985|986|989)[2-9][0-9]{6}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?usa)
exten => s,n,Set(regx=^([*]67)?1?(204|226|236|249|250|289|306|343|365|367|403|416|418|431|437|438|450|506|514|519|548|579|581|587|604|613|639|647|705|709|778|780|782|807|819|825|867|873|879|902|905)[2-9][0-9]{6}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?canada)
exten => s,n,Set(regx=^0118835100[0-9]{8}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?inum)
exten => s,n,NoOp(${CALLERID(num)} is calling ${OUTNUM} which is a NOT a permitted call)
exten => s,n,Jabbersend(asterisk,system_administrator_address@gmail.com,${CALLERID(num)} attempted to call ${OUTNUM} at ${STRFTIME(${EPOCH},,%l:%M:%S %p %Z on %A %B %e)})
exten => s,n,Playback(custom/sorry-outside-us-canada)
exten => s,n,Macro(hangupcall,)
exten => s,n,Set(PREDIAL_HOOK_RET=BYPASS)
exten => s,n,MacroExit()
exten => s,n(premium),NoOp(${CALLERID(num)} is calling ${OUTNUM} which is a premium number call - NOT perrmitted)
exten => s,n,Jabbersend(asterisk,system_administrator_address@gmail.com,${CALLERID(num)} attempted to call a premium number ${OUTNUM} at ${STRFTIME(${EPOCH},,%l:%M:%S %p %Z on %A %B %e)})
exten => s,n,Playback(silence/1&sorry-cant-let-you-do-that3)
exten => s,n,Macro(hangupcall,)
exten => s,n,Set(PREDIAL_HOOK_RET=BYPASS)
exten => s,n,MacroExit()
exten => s,n(usa),NoOp(${CALLERID(num)} is calling ${OUTNUM} which is a USA call)
exten => s,n,MacroExit()
exten => s,n(canada),NoOp(${CALLERID(num)} is calling ${OUTNUM} which is a Canada call)
exten => s,n,MacroExit()
exten => s,n(tollfree),NoOp(${CALLERID(num)} is calling ${OUTNUM} which is a toll free call)
exten => s,n,MacroExit()
exten => s,n(inum),NoOp(${CALLERID(num)} is calling ${OUTNUM} which is an inum call)
exten => s,n,MacroExit()
exten => h,1,Macro(hangupcall,)

Now here’s a breakdown of the above, in which I try to explain what I have done. I will note at the outset that some may think I am using two lines where one will do, by putting the regular expression into a variable when I could just evaluate it directly in the REGEX function in the following line. However, when I was researching the use of regular expressions in Asterisk, I read on several pages that at least some versions of Asterisk have issues parsing a regular expression if it contains certain characters that also have a meaning to Asterisk, unless you put the regex into a string variable first and then use that in the REGEX function. I don’t know if that’s always true but I figured better safe than sorry, and besides, it’s much less confusing to me when I do it that way. But if you want to try combining the two-line pairs into one, feel free — it’s your system, and your headache if it doesn’t work. Also, remember that Asterisk doesn’t parse regular expressions in the same way that some other software (such as Perl) does, so if you try to simplify my regex patterns you might run into trouble. So with that said, let’s begin our tour.

This part of the macro is already present in extensions_custom.conf. You only need to uncomment the lines that are not obviously comments:

[macro-dialout-trunk-predial-hook]
; this macro intentially left blank so it may be safely overwritten for any custom
; requirements that an installation may have.
;
; MACRO RETURN CODE: ${PREDIAL_HOOK_RET}
;                    if set to "BYPASS" then this trunk will be skipped
;
exten => s,1,NoOp(Trunk ${OUT_${DIAL_TRUNK}} selected)

The next line is the first of many places we will use MacroExit() to bail out of the macro. This checks to see in the number called was 9 digits or less. We don’t want to block 911 calls, intra-company calls to other extensions, calls to another network that uses extensions of 9 digits or less, etc.:

exten => s,n,ExecIf($[${LEN(${OUTNUM})}<10]?MacroExit())

This bails out if the call is going out a specific trunk. In this case we don’t want to mess with toll-free calls going to Future9 because they are sent using a rather unusual pattern that would not be recognized by the regular expressions below.

exten => s,n,ExecIf($["${OUT_${DIAL_TRUNK}}"="SIP/TollFree-Future9"]?MacroExit())

Here’s an example of blocking a call that conforms to a specific pattern, in this case 1-900-XXX-XXXX or just 900-XXX-XXXX. It will block calls with or without the *67 (privacy) prefix, and with or without the leading “1”. If you don’t allow *67 calling then just remove the [*]67)? from the start of the expression. If it matches that pattern, it is transferred to the label “premium”, which we’ll get to in a bit:

exten => s,n,Set(regx=^([*]67)?1?900[0-9]{7}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?premium)

Here’s an example of allowing a call that matches a specific pattern, in this case a NANP toll-free call, again with or without the *67 (privacy) prefix (as is the case with most of these regular expressions — note that your outbound route(s) would also need to allow the *67 pattern). These calls are transferred to the label “tollfree”, which we’ll also talk about later:

exten => s,n,Set(regx=^([*]67)?1?8(00|22|33|44|55|66|77|88)[2-9][0-9]{6}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?tollfree)

Here we block calls that match the pattern 1-[2-9]XX-976-XXXX or just [2-9]XX-976-XXXX (with or without a *67 prepend) and send them to “premium”:

exten => s,n,Set(regx=^([*]67)?1?[2-9][0-9]{2}976[0-9]{4}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?premium)

Here we test to see if the call is going to any USA area code. This is a really long regex but it works. If the pattern matches, it transfers to the “usa” label

exten => s,n,Set(regx=^([*]67)?1?(201|202|203|205|206|207|208|209|210|212|213|214|215|216|217|218|219|220|223|224|225|228|229|231|234|239|240|248|251|252|253|254|256|260|262|267|269|270|272|274|276|279|281|301|302|303|304|305|307|308|309|310|312|313|314|315|316|317|318|319|320|321|323|325|327|330|331|332|334|336|337|339|346|347|351|352|360|361|364|380|385|386|401|402|404|405|406|407|408|409|410|412|413|414|415|417|419|423|424|425|430|432|434|435|440|442|443|445|458|463|469|470|475|478|479|480|484|501|502|503|504|505|507|508|509|510|512|513|515|516|517|518|520|530|534|539|540|541|551|559|561|562|563|564|567|570|571|573|574|575|580|585|586|601|602|603|605|606|607|608|609|610|612|614|615|616|617|618|619|620|623|626|628|629|630|631|636|641|646|650|651|657|660|661|662|667|669|678|680|681|682|701|702|703|704|706|707|708|712|713|714|715|716|717|718|719|720|724|725|726|727|731|732|734|737|740|743|747|754|757|760|762|763|765|769|770|772|773|774|775|779|781|785|786|801|802|803|804|805|806|808|810|812|813|814|815|816|817|818|828|830|831|832|838|843|845|847|848|850|854|856|857|858|859|860|862|863|864|865|870|872|878|901|903|904|906|907|908|909|910|912|913|914|915|916|917|918|919|920|925|928|929|930|931|934|936|937|938|940|941|947|949|951|952|954|956|959|970|971|972|973|978|979|980|984|985|986|989)[2-9][0-9]{6}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?usa)

Same as the previous except now we’re testing for Canadian area codes, and if successful transferring to the “canada” label:

exten => s,n,Set(regx=^([*]67)?1?(204|226|236|249|250|289|306|343|365|367|403|416|418|431|437|438|450|506|514|519|548|579|581|587|604|613|639|647|705|709|778|780|782|807|819|825|867|873|879|902|905)[2-9][0-9]{6}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?canada)

(NOTE: It should be noted that a few new area codes are added every year. You can see a list of the most recently added ones here: NPAs Introduced over the Last 10 Years. You should check this periodically and add the new USA and Canadian area codes to the appropriate sections above.)

Google Voice used to complete calls to iNum numbers for free, so this test allowed for those. Now I don’t use this section, but leave it here as an example. Back when these calls were free, this was the only “international” format call that was permitted on our system:

exten => s,n,Set(regx=^0118835100[0-9]{8}$)
exten => s,n,GotoIf($[${REGEX("${regx}" ${OUTNUM})} = 1]?inum)

If we get here, the call failed all the tests that would indicate it might be a permitted call. I play a custom recorded message that tells the caller that their call is not allowed, possibly because it’s not to a destination in the United States or Canada. Just prior to playing the message, I use the Jabbersend function to send myself an instant message showing that a call failed. I mainly do this so I can determine whether the caller really tried to dial a number that’s not permitted, or if there’s an error in my logic. For example, someone could be trying to place calls to a valid area code that somehow got omitted from the regular expression. Note that Jabbersend will only work if you have jabber.conf configured correctly AND the res_jabber.so module is loading when Asterisk starts up. If you have ever configured your server to place Google Voice calls using Asterisk’s jabber/gtalk support, those things are probably true, but it still may be a bit tricky to get working, however I’m not going to discuss that in this article — just search for Asterisk Jabbersend in a search engine if you need help:

exten => s,n,NoOp(${CALLERID(num)} is calling ${OUTNUM} which is a NOT a permitted call)
exten => s,n,Jabbersend(asterisk,system_administrator_address@gmail.com,${CALLERID(num)} attempted to call ${OUTNUM} at ${STRFTIME(${EPOCH},,%l:%M:%S %p %Z on %A %B %e)})
exten => s,n,Playback(custom/sorry-outside-us-canada)
exten => s,n,Macro(hangupcall,)
exten => s,n,Set(PREDIAL_HOOK_RET=BYPASS)
exten => s,n,MacroExit()

One further note about the last three lines in the above section: Setting the PREDIAL_HOOK_RET variable and the MacroExit() are probably superfluous because the Macro(hangupcall,) will terminate the call, but I left them there for safety. Note that if you were to comment out the Macro(hangupcall,), the last two lines would execute, and then if more than one trunk was specified in the Outbound Route, it would keep retrying the call on each of those trunks (and the caller would hear the error message multiple times, and you’d get multiple IM’s). Although I can’t personally envision such a situation, there might be situations where trying the additional trunks would be desirable.

Next is where the call goes if the caller attempted to dial a premium (900 or 976) number. This is similar to the previous section, except the recording played to the caller is one second of silence followed by a standard system recording. The silence is necessary to keep the first word or two of the recording from being cut off on some clients. Also the IM I send to myself is slightly different. And the same comment applies on the last three lines. I know, I could have done a Goto to the same three lines in the previous section, but that would have only saved me two lines, and since the last two lines are superfluous anyway, if you are that tight on space you can just delete them.

exten => s,n(premium),NoOp(${CALLERID(num)} is calling ${OUTNUM} which is a premium number call - NOT perrmitted)
exten => s,n,Jabbersend(asterisk,system_administrator_address@gmail.com,${CALLERID(num)} attempted to call a premium number ${OUTNUM} at ${STRFTIME(${EPOCH},,%l:%M:%S %p %Z on %A %B %e)})
exten => s,n,Playback(silence/1&sorry-cant-let-you-do-that3)
exten => s,n,Macro(hangupcall,)
exten => s,n,Set(PREDIAL_HOOK_RET=BYPASS)
exten => s,n,MacroExit()

The next sections contain the usa, canada, tollfree, and inum labels. All they do is print an appropriate message to the CLI and exit. Rather than jump to these labels, I could have simply used statements of the form ExecIf($[${REGEX(“${regx}” ${OUTNUM})} = 1]?MacroExit()) and left out this entire next section, but I like the idea of being able to see which regex allowed the call in the CLI, and this also allows for the possibility of additional code. For example, I could insert code that keeps a running tally of calls broken down by their destination. I have no need to do anything like that, but maybe you do:

exten => s,n(usa),NoOp(${CALLERID(num)} is calling ${OUTNUM} which is a USA call)
exten => s,n,MacroExit()
exten => s,n(canada),NoOp(${CALLERID(num)} is calling ${OUTNUM} which is a Canada call)
exten => s,n,MacroExit()
exten => s,n(tollfree),NoOp(${CALLERID(num)} is calling ${OUTNUM} which is a toll free call)
exten => s,n,MacroExit()
exten => s,n(inum),NoOp(${CALLERID(num)} is calling ${OUTNUM} which is an inum call)
exten => s,n,MacroExit()

This last line is important and was omitted from the default macro. It handles the case where the caller hangs up while this code is executing. Although that window is only a small fraction of second, bad things could happen if the caller does manage to hang up within that window and this statement isn’t present:

exten => h,1,Macro(hangupcall,)

Well, that’s how I’m doing it right now. It may not be how I do it two months from now, but this seems better than any method I have used thus far.

I will point out, for whatever it’s worth, that you COULD use an Asterisk dial command right in this macro. So, for example, let’s say you had a trunk that was real expensive at a certain time of the day but much cheaper during another. You COULD test to see if the call is being made within the “cheap” time and if so just exit, but if not you could execute a dial command that looks like the one normally executed, which is…

exten => s,n,Dial(${OUT_${DIAL_TRUNK}}/${OUTNUM},300,${DIAL_TRUNK_OPTIONS})

…except that in place of ${OUT_${DIAL_TRUNK}} you could specify a different trunk, for example SIP/OtherTrunk. And of course you could use this macro to change the dial trunk options for specific trunks, or to play a recording to callers if their call is going out over an “expensive” trunk. In fact, I can see where this macro could be used to get to get around some of the FreePBX syntax checking that has plagued me in the past.

It should probably be noted that if you watch these macros execute in the Asterisk CLI, every time one of them is executed you may see a WARNING message such as this:

[2012-11-22 11:48:12] WARNING[30588][C-00000055]: pbx.c:1585 pbx_exec: The application delimiter is now the comma, not the pipe. Did you forget to convert your dialplan? (Set(regx=^([\*]{2}1)?([\*]67)?W?1?8(00|22|33|44|55|66|77|88)[2-9][0-9]{6}$))

This warning doesn’t apply in this situation because the pipe IS the correct character to use in a regular expression, when you are matching a single regular expression out of several possible regular expressions. See this page on Alternation with The Vertical Bar or Pipe Symbol for more information, with the caveat that Asterisk’s expression engine is a bit different from that used in other software, and therefore may not recognize syntax that would be perfectly valid in another application. Therefore, not all examples shown on various regular expression tutorials or reference pages will work. If you wish to try a particular syntax, be sure to do a test run or two and make sure it works. But, Asterisk’s regular expression engine DOES recognize the pipe character when used as shown in the above examples, but spits out these warning messages anyway, even though the pipe characters are part of the regular expression, and are not being used as application delimiters. You can safely ignore the warnings – they are a nuisance, but are meaningless in this particular situation. If you really don’t want to see those warnings, use a text editor to go to /etc/asterisk/asterisk.conf and uncomment this line:

;dontwarn = yes                 ; Disable some warnings.

This will disable some non-essential warnings such as this one. If it were me, I would have named this option “stopwhining” rather than “dontwarn”, because Asterisk does tend to complain a lot about things that don’t matter and that you can’t do anything about. 🙂

Once again, I will mention that articles in this blog (and especially one such as this) are subject to future editing as I discover more about the subject. If you find any neat uses for the [macro-dialout-trunk-predial-hook] macro (ESPECIALLY if they allow you to do something that FreePBX tries to stop you from doing, but that’s perfectly allowable in Asterisk), feel free to leave a comment!

1 thought on “How to use the FreePBX [macro-dialout-trunk-predial-hook] macro and regular expressions to blacklist or whitelist outgoing calls on all trunks

  1. Please be aware that Goggle Voice used to handle iNum calls for free, but apparently no longer does so. Therefore you might want to omit the iNum-related lines unless you have another way to send those calls out.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.