Distributed Universal Number Discovery, or DUNDi, is a service discovery protocol that can be used for locating resources at remote locations. The original intention of DUNDi was to permit decentralized routing among many peers using a General Peering Agreement (GPA). The GPA (available at http://dundi.com/PEERING.pdf) is intended to take on the role of a centralized control authority with a document to create a trust relationship among the peers in the cloud. While the idea is interesting and sound, the GPA has not taken off. That doesn’t mean the DUNDi protocol itself hasn’t found a home though: the original intention of DUNDi has been expanded so that now it doesn’t just act as a location service, but can be used to request and pass information among peers.
Think of DUNDi as a large phone book that allows you to ask peers if they know of an alternative VoIP route to an extension number or PSTN telephone number.
For example, assume that you are connected to another set of Asterisk boxes listening for and responding to DUNDi requests, and those boxes are in turn connected to other Asterisk boxes listening for and responding to DUNDi requests. Assume also that your system does not have direct access to request anything from the remote servers.
Figure 23-1
illustrates how DUNDi works. You ask your friend Bob if he knows how to
reach 4001
, an extension to which you
have no direct access. Bob replies, “I don’t know how to reach that
extension, but let me ask my peer, Sally.”
Bob asks Sally if she knows how to reach the
requested extension, and she responds with, “You can reach that
extension at IAX2/dundi:
very_long_password@hostname/extension
.”
Bob then stores the address in his database and passes on to you the
information about how to reach 4001
.
With the newfound information, you can then make a separate request to
actually place the call to Sally’s box in order to reach extension
4001
. (DUNDi only helps you find the
information you need in order to connect; it does not actually place the
call.)
Because Bob has stored the information he found, he’ll be able to provide it to any peers who later request the same number from him, so the lookup won’t have to go any further. This helps reduce the load on the network and decreases response times for numbers that are looked up often. (However, it should be noted that DUNDi creates a rotating key, and thus stored information is valid for a limited period of time.)
DUNDi performs lookups dynamically, either
with a switch =>
statement in your
extensions.conf file or with the
use of the DUNDILOOKUP()
dialplan
function.
While DUNDi was originally designed and intended to be used as a peering fabric for the PSTN, it is used most frequently in private networks. If you’re the Asterisk administrator of a large enterprise installation (or even an installation with only a pair of Asterisk boxes at different physical locations), you may wish to simplify the administration of extension numbers. DUNDi is a fantastic tool for this, because it allows you to simply share the extensions that have been configured at each location dynamically, by requesting the extension numbers from the remote location when your local box doesn’t know how to reach them.
Additionally, if one of the locations had a cheaper route to a PSTN number you wanted to dial, you could request that route in your DUNDi cloud. For example, if one box was located in Vancouver and the other in Toronto, the Vancouver office could send calls destined for the Toronto area across the network using VoIP and out the PRI in Toronto, so they can be placed locally on the PSTN. Likewise, the Toronto office could place calls destined for Vancouver out of the PRI at the Vancouver office.
It is often useful to be aware of the options available to us prior to delving into the configuration file, but feel free to skip this section for now and come back to reference particular options after you’ve got your initial configuration up and working.
There are three sections in the dundi.conf file: the [general]
section, the [mappings]
section, and the peer definitions,
such as [FF:FF:FF:FF:FF:FF]
. We’ll
show the options available for each section in separate tables.
Table 23-1 lists the
options available in the [general]
section of dundi.conf.
Table 23-1. Options available in the [general] section
Option | Description |
---|---|
department | Used when querying a remote system’s contact
information. An example might be: Communications . |
organization | Used when querying a remote system’s contact
information. An example might be: ShiftEight.org . |
locality | Used when querying a remote system’s contact
information. An example might be: Toronto . |
stateprov | Used when querying a remote system’s contact
information. An example might be: Ontario . |
country | Used when querying a remote system’s contact
information. An example might be: Canada . |
email | Used when querying a remote system’s contact
information. An example might be: [email protected] |
phone | Used when querying a remote system’s contact
information. An example might be: +1-416-555-1212 |
bindaddr | Used to control which IP address the system will
bind to. This can only be an IPv4 address. The default is
0.0.0.0 , meaning the system
will listen (and respond) on all available interfaces. |
port | The port to listen for requests on. The default is
4520 . |
tos | The Terms of Service or Quality of Service (ToS/QoS) value to be used for requests. See https://wiki.asterisk.org/wiki/display/AST/IP+Quality+of+Service for more information about the values available and how to use them. |
entityid | The entity ID of the system. Should be an
externally (network) facing MAC address. The format is 00:00:00:00:00:00 . |
cachetime | How long peers should cache our responses for, in
seconds. The default is 3600 . |
ttl | The time-to-live, or, maximum depth to search the network for a response. The maximum wait time for a response is calculated using (2000 + 200 * ttl) ms. |
autokill | Used to control how long we wait for an ACK to our DPDISCOVER . Setting this timeout
prevents the lookups from stalling due to a latent peer. This
can be yes , no , or a numeric value representing
the number of milliseconds to wait. You can use the qualify option to enable this
per-peer. |
secretpath | A rotating key is created and stored within the
AstDB. The value is stored in the key ’secret’ under the family
defined by secretpath . The
default secretpath is
dundi , resulting in the key
being stored in dundi/secret
by default. |
storehistory | Used to indicate whether or not the history of the
last several requests should be stored in memory, along with how
long the requests took. Valid values are yes and no (also available using the CLI
commands dundi store history
and dundi no store history).
This is a debugging tool that is disabled by default due to
possible performance impacts. |
Table 23-2 lists
the options you can configure in the [mappings]
section of dundi.conf.
Table 23-2. Options available in the [mappings] section
Finally, Table 23-3 lists the options available in the peer sections of dundi.conf.
Table 23-3. Options available for peer definitions in dundi.conf
There are three files that need to be configured for DUNDi: dundi.conf, extensions.conf, and sip.conf.[167] The dundi.conf file controls the authentication of peers whom we allow to perform lookups through our system. This file also manages the list of peers to whom we might submit our own lookup requests. Since it is possible to run several different networks on the same box, it is necessary to define a different section for each peer, and then configure the networks in which those peers are allowed to perform lookups. Additionally, we need to define which peers we wish to use to perform lookups.
The [general]
section
of dundi.conf contains parameters
relating to the overall operation of the DUNDi client and
server:
; DUNDi configuration file for Toronto ; [general] ; department=IT organization=toronto.example.com locality=Toronto stateprov=ON country=CA [email protected] phone=+14165551212 ; ; Specify bind address and port number. Default is port 4520. ;bindaddr=0.0.0.0 port=4520 entityid=FF:FF:FF:FF:FF:FF ttl=32 autokill=yes ;secretpath=dundi
The entity identifier defined by entityid
should generally be the Media
Access Control (MAC) address of an interface in the machine. The
entity ID defaults to the first Ethernet address of the server, but
you can override this with entityid
, as long as it is set to the MAC
address of something you own. The MAC address of
the primary external interface is recommended. This is the address
that other peers will use to identify you.
The time-to-live (ttl
) field defines how many hops away the
peers we receive replies from can be and is used to break loops. Each
time a request is passed on down the line because the requested number
is not known, the value in the TTL field is decreased by one, much
like the TTL field of an ICMP packet. The TTL field also defines the
maximum number of seconds we are willing to wait for a reply.
When you request a number lookup, an initial
query (called a DPDISCOVER
) is sent
to your peers requesting that number. If you do not receive an
acknowledgment (ACK
) of your query
(DPDISCOVER
) within 2000 ms (enough
time for a single transmission only) and autokill
is set to yes
, Asterisk will send a CANCEL
to the peers. (Note that an
acknowledgment is not necessarily a reply to the query; it is just an
acknowledgment that the peer has received the request.) The purpose of
autokill
is to keep the lookup from
stalling due to hosts with high latency. In addition to the yes
and no
options, you may also specify the number
of milliseconds to wait.
The pbx_dundi
module creates a rotating key and
stores it in the local Asterisk database (AstDB). The key name
secret
is stored in the dundi
family. The value of the key can be
viewed with the database show
command at the Asterisk console. The database family can be overridden
with the secretpath
option.
We need another peer to interact with, so here’s the configuration for the other node:
; DUNDi configuration file for Vancouver ; [general] ; department=IT organization=vancouver.example.com locality=Vancouver stateprov=BC country=CA [email protected] phone=+16135551212 ; ; Specify bind address and port number. Default port is 4520. ;bindaddr=0.0.0.0 port=4520 entityid=00:00:00:00:00:00 ttl=32 autokill=yes ;secretpath=dundi
In the next section we’ll create our initial DUNDi peers.
A DUNDi peer is identified by the unique layer-two MAC address of an interface on the remote system. The dundi.conf file is where we define what context to search for peers requesting a lookup and which peers we want to use when doing a lookup for a particular network. The following configuration is defined in the dundi.conf file on our Toronto system:
[00:00:00:00:00:00] ; Vancouver Remote Office model = symmetric host = vancouver.example.com inkey = vancouver outkey = toronto qualify = yes dynamic=yes
The remote peer’s identifier (MAC address)
is enclosed in square brackets ([]
). The inkey
and outkey
are the public/private key pairs that
we use for authentication. Key pairs are generated with the astgenkey script, located in the ~/src/asterisk-complete/asterisk/1.8/contrib/scripts/
source directory. We use the -n
flag so that we don’t have to initialize passwords every time we start
Asterisk:
$cd /var/lib/asterisk/keys
$sh ~/src/asterisk-complete/asterisk/1.8/contrib/scripts/astgenkey -n toronto
We’ll place the resulting keys, toronto.pub and toronto.key, in our /var/lib/asterisk/keys/ directory. The toronto.pub file is the public key, which we’ll post to a web server so that it is easily accessible for anyone with whom we wish to peer. When we peer, we can give our peers the HTTP-accessible public key, which they can then place in their /var/lib/asterisk/keys/ directories (using something like wget).
On the Vancouver box, we’ll use the following peer configuration in dundi.conf:
[FF:FF:FF:FF:FF:FF] ; Toronto Remote Office model = symmetric host = toronto.example.com inkey = toronto outkey = vancouver qualify = yes dynamic=yes
Then we’ll execute the same astgenkey script on the Vancouver box to generate the public and private vancouver keys. Finally, we’ll place the toronto.pub key on the Vancouver server in /var/lib/asterisk/keys/ and place the vancouver.pub file on the Toronto server in the same location.
After downloading the keys, we must reload the res_crypto.so and pbx_dundi.so modules in Asterisk:
toronto*CLI> module reload res_crypto.so
-- Reloading module 'res_crypto.so' (Cryptographic Digital Signatures)
-- Loaded PUBLIC key 'vancouver'
-- Loaded PUBLIC key 'toronto'
-- Loaded PRIVATE key 'toronto'
vancouver*CLI> module reload res_crypto.so
-- Reloading module 'res_crypto.so' (Cryptographic Digital Signatures)
-- Loaded PUBLIC key 'toronto'
-- Loaded PUBLIC key 'vancouver'
-- Loaded PRIVATE key 'vancouver'
We can verify the keys so we know they’re ready to be loaded at any time with the keys show CLI command:
*CLI> keys show
Key Name Type Status Sum
------------------ -------- ---------------- --------------------------------
vancouver PRIVATE [Loaded] c02efb448c37f5386a546f03479f7d5e
vancouver PUBLIC [Loaded] 0a5e53420ede5c88de95e5d908274fb1
toronto PUBLIC [Loaded] 5f806860e0c8219f597f876caa6f2aff
3 known RSA keys.
With the keys loaded into memory, we can reload the pbx_dundi.so module on both systems in order to peer them together:
*CLI> module reload pbx_dundi.so
-- Reloading module 'pbx_dundi.so' (Distributed Universal Number
Discovery (DUNDi))
== Parsing '/etc/asterisk/dundi.conf': Found
Finally, we can verify that the systems have peered successfully with dundi show peers:
toronto*CLI> dundi show peers
EID Host Port Model AvgTime Status
00:00:00:00:00:00 172.16.0.104 (S) 4520 Symmetric Unavail OK (3 ms)
1 dundi peers [1 online, 0 offline, 0 unmonitored]
Now, with our peers configured and reachable, we need to create the mapping contexts that will control what information will be returned in a lookup.
The dundi.conf file
defines DUNDi contexts that are mapped to dialplan contexts in your
extensions.conf file. DUNDi
contexts are a way of defining distinct and separate directory service
groups. The contexts in the [mapping]
section point to contexts in the
extensions.conf file, which
control the numbers that you advertise.
When you create a peer, you need to define
which mapping contexts you will allow this peer to search. You do this
with the permit
statement (each
peer may contain multiple permit
statements). Mapping contexts are
related to dialplan contexts in the sense that they are a security
boundary for your peers. We’ll enable our mapping in the next
section.
All DUNDi mapping contexts take the form of:
dundi_context
=>local_context
,weight
,technology
,destination
[,options
]]
The following configuration creates a DUNDi
mapping context that we’ll use to advertise our local extension
numbers to the group. We’ll add this configuration to the dundi.conf file on the Toronto system under
the [mappings]
header. Note that
this should all appear on one line:
[mappings]
; All on a single line
;
extensions => RegisteredDevices,0,SIP,dundi:very_secret_secret
@toronto.example.com/
${NUMBER},nopartial
The configuration on the Vancouver system will look like this:
[mappings]
; All on a single line
;
extensions => RegisteredDevices,0,SIP,dundi:very_secret_secret
@vancouver.example.com/
${NUMBER},nopartial
In this example, the mapping context is
extensions
, which points to the
RegisteredDevices
context within
extensions.conf (providing a
listing of extension numbers to reply with: our phone book). Numbers
that resolve to the PBX should be advertised with a
weight
of zero (directly connected).
Numbers higher than zero indicate an increased number of hops or paths
to reach the final destination. This is useful when multiple replies
for the same lookup are received at the end that initially requested
the number; a path with a lower weight
will
be preferred. We’ll look at how to control responses in Controlling Responses.
If we can reply to a lookup, our response will contain the method by which the other end can connect to the system. This includes the technology to use (such as IAX2, SIP, H323, and so on), the username and password with which to authenticate, which host to send the authentication to, and finally the extension number.
Asterisk provides some shortcuts to allow us to create a “template” with which we can build our responses. The following channel variables can be used to construct the template:
It is generally safest to statically
configure the hostname, rather than make use of the ${IPADDR}
variable. The ${IPADDR}
variable will sometimes reply
with an address in the private IP space, which is unreachable from
the Internet.
With our mapping configured, let’s create a simple dialplan context against which we can perform lookups for testing. We’ll make this more dynamic in Controlling Responses.
In extensions.conf, we can add the following on both systems:
[RegisteredDevices] exten => 1000,1,NoOp()
With our dialplan and mappings configured, we need to load them into memory from the CLI:
*CLI>dialplan reload
*CLI>module reload pbx_dundi.so
-- Reloading module 'pbx_dundi.so' (Distributed Universal Number Discovery (DUNDi)) == Parsing '/etc/asterisk/dundi.conf': == Found
We can verify the mapping was loaded into memory with the dundi show mappings command:
toronto*CLI> dundi show mappings
DUNDi Cntxt Weight Local Cntxt Options Tech Destination
extensions 0 RegisteredDe NONE SIP dundi:${SECRET}@172.16.0.
With our simple dialplan and mappings configured, we need to define the mappings each of our peers is allowed to use. We’ll do this in the next section.
With our mappings defined in the dundi.conf file, we need to give our peers
permission to use them. Control of the various mappings is done via
the permit
, deny
, include
, and noinclude
options within a peer definition.
We use permit
and deny
to control whether the remote peer is
allowed to search a particular mapping on our local system. We use
include
and noinclude
to control which peers we will use
to perform lookups with in a particular mapping.
Since we only have a single mapping
defined (extensions
), we’re going
to permit
and include extensions
within our peer
definitions on both the Toronto and Vancouver systems.
On Toronto, we’ll permit Vancouver to
search the extensions
mapping, and
use Vancouver whenever we’re performing a lookup within the extensions
mapping:
[00:00:00:00:00:00] ; Vancouver Remote Office
model = symmetric
host = vancouver.example.com
inkey = vancouver
outkey = toronto
qualify = yes
dynamic=yes
permit=extensions
include=extensions
Similarly, we’ll permit
and include
the extensions
mapping for the Toronto office on
the Vancouver system:
[FF:FF:FF:FF:FF:FF] ; Toronto Remote Office
model = symmetric
host = toronto.example.com
inkey = toronto
outkey = vancouver
qualify = yes
dynamic=yes
permit=extensions
include=extensions
After modifying the peers, we reload the pbx_dundi.so module to have the changes take effect:
*CLI> module reload pbx_dundi.so
The
include
and permit
configuration can be verified via the
dundi show peer command on the Asterisk CLI:
*CLI>dundi show peer 00:00:00:00:00:00
Peer: 00:00:00:00:00:00 Model: Symmetric Host: 172.16.0.104 Port: 4520 Dynamic: no Reg: No In Key: vancouver Out Key: torontoInclude logic: -- include extensions Query logic: -- permit extensions
Now we can test our lookups. We can do
this easily from the Asterisk CLI using the dundi lookup command. If we perform a lookup from the Vancouver system, we’ll
receive a response from the Toronto system with an address we can use
to place a call. We’ve added the keyword bypass
to the end of the lookup in order to
bypass the cache (in case we wish to perform several tests):
vancouver*CLI>dundi lookup 1000@extensions bypass
1. 0 SIP/dundi:very_secret_secret
@172.16.0.161/1000 (EXISTS) from ff:ff:ff:ff:ff:ff, expires in 3600 s DUNDi lookup completed in 12 ms
The response of SIP/dundi:
gives us an address that we can use to call extension very_secret_secret
@172.16.0.161/10001000
. (Of course, we can’t use this address
at the moment because we haven’t configured any peers on the Toronto
(or Vancouver) system to actually receive the call, but at least we
have the DUNDi lookup portion working now!) In the next section we’ll
explore how to receive calls into our system after we’ve replied to a
DUNDi response.
Within our sip.conf file, we need to enable a peer that we can accept calls from and handle that peer’s calls in the dialplan appropriately. The authentication is done using a password as defined in the mapping within dundi.conf.
If you’re using iax.conf, you can use the ${SECRET}
variable in the mapping in place of the password, which is
dynamically replaced with a rotated key and is refreshed every 3600
seconds (1 hour). The value of the secret key is stored in the
Asterisk database and is accessed using the dbsecret
option within the peer definition
of iax.conf.
Here is the user definition for the dundi user as defined in sip.conf:
[dundi]
type=user
secret=very_secret_secret
context=DUNDi_Incoming
disallow=all
allow=ulaw
allow=alaw
The context
entry, DUNDi_Incoming
, is where authorized callers
are sent in extensions.conf. From
there, we can control the call just as we would in the dialplan of any
other incoming connection.
We could also use the permit
and deny
options for the peer in sip.conf to control which IP addresses
we’ll accept calls from. Controlling the IP addresses will give us
an extra layer of security if we’re only expecting calls from known
endpoints, such as those within our organization.
Be sure to reload chan_sip.so to enable the newly created user in sip.conf:
toronto*CLI> sip reload
To
accept the incoming calls, define the [DUNDi_Incoming]
context in extensions.conf and add the following to
the Toronto system’s dialplan.
[DUNDi_Incoming] exten => 1000,1,Verbose(2,Incoming call from the DUNDi peer) same => n,Answer() same => n,Playback(silence/1) same => n,Playback(tt-weasels) same => n,Hangup()
Reload the dialplan with dialplan reload after saving your changes to extensions.conf.
For our first test, we’ll create an
extension in the LocalSets
context
and try placing a call to extension 1000
using the information provided via
DUNDi:
[LocalSets] exten => 1000,1,Verbose(2,Test extension to place call to remote server) same => n,Dial(SIP/dundi:[email protected]/1000,30) same => n,Hangup()
If we reload the dialplan and try testing
the extension by dialing 1000
, we
should be connected to the tt-weasels prompt on the remote machine.
With our user configured correctly to accept incoming calls, let’s
make our dialplan and responses more dynamic with some additional
tools.
Responses are controlled with the dialplan. Whenever an
incoming request matches the dialplan configured for the mapping
(whether the request is for a specific extension or a pattern match),
a response will be sent. If the request does not match within the
dialplan, no response is sent. In the example we’ve been building, the
extension 1000
is the only
extension that can be matched and thus generate a response.
In the next few sections we’ll look at some of the methods we can use to control what requests are responded to.
The extensions.conf file handles what numbers you advertise and what you do with the calls that connect to them.
The simple method to control responses is
to simply add them manually to the [RegisteredDevices]
context. If we had
several extensions at one of our locations, we could add them all to
that context:
[RegisteredDevices] exten => 1000,1,NoOp() exten => 1001,1,NoOp() exten => 1002,1,NoOp()
The NoOp()
dialplan application is used here because the matching and responding is
done only against the extension number, and no dialplan is executed.
While we could overload this context and cause it to also be the
destination for our calls, it’s not recommended. Other reasons for
using the NoOp()
application
should become clear as we progress.
Of course, adding everything we want to respond with manually would be silly, especially if we wanted to advertise a larger set of numbers, such as all numbers for an area code. As mentioned earlier, in our example we might wish to allow our Toronto and Vancouver offices to call out from one another when placing calls that are free or cheap to make from the other location.
We can respond with all of an area code using pattern matches, just as we do in other parts of the dialplan:
[RegisteredDevices] exten => _416NXXXXXX,1,NoOp() exten => _647NXXXXXX,1,NoOp() exten => _905NXXXXXX,1,NoOp()
We could also advertise a full or partial range of extensions using pattern matches:
[RegisteredDevices] exten => _1[1-3]XX,1,NoOp() ; extensions 1100->1399 exten => _1[7-9]XX,1,NoOp() ; extensions 1700->1999
Pattern matches are a good way of adding
ranges of numbers, but these are still static. In the next section
we’ll explore how we can add some fluidity to the Registered
Devices
context.
In some cases, you might want to only advertise extensions at your location that are currently registered to the system. Perhaps we have a salesperson who flies between the Toronto and Vancouver offices, and plugs her laptop into the network and registers at whichever location she is currently at. In that case, we would want to make sure that calls to that person are routed to the appropriate office in order to avoid sending calls across the country unnecessarily.
The regcontext
and regexten
options in iax.conf and
sip.conf are useful for this.
When a peer registers the value associated with regexten
for that peer, an extension of
that value will be created in the context defined by regcontext
. So, for example, if we define
regcontext
in the [general]
section of sip.conf to contain RegisteredDevices
, and we define the
regexten
for each peer to contain
the extension number of that peer, when the peers register the
RegisteredDevices
context will be
populated automatically for us. We’ll modify our sip.conf to look like this:
[general] regcontext=RegisteredDevices [0000FFFF0001](office-phone) regexten=1001
Now, we’ll register our device to the
system and look at the RegisteredDevices
context:
*CLI> dialplan show RegisteredDevices
[ Context 'RegisteredDevices' created by 'SIP' ]
'1001' => 1. Noop(0000FFFF0001) [SIP]
'1002' => 1. Noop(0000FFFF0002) [SIP]
With our devices registered and the
context used for determining when to respond populated, the only
task left is to include the LocalSets
context within the DUNDi_
Incoming
context in
order to permit routing of calls to the endpoints.
Sometimes it’s useful to utilize a dialplan function
within the mappings to control what a peer responds with. Throughout
this book we’ve been touting the advantages of decoupling the user’s
extension number from the device in order to permit hot-desking.
Because the other end is just going to request an extension number
and won’t necessarily know the location of the device on our system,
we can use the DB()
and DB_EXISTS()
functions within the mapping
to perform a lookup from our AstDB for the device to call.[168]
Prior to Asterisk version 1.8.3, the
maximum length of the destination
field
(see Creating Mapping Contexts) was 80 characters, which
made the use of nested dialplan functions nearly impossible. As of
Asterisk 1.8.3, the maximum length is 512 characters.
First we need to make sure our database is populated with the information we might respond with. While this would normally be done by the dialplan written for the hot-desking implementation, we’ll just add the content directly from the Asterisk console for demonstration purposes:
*CLI> database put phones 1001/device 0000FFFF0001
Updated database successfully
With our database populated, we need to
modify our mapping to utilize some dialplan functions that will take
the value requested, perform a lookup to our database for that
value, and return a value. If no value exists in the database, we’ll
return the value of None
.
Our existing mapping looks like this:
[mappings] ; The mapping exists on a single line extensions => RegisteredDevices,0,SIP, dundi:[email protected]/${NUMBER},nopartial
Our current example simply reflects back
the same extension number that was requested, along with some
authentication information. The number requested is the extension
the peer is looking for. However, because we’re using hot-desking,
the extension number may be located at various phone locations, so
we may want to return the device identifier directly.[169] We can do this by being clever with the use of
dialplan functions in our response. While we may not have the full
power of the dialplan (multiple lines, complex logic, etc.) at our
disposal, we can at least use some of the simpler dialplan
functions, such as DB()
, DB_EXISTS()
, and IF()
.
We’re going to replace ${NUMBER}
with the following bit of
dialplan logic:
${IF($[${DB_EXISTS(phones/${NUMBER}/device)}]?${DB(phones/${NUMBER}/device)}:None)}
If we break this down, we end up with an
IF()
statement that will return
either true or false. If false, we return the value of None
. If true, we return the value located
in the database at phones/${NUMBER}/device
(where ${NUMBER}
contains the value of 1001
for our example) using the DB()
function. To determine which value
the IF()
function will return, we
use the DB_EXISTS()
function.
This function checks whether a value exists at phones/${NUMBER}/device
within the AstDB,
and returns either 1
or 0
(true or false).
The DB_EXISTS()
function not only returns 1
or 0
, but also sets the ${DB_RESULT}
channel variable that
contains the value inside the database if the return value is
1
. However, we can’t use that
value because the IF()
function
is evaluated prior to the condition field being evaluated, which
means ${DB_RESULT}
will be
blank. Thus, we need to use the DB()
function to look up the value prior
to the condition field being evaluated.
After reloading pbx_dundi.so from the console (module reload pbx_dundi.so), we can perform a lookup from another server and check out the result:
vancouver*CLI> dundi lookup 1001@extensions bypass
1. 0 SIP/dundi:very_awesome_password/0000FFFF0001 (EXISTS)
from ff:ff:ff:ff:ff:ff, expires in 3600 s
DUNDi lookup completed in 77 ms
With dialplan functions, you can make the
responses in your dialplans a lot more dynamic. In the next section
we’ll look at how you can perform these lookups from the dialplan
using the DUNDILOOKUP()
, DUNDIQUERY()
, and DUNDIRESULT()
functions.
When you perform lookups using the
example in this chapter, because all the peers in your network
will return a result (None
, or
the value you want), you’ll need to use the DUNDIQUERY()
and DUNDIRESULT()
functions to parse through
the list of results returned. The alternative would be to try
calling SIP/dundi:very_long_pass@remote_server/None
,
but this wouldn’t be very effective. You might even want to handle
the extension None
elegantly,
in case it gets called.
Performing lookups from the dialplan is really the bread
and butter of all of this, because it allows more dynamic routing from
within the dialplan. With DUNDi, you can perform lookups and route
calls within your cluster using either the DUNDILOOKUP()
or DUNDIQUERY()
and DUNDIRESULT()
functions.
The DUNDILOOKUP()
function replaces the old DUNDiLookup()
dialplan application,
performing nearly the same functionality. With DUNDILOOKUP()
, you perform your lookup like
you would at the Asterisk console, and the result can then be saved
into a channel variable, or used wherever you might use a dialplan
function. Here is an example:
[TestContext] exten => 1001,1,Verbose(2,Look up extension 1001) same => n,Set(DUNDi_Result=${DUNDILOOKUP(1001,extensions,b)}) same => n,Verbose(2,The result of the lookup was ${DUNDi_Result}) same => n,Hangup()
The arguments passed to DUNDILOOKUP()
are:
extension
,context
,options
.
Only one option, b
, is available
for the DUNDILOOKUP()
function, and
that is used to bypass the local cache. The advantage to using the
DUNDILOOKUP()
function is that it
is straightforward and easy to use. The disadvantage is that it will
only set the first value returned; if multiple values are returned,
they will be discarded.
You won’t always want to use the bypass option when performing lookups, because the use of the cache is what will lower the number of requests over your network and limit the amount of resources required. We’re using it in our examples simply because it is useful for testing purposes, so that we know we’ve returned a result each time rather than just a cached value from the previous lookup.
To parse through multiple returned values,
we need to use the DUNDIQUERY()
and DUNDIRESULT()
functions. Each plays an important part in sifting through
multiple returned values from a lookup. The DUNDIQUERY()
function performs the initial
lookup and saves the resulting hash into memory. An ID value is then
returned, which can be stored in a channel variable. The ID value
returned from the DUNDIQUERY()
function can then be passed to the DUNDIRESULT()
function to parse through the
returned values from the query.
Lets take a look at some dialplan that uses these functions:
[TestContext] exten => _1XXX,1,Verbose(2,Looking up results for extension ${EXTEN}) ; Perform our lookup and save the resulting ID to DUNDI_ID same => n,Set(DUNDI_ID=${DUNDIQUERY(${EXTEN},extensions,b)}) same => n,Verbose(2,Showing all results returned from the DUNDi Query) ; The DUNDIRESULT() function can return the number of results using 'getnum' same => n,Set(NumberOfResults=${DUNDIRESULT(${DUNDI_ID},getnum)}) same => n,Set(ResultCounter=1) ; If there is less than 1 result, no results were returned same => n,GotoIf($[${NumberOfResults} < 1]?NoResults,1) ; The start of our loop showing the returned values same => n,While($[${ResultCounter} <= ${NumberOfResults}]) ; Save the returned result at position ${ResultCounter} to thisResult same => n,Set(thisResult=${DUNDIRESULT(${DUNDI_ID},${ResultCounter})}) ; Show the current result on the console same => n,Verbose(2,One of the results returned was: ${thisResult}) ; Increase the counter by one same => n,Set(ResultCounter=${INC(ResultCounter)}) ; End of our loop same => n,EndWhile() same => n,Playback(silence/1) same => n,Playback(vm-goodbye) same => n,Hangup() ; If no results were found, execute this dialplan exten => NoResults,1,Verbose(2,No results were found) same => n,Playback(silence/1) same => n,Playback(invalid) same => n,Hangup()
Our example dialplan performs a lookup using
the DUNDIQUERY()
function and
stores the resulting ID value in the DUNDI_ID
channel variable. Using the
DUNDIRESULT()
function and the
getnum
option, we store the total
number of returned results in the NumberOfResults
channel variable. We then
set the ResultCounter
channel
variable to 1
as our starting
position in the loop.
Using GotoIf()
, we check if the ${NumberOfResults}
returned is less than one
and, if so, jump to the NoResults
extension, where we Playback()
“Invalid extension”. If at least one extension is found, we continue
on in the dialplan.
Using the While()
application, we check if the
${ResultCounter}
is less than or
equal to the value of ${NumberOfResults}
. If that is true, we
continue on in the dialplan, and otherwise, we jump to the EndWhile()
application.
For each iteration of our loop, the DUNDIRESULT()
function is used to save the
value at position ${ResultCounter}
to the thisResult
channel variable.
After storing the value, we output it to the Asterisk console using
the Verbose()
application.
Following that, we increase the value of ResultCounter
by one using the INC()
function. Our loop test is then done
again within the While()
loop, and
the loop will continue while the value of ${ResultCounter}
is less than or equal to
the value of ${Number
OfResults}
.
Using the same type of logic, we could check
for values other than None
and, if
such a value is found, ExitWhile()
and continue in the dialplan to perform a call to the endpoint. The
dialplan logic might look something like this:
[subLookupExtension] exten => _1XXX,1,Verbose(2,Looking up results for extension ${EXTEN}) ; Perform our lookup and save the resulting ID to DUNDI_ID same => n,Set(DUNDI_ID=${DUNDIQUERY(${EXTEN},extensions,b)}) same => n,Set(NumberOfResults=${DUNDIRESULT(${DUNDI_ID},getnum)}) same => n,Set(ResultCounter=1) ; If no results are found, return 'None' same => n,GotoIf($[${NumberOfResults} < 1]?NoResults,1) ; Perform our loop same => n,While($[${ResultCounter} <= ${NumberOfResults}]) ; Get the current value same => n,Set(thisResult=${DUNDIRESULT(${DUNDI_ID},${ResultCounter})}) ; If the current value returned is not None, we have a resulting ; location to call and we can exit the loop same => n,ExecIf($["${thisResult}" != "None"]?ExitWhile()) ; If we made it this far, no value has been returned yet that we want to ; use, so increase the counter and try the next value. same => n,Set(ResultCounter=${INC(ResultCounter)}) ; End of our loop same => n,EndWhile() ; We've made it here because we made it to the end of the loop or we found ; a value we want to return. Check to see which it is. If we just ran out of ; values, return 'None'. ; same => n,GotoIf($["${thisResult}" = "None"]?NoResults,1) ; If we make it here, we have a value we want to return. same => n,Return(${thisResult}) ; If there were no acceptable results, return the value 'None' exten => NoResults,1,Verbose(2,No results were found) same => n,Return(None)
With the DUNDIQUERY()
and DUNDIRESULT()
functions, you have a lot of
power to control how to handle the results returned and perform
routing logic with those values.
In this chapter we looked at how the DUNDi protocol helps you to perform lookups against the other Asterisk systems in your cluster, to perform dynamic routing. With the use of DUNDi, you can take multiple systems and control when and where calls are placed within them, providing toll-bypass capabilities and even giving your employees the ability to move between physical locations, while also limiting the number of out-of-system hops a call must take to find them.
While the original intention of DUNDi was to help us migrate away from centralized directory services—an intention that has yet to come to fruition—DUNDi is an extremely effective and useful tool that can be put to work in organizations to advertise and route calls dynamically between systems in a cloud environment. DUNDi is a tool that gives great power to Asterisk administrators looking to create a distributed network.
[167] The dundi.conf and extensions.conf files must be configured. We have chosen to configure sip.conf for the purposes of address advertisement on our network, but DUNDi is protocol-agnostic, so iax.conf, h323.conf, or mgcp.conf could be used instead. DUNDi simply performs the lookups; the standard methods of placing calls are still required.
[168] …or func_odbc
, or
func_curl
, or res_ldap
(using the REALTIME_FIELD()
function).
[169] We’ve also looked at using the GROUP()
and GROUP_COUNT()
functions for looking up
the current channel usage on a remote system to determine which
location to route calls to (the one with the lowest channel
usage) as a simple load balancer.