Virtual voice/VoIP PBX based on Mikrotik’s metaROUTER functionality (Asterisk + voice recognition)

For some time I’ve been looking for solution which would help allow me to run VoIP (Asterisk PBX) call-center services for business customers painlessly and in a not expensive way. Solutions like Slican ITS-0286, Platan Prima Mini and others are fine and functional but in general – they are too expensive for simple PBX scenarios and on the other hand – too simple to serve sophisticated PBX logic. What’s more – when I am talking about business customer I mean a full, dedicated optical fiber connection with managed ethernet switch at the end. In some cases – we will need a L2/L3 device (termination of fiber, router) and PBX device (for example – Platan Prima). More devices means more points of failure. Here is where Mikrotik comes in handy.

Solution which I describe in this article involves use only a L2 ethernet switch (also capable of QinQ). We can provide DHCP service in dedicated vlan, we can also give a management access to “his” router device also virtualised on metaROUTER. Same situation applies to PBX VoIP service. I create a fully-functional Asterisk server on metaROUTER configured especially for this particular customer. Of course – the “host” MikroTik is on the operator side (being router/pbx for many customers) simultaneously.

This article will cover these scenarios:
– automatic voice recognition (changing voice records to emails)
– securing the access to IVR instance (voice or DTMF password method)
– simple example of “reading out loud” data in SQL server

Our “host” device for all the services is MikroTik RB1100AH MikroTik – RB1100AH – specs, which does the job well. It is able to handle 600-700Mbit of traffic without any packet loss while it is hosting 5 instances of virtual router (OpenWRT) and 5 instances of Asterisk PBX as well as some VPNs, some queues and firewall rules.

Images of OpenWRT suitable for metaROUTER can be found easily on the Internet or you can try build your own. There are many of detailed tutorials out there, so I won’t repeat someone’s work. I use OpenWRT 14.07 Barrier Breaker (as an router image) and OpenWRT 8.09 Kamikaze (as an Asterisk PBX – ver. 1.8.11.1). Be sure to edit /etc/opkg.conf just after install and add repositiories to make your work easier. To make Asterisk fully capable of configurations visible in this article I needed some missing packages installed.

I won’t mention installing and configuring OpenWRT at all – this was covered multiple times. Just one word from me – make sure that “vif” interfaces created by RouterOS must be properly binded to specified bridge interfaces. Not a big deal, but worth mentioning. Let’s focus on PBX configuration and its possibilities.

First of all – get an OpenWRT image with Asterisk for PPC architecture:
OpenWRT 8.09 + Asterisk – PPC

Here is the useful repository link:
Archive OpenWRT 8.09

Copy the image to Mikrotik and start its import. I created a VM with 64MB of memory – customize it for your needs.

import-image file-name=openwrt-metarouter-asterisk-ppc.tar.gz memory-size=64

Rename your virtual machine – when you create 8 of them you want this to be easy to read. Now let’s configure virtual interfaces. This command will create an “eth0” interface and bind it to bridge-LAN bridge. In such case I mostly create a static lease to make sure it gets same address every single time.

[admin@MikroTik] /metarouter> interface add virtual-machine=asterisk type=dynamic dynamic-bridge=bridge-LAN

Here is how my /etc/config/network file looks (DHCP on eth0):

root@meta-asterisk:~# cat /etc/config/network
# Copyright (C) 2006 OpenWrt.org

config interface loopback
option ifname lo
option proto static
option ipaddr 127.0.0.1
option netmask 255.0.0.0

config interface lan
option ifname eth0
option proto dhcp

After first login we change password, exchange keys and configure firewall. Next step is clearing Asterisk config files.

cd /etc/asterisk
cat /dev/null > sip.conf
cat /dev/null > voicemail.conf
cat /dev/null > extensions.conf

You can install ssmtp package from repo to use simple e-mail sending from Asterisk itself. Here is ssmtp’s configuration:

root@meta-asterisk:/etc/ssmtp# cat ssmtp.conf
Hostname=DOMAIN
root=SOURCE_ADDRESS
mailhub=MAIL_SERVER:465
rewriteDomain=
UseTLS=YES
UseSTARTTLS=NO
AuthUser=LOGIN
AuthPass=PASSWORD
FromLineOverride=YES

Now let’s start with configuring Asterisk – define VoIP account. (/etc/asterisk/sip.conf)

