Dean

How To Build An ENUM Server

Written by dean on Aug 28, 2008 - 05:32 PM

With ENUM ever growing in use across multiple networks and having just gone through the process of rebuilding our own ENUM server I thought I would document the overall process.

This procedure is based on a base Debian install, but should be appropriate for any *nix based server with package installs altered as appropriate.

All text within "code" tags (so green text) is either something you type on the command line, a script or a MySQL command line query, preceded root> , <?php or MySQL> accordingly. If you're not root, precede any root> command line operations with "sudo" and type in your root password when requested.

We use PowerDNS but, again, pretty much any DNS server will work fine for ENUM. I chose PowerDNS because it neatly operates in both recursive and authority modes and can use MySQL, which we use on VoIP User as our core database, as the backend. Part of the install documentation relating specifically for PowerDNS is taken from their own installation guide.


Setup Parent Domain Nameservers

I recommend buying a dedicated domain for your ENUM server as we'll be messing with nameserver and DNS entries in a way which I would not recommend for any domain running a website or mailserver.

Assuming you bought the domain "mydomain.net" for your ENUM server, you'll need this DNS server setup as the authority for it so that ENUM queries head your way over the internet. Updating the nameservers takes time to populate across the internet - assume 24-72 hours for this to take effect. Don't forget to change all references to mydomain.net in the following to your own domain and TLD.

Set parent domain nameservers to:-

ns.mydomain.net
ns2.mydomain.net

... and point those to the IP address of the server that you're building this on. NOTE : this is invalid according to section 2.2 of the DNS RFC (http://www.faqs.org/rfcs/rfc1035.html) which states that you must have at least 2 nameservers on different IP addresses per domain. I've never seen DNS fail for that reason but be aware that having 2 nameservers on one IP address is technically a breach of the standard.

Package Installation

Start of with a basic Debian install.

First off, update the packaging system.

Code:
root> apt-get update


Install Apache, MySQL and PHP5. Apache and PHP5 are not critical, but these are the basic steps to create a LAMP server and having both available enables you do create a webservices API for inserting, updating and deleting ENUM records if you need to (more on this below).

Code:
root> apt-get install mysql-server mysql-client php5-mysql

root> apt-get install apache2 php5 libapache2-mod-php5


Ensure NTP is installed and configured correctly. This is not critical, but recommended.

Code:
root> apt-get install ntp


The PHP Command Line Interpreter is a useful tool for testing, and recommend if you want to build a webservice API, but is not required for core ENUM operation.

Code:
root> apt-get install php5-cli


Install pDNS

Code:
root> apt-get install pdns-server


We use the MySQL backend which also needs to be installed.

Code:
root> apt-get install pdns-backend-mysql


