Configuring Asterisk for Use with DUNDi

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.

General Configuration

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=support@toronto.example.com
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=support@vancouver.example.com
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.

Initial DUNDi Peer Definition

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.

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.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 the section called “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:

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

Warning

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 the section called “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.

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

Tip

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.

Tip

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

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

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. In the next section we’ll explore how we can add some fluidity to the RegisteredDevices context.

Dynamically adding extension numbers

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

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.[168]

Warning

Prior to Asterisk version 1.8.3, the maximum length of the destination field (see the section called “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:very_secret_secret@toronto.example.com/${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).

Note

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.

Tip

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

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.

Note

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.



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