[general]
allowsubscribe=yes
disallow=all
allow=alaw
allowexternalinvites=no
allowguest=no
allowoverlap=yes
autocreatepeer=no
autodomain=no
bindaddr=0.0.0.0
bindport=5060
canreinvite=no
checkmwi=60
compactheaders=no
defaultexpirey=60
dtmfmode=rfc2833
dumphistory=no
ignoreregexpire=yes
language=pl
localnet=0.0.0.0/0.0.0.0
insecure=no
maxexpiry=120
nat=force_rport,comedia
notifyringing=yes
pedantic=no
progressinband=yes
promiscredir=yes
qualify=no
realm=DOMAIN
recordhistory=no
registerattempts=0
registertimeout=30
relaxdtmf=yes
rtautoclear=no
rtcachefriends=no
rtsavesysname=no
rtpholdtimeout=180
rtpkeepalive=180
rtptimeout=180
rtupdate=no
sendrpid=no
sipdebug=no
srvlookup=no
trustrpid=no
useclientcode=no
usereqphone=no
rtignoreregexpire=yes
useragent=MR-Asterisk
videosupport=no
callevents=no

Now let’s register it.

;; Rejestrujemy konto 1
register => 123456789:silneHASLO-vOIP12%@HOST/123456789

[123456789]
type=friend
username=123456789
secret=goodPASSWORD-vOIP12%
context=incoming
disallow=all
allow=alaw,ulaw
qualify=yes
host=HOST
defaultip=HOST
canreinvite=no
insecure=port,invite
call-limit=5

Let’s log into asterisk CLI, reload sip accounts and verify registration.

root@meta-asterisk:/etc/asterisk# asterisk -r

Asterisk 1.8.11.1, Copyright (C) 1999 - 2012 Digium, Inc. and others.
Created by Mark Spencer <markster@digium.com>
Asterisk comes with ABSOLUTELY NO WARRANTY; type 'core show warranty' for details.
This is free software, with components licensed under the GNU General Public
License version 2 and other licenses; you are welcome to redistribute it under
certain conditions. Type 'core show license' for details.
=========================================================================
Connected to Asterisk 1.8.11.1 currently running on meta-asterisk (pid = 2146)
meta-asterisk*CLI> sip reload
meta-asterisk*CLI> sip show peers
Name/username Host Dyn Forcerport ACL Port Status
123456789/123456789 IP_VOIP_SERVER N 5060 OK (3 ms)
1 sip peers [Monitored: 1 online, 0 offline Unmonitored: 0 online, 0 offline]

Now let’s create two accounts for VoIP phones @ customers place. We would also need to apply same configuration to the phones itself.

[10]
callerid=10
type=friend
username=10
accountcode=10
secret=passINTERNaL10-tajn3!
host=dynamic
context=outgoing
qualify=yes
keepalive=yes
;nat=no
nat=force_rport,comedia
disallow=all
allow=alaw
limitonpeers=yes
call-limit=3

[11]
callerid=11
type=friend
username=11
accountcode=11
secret=passINTErnAL11-tajn3!
host=dynamic
context=outgoing
qualify=yes
keepalive=yes
;nat=no
nat=force_rport,comedia
disallow=all
allow=alaw
limitonpeers=yes
call-limit=3

Now let’s create simple dialplan (/etc/asterisk/extension.conf). Make your own modifications if necessary.

[general]
static=yes
writeprotect=yes
[globals]
CONSOLE=Console/dsp
SOUNDPATH=/etc/asterisk/SOUNDS/
RECINPATH=/var/www/RECORDS
RECOUTPATH=/var/www/RECORDS
RECINPATH-IPB=/var/www/ipb/RECORDS
RECOUTPATH-IPB=/var/www/ipb/RECORDS
MONITOR_EXEC=/usr/bin/sox -m

[incoming]

exten => 123456789,1,NoOp("Incoming | Number : ${CALLERID(num)}")

;;;
;;; PBX Company
;;;
exten => 123456789,n,Wait(1)
exten => 123456789,n,NoOp("Incoming | Number - ${CALLERID(num)}")
exten => 123456789,n,System(echo "To: EMAIL_ADDRESS@DOMAIN.LTD" > /etc/ssmtp/mail-body && echo "Subject: Incoming connection from number: ${CALLERID(num)}" >> /etc/ssmtp/mail-body && echo " " >> /etc/ssmtp/mail-body && echo " " >> /etc/ssmtp/mail-body && cat /etc/ssmtp/mail-body | sendmail -t)
;calling internal 10, then 11
exten => 123456789,n,Dial(sip/10,25,tTr);
exten => 123456789,n,Dial(sip/11,20,tTr);
exten => 123456789,n,Hangup()

