Distributed Universal Number Discovery (DUNDi)

Distributed Universal Number Discovery (DUNDi)

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 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 act just as a location service, but can beused to request and pass information among peers.

How Does DUNDi Work?

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 -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.”

101

Figure -1. DUNDi peer-to-peer request system

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. 1 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 of 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.

The dundi.conf File

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 -1. Options available in the [general] section

10111

alpha

Table -2 lists the options you can configure in the [mappings] section of dundi.conf.

Table -2. Options available in the [mappings] section

0101010101

Finally, Table -3 lists the options available in the peer sections of dundi.conf.

Table -3. Options available for peer definitions in dundi.conf

00000000000000000000

Configuring Asterisk for Use with DUNDi

There are three files that need to be configured for DUNDi: dundi.conf, extensions.conf, and sip.conf. 2 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.

General Configuration

The [general] section of dundi.conf contains parameters relating to the overall oper‐
ation of the DUNDi client and server:
; DUNDi configuration file for Toronto
;
[general]
;
department=IT
organization=toronto.shifteight.org
locality=Toronto
stateprov=ON
country=CA
email=support@toronto.shifteight.org
phone=+14165551212
;
; Specify bind address and port number.
;bindaddr=0.0.0.0
port=4520
entityid=FF:FF:FF:FF:FF:FF
ttl=32
autokill=yes
;secretpath=dundi
Default is port 4520.

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 that we receive replies from can be and is used to break loops. Each time a request is passed 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 maxi‐ mum 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 2,000 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.shifteight.org
locality=Vancouver
stateprov=BC
country=CA
email=support@vancouver.shifteight.org
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

Initial DUNDi Peer Definition

A DUNDi peer is identified by the unique Layer 2 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.shifteight.org
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/11/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/11/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.shifteight.org
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 Vancouverserver 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.

Creating Mapping Contexts

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.shifteight.org/
${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.shifteight.org/
${NUMBER},nopartial

In this example, the mapping context is extensions , which points to the Registered Devices 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 in‐ creased 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.

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, H.323, 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:
${SECRET}
Replaced with the password stored in the local AstDB. Only used with iax.conf.
${NUMBER}
The number being requested.
${IPADDR}
The IP address to connect to.

With our mapping configured, let’s create a simple dialplan context against which we can perform lookups for testing.

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 thia simple dialplan and mappings configured, we need to define the mappings each of our peers is allowed to use.

Using Mapping Contexts with Peers

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 within 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.shifteight.org
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.shifteight.org
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: toronto
Include 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:very_secret_secret@172.16.0.161/1000 gives us an address that we can use to call extension 1000 . (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.

Allowing Remote Connections

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.

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.

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:very_secret_secret@172.16.0.161/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.

Controlling Responses

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 one that can be matched and thus generate a response.

Manually adding responses

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 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.

Using pattern matches

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.

Dynamically adding extension numbers

In some cases, we 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 visiting. 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 defined by regexten for the registering peer will cause that same value to be populated inside the context, as defined by the regcontext attribute.
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
and then reload chan_sip.so.
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.

Using dialplan functions in mappings

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.

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,

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 at various phone locations, so we may want to return the device identifier directly.  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:
; this should all be on a single line
${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).

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.

Performing Lookups from the Dialplan

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 per‐ form 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 as 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 straight‐forward 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.

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.

Let’s 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($[0${NumberOfResults} < 1]?NoResults,1)

; The start of our loop showing the returned values
same => n,While($[0${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 ${NumberOfResults} .

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 end‐ point. 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($[0${NumberOfResults} < 1]?NoResults,1)

; Perform our loop
same => n,While($[0${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.