Edit the config file in your favourite editor (I'm using NANO here):-

Code:
root> nano /etc/powerdns/pdns.conf


Add at the end we need to add our MySQL connection information (we'll be setting the database up shortly):-

launch=gmysql
gmysql-host=127.0.0.1
gmysql-user=pdns
gmysql-dbname=pdns
gmysql-password=[CHOOSE A PASSWORD]

Set up MySQL

Connect to MySQL as Root:-

Code:
root> mysql -u root


Create a user for pdns (change the password below to the one you chose):-

Code:
MySQL> GRANT ALL PRIVILEGES ON *.* TO 'pdns'@'localhost' IDENTIFIED BY '[PASSWORD YOU CHOSE]' WITH GRANT OPTION;


Create a new database for PDNS:-

Code:
MySQL> create database pdns;


Build the Database Tables

Connect to MySQL as a user with sufficient privileges and issue the following commands to build the PDNS tables:

Code:
MySQL> create table domains (
 id       INT auto_increment,
 name       VARCHAR(255) NOT NULL,
 master       VARCHAR(128) DEFAULT NULL,
 last_check    INT DEFAULT NULL,
 type       VARCHAR(6) NOT NULL,
 notified_serial INT DEFAULT NULL,
 account         VARCHAR(40) DEFAULT NULL,
 primary key (id)
)type=InnoDB;

CREATE UNIQUE INDEX name_index ON domains(name);

CREATE TABLE records (
  id              INT auto_increment,
  domain_id       INT DEFAULT NULL,
  name            VARCHAR(255) DEFAULT NULL,
  type            VARCHAR(6) DEFAULT NULL,
  content         VARCHAR(255) DEFAULT NULL,
  ttl             INT DEFAULT NULL,
  prio            INT DEFAULT NULL,
  change_date     INT DEFAULT NULL,
  primary key(id)
)type=InnoDB;

CREATE INDEX rec_name_index ON records(name);
CREATE INDEX nametype_index ON records(name,type);
CREATE INDEX domain_id ON records(domain_id);

create table supermasters (
  ip VARCHAR(25) NOT NULL,
  nameserver VARCHAR(255) NOT NULL,
  account VARCHAR(40) DEFAULT NULL
);

GRANT SELECT ON supermasters TO pdns;
GRANT ALL ON domains TO pdns;
GRANT ALL ON records TO pdns;


Now we need to delegate our DNS sub-zone in the main zone properly by adding a delegation record:-

Code:
MySQL> INSERT INTO records (domain_id, name, content, type,ttl,prio)
VALUES (1,'e164.mydomain.net','ns.mydomain.net','NS',86400,NULL);

INSERT INTO records (domain_id, name, content, type,ttl,prio)
VALUES (1,'e164.mydomain.net','ns2.mydomain.net','NS',86400,NULL);


And a glue record:-

Code:
MySQL> INSERT INTO records (domain_id, name, content, type,ttl,prio)
VALUES (1,'ns.mydomain.net','[IP Address of the ENUM server you're building]','A',120,NULL);


Code:
MySQL> INSERT INTO records (domain_id, name, content, type,ttl,prio)
VALUES (1,'ns2.mydomain.net','[IP Address of the ENUM server you're building]','A',120,NULL);


Tell the server that we're authoritative for the e164.mydomain.net ENUM tree with an SOA record:-

The recommended format (per RFC1912 2.2) for SOA Serial numbers is YYYYMMDDnn, where 'nn' is the revision. For example, if you are making the 3rd change on 27 August 2008, you would use 2008082803. This number should be incremented every time you make a DNS change. In the below example, you should change "2008082803" to the appropriate value in accordance with this recommendation. Don't forget to change the hostmaster email address to one of your own.

Code:
MySQL> INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'mydomain.net','localhost hostmaster<at>mydomain.net 2008082803','SOA',86400,NULL);


Testing

OK, we're done. We should now be able to insert some test records into the database and start querying.

Insert a test record:-

Code:
MySQL> INSERT INTO `records` VALUES (16, 1, '0.0.1.e164.mydomain.net', 'NAPTR', '100 10 "u" "E2U+sip" "!^.*$!sip:hello@mydomain.net!" .', 600, NULL, NULL);


Now we can test the database for the return of this ENUM record:-

Code:
root> dig 0.0.1.e164.mydomain.net ANY

; <<>> DiG 9.4.2-P1 <<>> 0.0.1.e164.mydomain.net ANY
;; global options:  printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9906
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;0.0.1.e164.mydomain.net   IN   ANY

;; ANSWER SECTION:
0.0.1.e164.mydomain.net. 591   IN   NAPTR   100 10 "u" "E2U+sip" "!^.*$!sip:hello@mydomain.net!" .

;; Query time: 107 msec
;; SERVER: 192.168.1.254#53(192.168.1.254)
;; WHEN: Sat Aug 16 15:36:01 2008
;; MSG SIZE  rcvd: 100


Webservices API

You need a mechanism to get records in, updated, and out of your ENUM server. At VoIP User we use a simple PHP webservices API which uses a set of functions defined in an application like that shown below. You can include this PHP file in any other PHP application that you want to give the ability to add delete and update records. Keep this file outside of public HTTP access.

Code:

<?php

///////////////////////////////////////////////////////////////////////////////////////////////
//
// ENUM webservices API functions
//
// Version 0.1 by Dean Elwood 1st August 2008
//
// A set of functions to add, delete and update records in a central ENUM database.
//
// Released under GNU Public licence http://www.gnu.org/licenses/gpl.txt
//
///////////////////////////////////////////////////////////////////////////////////////////////

// EDIT THIS
$database_password = "[SET THIS TO YOUR DB PASSWORD]";
$logfile_path = "[SET THIS TO YOUR LOGFILE PATH]";

// AND DON'T FORGET TO CHANGE ALL REFERENCES TO mydomain.net TO YOUR OWN DOMAIN

function getEnumDatabaseConnection () {
        $connection = mysql_connect("localhost", "pdns", $database_password) or die ("Cannot make the connection");
        $db = mysql_selectdb("pdns", $connection) or die ("Cannot connect to the database");
        return $db;
}


function logThis($text) {
   if (!$fp = fopen($logfile_path, 'a')) {
      $result = "0";
   } else {
      flock ($fp, LOCK_SH);
      $result = fwrite($fp, date("y-m-d G.i:s", time())." ".$text."\n");
      flock($fp, LOCK_UN);
      fclose($fp);
   }
   
   return $result;
}

function cleanUpNumber($number) {

   // Strip leading chars
   $number = trim($number);
   
   // Strip non-numerics
   $number = preg_replace ("/[^0-9]/", "", $number);
   
   // Strip leading zeros
   while ( $number[0] == "0" ) {
      $number = ltrim($number, "0");
   }
   
   return $number;
}


function enumFormat($number) {
// Takes an e164 formatted number and returns ENUM formatted

   $number = cleanUp($number);

   $e164 = ereg_replace("[^0-9]", "", $number);

   // Reverse the order of our e164 number and put dots in between
   $rev_e164 = "";

   for ($i = strlen($e164); $i >= 0; $i-- ) {
      $rev_e164 .= $e164[$i];
      if ( $i != strlen($e164) ) { $rev_e164 .= "."; }
   }

   // Shove our private tree tail on the end
   $rev_e164 .= "e164.mydomain.net";
   
   return $rev_e164;

}

function getEnumRecord($number) {
    $enum = enumFormat($number);
    $db   = getEnumDatabaseConnection();
    $sql  = "SELECT * FROM `records` WHERE `name` = '$enum' LIMIT 1";
    $result = mysql_query($sql);

    if ($result) {
        $row = mysql_fetch_row($result);
        $response = $row['0'];
    } else {
        $response = False;
    }
       
    return $response;
}

function addEnumRecord($sipUri, $number) {
   // Check it doesn't already exist...
   logThis("Trying to add $sipUri as destination for $number");

   if ( getEnumRecord($number) ) {
      $response = False;
   } else {
   
       $enum = enumFormat($number);
       $content = "100 10 \"u\" \"E2U+sip\" \"!^.*$!sip:".$sipUri."!\" .";

       // Shove it in the DB as a new NAPTR record
       $db = getEnumDatabaseConnection();
       $sql = "INSERT INTO `records` values('', '10', '$enum', 'NAPTR', '$content', '600', NULL, UNIX_TIMESTAMP());";
       $response = mysql_query($sql);

      if (!$response) {
          logThis("Database insert failed trying to add enum record.");
      } else {
         logThis("Added $number to ENUM routing to $sipUri");
      }    

   }
    return $response;
}

function updateEnumRecord($number, $sipUri) {
   $enum = enumFormat($number);

   $content = "100 10 \"u\" \"E2U+sip\" \"!^.*$!sip:".$sipUri."!\"";

   $db = getEnumDatabaseConnection();
      $sql = "UPDATE `records` SET `content` = '$content' WHERE `name` ='$enum' LIMIT 1";   
      $response = mysql_query($sql);

      if (!$response) {
          logThis("Database insert failed trying to update enum record.");
      }     
   
   return $response;
}


function deleteEnumRecord($recordId) {
   $db = getEnumDatabaseConnection();
    $sql = "DELETE FROM `records` WHERE `id` = '$recordId'";
    $response = mysql_query($sql);

    if (!$response) {
       logThis("Database insert failed trying to delete enum record.");
    }
   
    return $response;
}

?>




If you need to call these functions from another server over HTTP, you can use a webservice front-end like the script below. This is not finished, but a basic framework ready for final touches (and debugging) as required. Should be enough to get you started.

Code:

<?php

///////////////////////////////////////////////////////////////////////////////////////////////
//
// ENUM Webservice API
//
// Version 0.1 by Dean Elwood 1st August 2008
//
// Released under GNU Public licence http://www.gnu.org/licenses/gpl.txt
//
//
///////////////////////////////////////////////////////////////////////////////////////////////

// EDIT THIS LINE
include_once('[/path/to/the/functions/file/above]');

$todo     = $_GET['todo'];
$number   = $_GET['number'];
$sipuri   = $_GET['sipuri'];

$success  = false;

switch ($todo) {
   
   case "add" :
      $success = addEnumRecord($sipuri, $number);
      break;
      
   case "delete" :
      $recordId = getEnumRecord($number);

      if ( $recordId > 0 ) {
         $success = deleteEnumRecord($recordId);
      } else {
         $success = False;
      }

      break;

   case "update" :
      $success = updateEnumRecord($sipuri, $number);
      break;

   default :
      echo "Nothing to do?\n";
      break;
   
}

if ( $success ) {
   echo $success;
} else {
      print_r($_GET);
}

?>



Using the webservice API is a matter of calling it with the vars required as GET commands in the URL:-

Code:
http://mydomain.net/webservice.php?todo=add&number=200&sipuri=hello2@mydomain.net


Bear in mind that the above makes no attempt at any security measures. You might want to consider using HTTPS (as we do) or at least firewalling access to the ENUM box such that only trusted IP addresses can access this (as we also do).

OpenSER configuration

To get an OpenSER box to use the enum service, you need to make sure the enum module is loaded in your modparam statements in the global section of your openser.cfg. Then you need to place a snippet of config similar to the one below in your config section that deals with outbound calls from local, authenticated users.

Code:

# enum section
# - This snippet should be placed in your local number routing block
# - ENUM_REGEX_VAL should be replaced with a valid regex to suit your environment
# - e164.voipuser.org should be replaced with your enum server

if ( (method=="INVITE" || method=="CANCEL") && uri=~"ENUM_REGEX_VAL") {
    xlog("SCRIPT: trying enum lookup for [$ru]\n");
    if (enum_query("e164.mydomain.net")) {
        xlog("SCRIPT: enum lookup [ => $ru]\n");
        if (!(uri==myself )) {
            append_hf("P-hint: ENUM-OUTBOUND\r\n");
            return(-1);
        }
    }
}


Asterisk Configuration

For ENUM lookups from asterisk the following code will perform the lookup as well as route the call, if no result is found it will route over a defined other route.

Change all references of mydoman.net to your own domain and TLD.

Code:
[enum]
; enum lookup.
;The folllowing may need regionalising if your international numbers dont start 00 when dialing.
exten=> _740XXX.,1,Set(enum_m=${EXTEN:2:2})
exten=> _740XXX.,n,GotoIf($["${enum_m}" = "00"]?3:120)
exten=> _740XXX.,n,Set(enum_d=${EXTEN:4})
exten=> _740XXX.,n,Set(enum_n=${ENUMLOOKUP(+${enum_d},,,e164.mydomain.net)})
exten=> _740XXX.,n,Set(enum_c=${ENUMLOOKUP(+${enum_d},,c,e164.mydomain.net)})

exten=> _740XXX.,n,NoOp(${enum_c})
exten=> _740XXX.,n,GotoIf($["${enum_c}" = "0"]?102)
exten=> _740XXX.,n,GotoIf($["${enum_c}" = ""]?102)
exten=> _740XXX.,n,NoOp(SIP/${enum_n})
exten=> _740XXX.,n,Dial(SIP/${enum_n})
exten=> _740XXX.,n,Hangup()
exten=> _740XXX.,102,Dial,${TRUNKout}/${EXTEN:2}
exten=> _740XXX.,103,Hangup()
exten=> _740XXX.,111,Hangup()
exten=> _740XXX.,120,Set(enum_d=44${EXTEN:3}) ; You need to make sure your national code is here


What can I do with this?

If you run a network, like we do, you can use ENUM to re-write calls to a PSTN number to a SIP URI and route them on net without hitting the PSTN. You can also use it to restrict calls to certain numbers by populating those into your ENUM registry and creating a NAPTR record for them pointing to sip:dev@null.com or an IVR or recorded message.

Asterisk can use ENUM in the same way allowing callers to use PSTN numbers to make on-net SIP calls. For example, if you have a satellite office, with an Asterisk server at each and a SIP trunk between the two, you can rewrite calls from one office to a DDI number at the satellite office so that callers don't have to use a secondary number and the call does not hit the PSTN.

Of course ENUM is pretty useless without a populated ENUM registry. If you don't have a network and there's no point in you running your own, you can always point your openSER or asterisk at one of the public registrars instead.

 


Further resources:-

http://bind8nt.meiway.com/publicDNS.cfm
http://www.ludd.luth.se/~kavli/BIND-FAQ.html
http://www.faqs.org/rfcs/rfc1035.html
http://www.isc.org/index.pl?/sw/bind/
http://www.powerdns.com
Add To Delicious Print this Thread Grab our feed
Voip User Forum Index » The World of VoIP » VoIP General
Reply to topic
Forum Rules and Guidelines | About VoIP User | Privacy Policy


All logos and trademarks in this site are property of their respective owner.
Comments and posts are property of the poster, all the rest (c) 2003-2008 VoIP User Limited.

VoIP User Limited is incorporated in England and Wales under Company Number 6694577.

No part of this site may be reproduced without our prior consent.