Finally – reload dialplan and try making call while watching verbose on asterisk console.

asterisk -r
dialplan reload
core set verbose 99

At first – you will receive email about incoming call with a topic: “Incoming connection from number: XXXXXX”. Next step will be calling SIP client 10. After 25 seconds (if no one takes the call) SIP client 11 will ring. This is one of the simplest and easy way to build a PBX solution. Remember that variety of variables which you can use in dialplan is extremely wide. Building your complex, professional solution with analytics (send by email or presented on WWW) is fairly easy. Next step is a voice recognition (voice2text / text2voice). Let me show you some examples.

You need an API access to Google Services. Log in into your account and go to: Console Developers Google, now you need to create project and enable API access to “Cloud Speech API”. We will use a AGI script written by Lefteris Zafirisa. Some more information here: Speech recognition for Asterisk. After fullfilling all requirements (it may take some time) we can try some examples.

Start editing /etc/asterisk/asterisk.conf. You need to define the location of AGI scripts:

astagidir => /var/lib/asterisk/agi-bin/

Now let’s copy AGI scripts (speech-recog.agi and googletts.agi) to that location. We will try to read some text, make a voice record and check the transcription quality.

exten => 123456789,1,Answer()
exten => 123456789,n,Wait(1)

; text2voice
exten => 123456789,n,agi(googletts.agi,"Welcome. First test.",en)
exten => 123456789,n,Wait(1)
exten => 123456789,n,agi(googletts.agi,"Say something after a beep.",en)
exten => 123456789,n(record),agi(speech-recog.agi,en_EN)
exten => 123456789,n,Verbose(1,Script returned data: ${confidence} , ${utterance})

; check-quality-of-transcription
exten => 123456789,n(success),GotoIf($["${confidence}" > "0.8"]?playback:retry)

; play-text
exten => 123456789,n,NoOp(${confidence})
exten => 123456789,n(playback),agi(googletts.agi,"Accuracy of transcription is: ${confidence:2:2}",en)
exten => 123456789,n,Wait(1)
exten => 123456789,n,agi(googletts.agi,"This is what you recorded.",en)
exten => 123456789,n,agi(googletts.agi,"${utterance}",en)
exten => 123456789,n,goto(end)

; repeat if quality of transcription is <80%
exten => 123456789,n(retry),agi(googletts.agi,"Try saying it once more",en)
exten => 123456789,n,goto(record)

exten => 123456789,n(fail),agi(googletts.agi,"No data.",en)
exten => 123456789,n(end),Hangup()

Ok. We’ve managed so far to register outbound VoIP account, prepare “internal” VoIP accounts for VoIP phones, make inbound and outbound call and change some voice to text (and opposite). Let’s have a look at a simple NOC project realised with this solution:

exten => VOIP_NUMBER,1,Answer()
exten => VOIP_NUMBER,n,Wait(1)
exten => VOIP_NUMBER,n(playback),agi(googletts.agi,"Welcome to Blogtech company",en)
exten => VOIP_NUMBER,n,Wait(1)
exten => VOIP_NUMBER,n,agi(googletts.agi,"Please answer the questions and submit the answer by pressing the pound key.",en)
exten => VOIP_NUMBER,n,Wait(1)

exten => VOIP_NUMBER,n(playbackfirma),agi(googletts.agi,"Please provide a name of your company.",en)
exten => VOIP_NUMBER,n(recordfirma),agi(speech-recog.agi,en_EN)
exten => VOIP_NUMBER,n,Verbose(1,Transcription quality: ${confidence}, Company name: ${utterance})
exten => VOIP_NUMBER,n,Set(firmazgloszenie=${utterance})
exten => VOIP_NUMBER,n,Set(confidence=0)
exten => VOIP_NUMBER,n,Wait(1)

exten => VOIP_NUMBER,n(playbackmiasto),agi(googletts.agi,"In what city your company is located at?",en)
exten => VOIP_NUMBER,n(recordmiasto),agi(speech-recog.agi,en_EN)
exten => VOIP_NUMBER,n,Verbose(1,Transcription quality: ${confidence}, City: ${utterance})
exten => VOIP_NUMBER,n,Set(firmamiasto=${utterance})
exten => VOIP_NUMBER,n(successmiasto),GotoIf($["${confidence}" > "0.6"]?playbackulica:retrymiasto)
exten => VOIP_NUMBER,n(retrymiasto),agi(googletts.agi,"Please repeat the name of the city.",en)
exten => VOIP_NUMBER,n,goto(recordmiasto)
exten => VOIP_NUMBER,n,Set(confidence=0)
exten => VOIP_NUMBER,n,Wait(1)

exten => VOIP_NUMBER,n(playbackulica),agi(googletts.agi,"Please provide streets name and number where your services are supplied.",en)
exten => VOIP_NUMBER,n(recordulica),agi(speech-recog.agi,en_EN)
exten => VOIP_NUMBER,n,Verbose(1,Transcription quality: ${confidence}, Street: ${utterance})
exten => VOIP_NUMBER,n,Set(firmaulica=${utterance})
exten => VOIP_NUMBER,n(successulica),GotoIf($["${confidence}" > "0.7"]?playbackzasilanie:retryulica)
exten => VOIP_NUMBER,n(retryulica),agi(googletts.agi,"Please repeat the name of the street and its number.",en)
exten => VOIP_NUMBER,n,Set(firmaulica=${utterance})
exten => VOIP_NUMBER,n,Set(confidence=0)
exten => VOIP_NUMBER,n,Wait(1)

exten => VOIP_NUMBER,n(playbackzasilanie),agi(googletts.agi,"Please answer yes or no if power supply was verified at your location.",en)
exten => VOIP_NUMBER,n(recordzasilanie),agi(speech-recog.agi,en_EN)
exten => VOIP_NUMBER,n,Verbose(1,Transcription quality: ${confidence}, Power verified: ${utterance})
exten => VOIP_NUMBER,n,Set(firmazasilanie=${utterance})
exten => VOIP_NUMBER,n(successzasilanie),GotoIf($["${confidence}" > "0.6"]?playbacknumer:retryzasilanie)
exten => VOIP_NUMBER,n(retryzasilanie),agi(googletts.agi,"Please answer yes or no if power supply was verified at your location.",pl)
exten => VOIP_NUMBER,n,Set(confidence=0)
exten => VOIP_NUMBER,n,Wait(1)

exten => VOIP_NUMBER,n(playbacknumer),agi(googletts.agi,"Please provide a contact number to a technician available at the location by using your phone dialpad",en)
exten => VOIP_NUMBER,n,Read(digito,,9)
exten => VOIP_NUMBER,n,Set(firmanumer=${digito})
exten => VOIP_NUMBER,n,Wait(1)

exten => VOIP_NUMBER,n(playbackprzyjete),agi(googletts.agi,"Thank you. All of information are gathered successfully and are being proceded. We will contact you soon.",en)

exten => VOIP_NUMBER,n,System(echo "To: MAIL@DOMAIN.LTD" > /tmp/noc-mail)
exten => VOIP_NUMBER,n,NoOp(${firmazgloszenie})
exten => VOIP_NUMBER,n,NoOp(${firmamiasto})
exten => VOIP_NUMBER,n,NoOp(${firmaulica})
exten => VOIP_NUMBER,n,NoOp(${firmazasilanie})
exten => VOIP_NUMBER,n,NoOp(${firmanumer})
exten => VOIP_NUMBER,n,System(echo "Subject: PBX: NOC Ticket from Company: "${firmazgloszenie}" - "${CALLERID(NUM)}" - "${firmaulica}" - "${firmamiasto} >> /tmp/noc-mail)
exten => VOIP_NUMBER,n,System(echo "" >> /tmp/noc-mail)
exten => VOIP_NUMBER,n,System(echo "Incoming call from ${CALLERID(NUM)} na ${EXTEN} on time: ${STRFTIME(%C%m%d%y%H%M)}" >> /tmp/noc-mail)
exten => VOIP_NUMBER,n,System(echo "" >> /tmp/noc-mail)
exten => VOIP_NUMBER,n,System(echo "Company: "${firmazgloszenie} >> /tmp/noc-mail)
exten => VOIP_NUMBER,n,System(echo "City: "${firmamiasto} >> /tmp/noc-mail)
exten => VOIP_NUMBER,n,System(echo "Street: "${firmaulica} >> /tmp/noc-mail)
exten => VOIP_NUMBER,n,System(echo "Contact number: "${firmanumer} >> /tmp/noc-mail)
exten => VOIP_NUMBER,n,System(echo "Power verified: "${firmazasilanie} >> /tmp/noc-mail)
exten => VOIP_NUMBER,n,System(sendmail -t -f MAIL@DOMAIN.LTD < /tmp/noc-mail)
exten => VOIP_NUMBER,n,Hangup()

This scenario involves – creating ticket directly from voice of a customer, transcripting it to email message. If you find it necessary – you can attach answers (as an WAV) to email message to help ‘decipher’ if needed. After the tests I would say that accuracy of voice recognition reaches 80%. Worth mentioning is fact that gathering telephone number by DTMF is 100% accurate what can be helpful to contact with your caller.

Now let’s assume that access to our IVR must be restricted. We can achieve this on many ways but I will focus on two methods – voice password and DTMF code.

Here are the configurations for both.
Voice password:

exten => VOIP_NUMBER,1,Answer()
exten => VOIP_NUMBER,n,Wait(1)
exten => VOIP_NUMBER,n,agi(googletts.agi,"Hello. This is super-secure PBX system. Please provide password.",en)
exten => VOIP_NUMBER,n(record),agi(speech-recog.agi,en_EN)
exten => VOIP_NUMBER,n,Verbose(1,Returned data: ${confidence} , ${utterance})
exten => VOIP_NUMBER,n(success),GotoIf($["${utterance}" = "secret password"]?granted)
exten => VOIP_NUMBER,n,agi(googletts.agi,"Wrong password. Access denied.",en)
exten => VOIP_NUMBER,n,agi(googletts.agi,"Good bye.",pl)
exten => VOIP_NUMBER,n,Hangup()
exten => VOIP_NUMBER,n(granted),agi(googletts.agi,"Access granted.",en)
exten => VOIP_NUMBER,n,Hangup()

DTMF password:

exten => VOIP_NUMBER,1,Answer()
exten => VOIP_NUMBER,n,Wait(1)
exten => VOIP_NUMBER,n,agi(googletts.agi,"Hello. Please provide password on your dialpad.",en)
exten => VOIP_NUMBER,n,WaitExten(5)
exten => 1410,1,Answer()
exten => 1410,n,agi(googletts.agi,"Password correct.",en)
exten => 1410,n,agi(googletts.agi,"Welcome.",en)
exten => 1410,n,Hangup()
exten => VOIP_NUMBER,n,Hangup()
exten => VOIP_NUMBER,n,agi(googletts.agi,"Wrong password.",pl)
exten => VOIP_NUMBER,n,Hangup()

This is simple solution, I strongly recommend making a whitelist of numbers which are allowed to make a call to the PBX PSTN for security. Access control will also be useful when you store issues from you customers in a database. This example will show you how to take this data out and present them to you by text2voice script.
MySQL example:

exten => VOIP_NUMBER,1,Hangup()
exten => VOIP_NUMBER,1,MYSQL(Connect connid localhost LOGIN DB_PASS DB_NAME)
exten => VOIP_NUMBER,n,GotoIf($["${connid}" = ""]?error,1)
exten => VOIP_NUMBER,n,MYSQL(Query resultid ${connid} SELECT transcription, sourcenumber FROM zgloszenia)
exten => VOIP_NUMBER,n(fetchrow),MYSQL(Fetch foundRow ${resultid} transkrypcja sourcenumber)
exten => VOIP_NUMBER,n,Set(numbercut=${SHELL(echo ${sourcenumber} | sed -e 's/\(.\)/\1 /g')});
exten => VOIP_NUMBER,n,GotoIf($["${foundRow}" = "1"]?done)
exten => VOIP_NUMBER,n,Goto(fetchrow)
exten => VOIP_NUMBER,n(done),MYSQL(Clear ${resultid})
exten => VOIP_NUMBER,n,NoOp(${transkrypcja},${sourcenumber})
exten => VOIP_NUMBER,n,Wait(1)
exten => VOIP_NUMBER,n,agi(googletts.agi,"Last recorded ticket: ",en)
exten => VOIP_NUMBER,n,Wait(1)
exten => VOIP_NUMBER,n,agi(googletts.agi,"${transkrypcja}",en)
exten => VOIP_NUMBER,n,Wait(1)
exten => VOIP_NUMBER,n,agi(googletts.agi,"Caller number is",en)
exten => VOIP_NUMBER,n,agi(googletts.agi,"'${numbercut}'"\",pl)
exten => VOIP_NUMBER,n,MYSQL(Disconnect ${connid})
exten => VOIP_NUMBER,n,Hangup()

These examples I showed you allows you to easily create advanced PBX system with interesting functionalities. The image of Asterisk on metaROUTER can be easily migrated to different Mikrotik or duplicated for other customers.

To sum up – such solution works for me flawlessly for almost 2 years. No issues at all. metaROUTER allows you to simplify backup process and provides easy migration and recovery after disaster.

Leave a Reply

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