Tuesday, August 19, 2014

Synchronization Constructs with Node + Redis

Summary

In this article I'll be describing how to create the following basic synchronization objects using a combination of Node and Redis:  semaphores, mutex locks, condition variables, and by a combination of locks and conditions - monitors.  I'll also include implementations of these objects in two classic computer science problems in concurrency.

Implementation of the Synchronization Objects

Given Node is a single-threaded architecture, the question arises - Why would I ever need to worry about concurrent programming problems like critical sections, mutual exclusion, synchronization, etc? Well, even if your code only involves one process, the asynchronous nature of Node can lead to situations where a shared object is being accessed in a non-coherent manner, i.e. race conditions.

There are no shared variables/memory in Node applications.  That includes even those apps with multiple processes utilizing the 'cluster' feature of Node.  That leads to use of objects external to Node to implement mutual exclusion and synchronization.  For this exercise, I used Redis.

Figure 1 depicts the overall approach I used.


Figure 1
Figure 2 depicts an overview of the Javascript object structure.

Figure 2
Synchronization Object Code Snippets

SyncConstruct - constructor
The constructor for this object creates Redis clients for command execution and subscription.  A callback map is also allocated to save callbacks for a waiting process.  A waiting process is 'awakened' when its Redis subscriber receives a message indicating it can proceed.  Its callback is fetched from the map and executed.

function syncConstruct(key, func)
{
    logger.debug('Entering - File: syncConstruct.js, Method: syncConstruct, key:%s', key);
    if (key && func)
    {
        var self = this;
        self.key = key;
        self.cbMap = {};  //object map containing the callback functions of processes waiting to execute
        self.client = redis.createClient(properties.redisServer.port, properties.redisServer.host);  //redis command client
        self.subscriber = redis.createClient(properties.redisServer.port, properties.redisServer.host);  //redis subscription client
        var channel = self.key + ':' + os.hostname() + ':' + process.pid;
        
        self.subscriber.subscribe(channel);
        
        
        /*
         * When a process that was queued due to synchronization delay, it is 'signal'ed to resume via a redis publication.  The
         * 'on message' event triggers execution of the delayed process's callback function
         */
        self.subscriber.on('message', function(channel, msg){
            logger.debug('Entering - File: syncConstruct.js, Method: on message, channel: %s, pid: %d', channel, process.pid);
            var cbFunc = self.cbMap.channel;
            delete self.cbMap.channel;
            cbFunc();
            logger.debug('Exiting - File: syncConstruct.js, Method: on message channel: %s, pid: %d', channel, process.pid);
        });
        
        /*
         * Constructor callback is invoked upon receipt of the 'subscribe' event.  
         */
        self.subscriber.on('subscribe', function(channel, count){
            logger.debug('Entering - File: syncConstruct.js, Method: on subscribe, channel: %s, pid: %d', channel, process.pid);
            func();
            logger.debug('Exiting - File: syncConstruct.js, Method: on subscribe, channel: %s, pid: %d', channel, process.pid);
        });
        
    }
    logger.debug('Exiting - File: syncConstruct.js, Method: syncConstruct');
};


SyncConstruct - enter
This provides the base method for critical section entry used by all the inherited objects (semaphore, etc).  This method executes the particular Redis Lua Script for that inherited object.  The process is either allowed to continue or is delayed by placing its callback on the wait queue (implemented as a Redis List object).

syncConstruct.prototype.enter = function(luaScript, callback1, callback2)
{
    
    var self = this;
    var channel = self.key + ':' + os.hostname() + ':' + process.pid;
    logger.debug('Entering - File: syncConstruct.js, Method: enter, channel: %s', channel);
    self.cbMap.channel = callback1;
   
    self.client.eval(luaScript, 2, self.key, self.key + ':WaitQueue', channel, function (err, res){
        if (err)
        {
            throw err;
        }
            
        if (res > 0)  //will never be true for a condition variable wait method call
        {
            logger.debug('File: syncConstruct.js, Method: enter, passed eval script, res: %s, channel: %s, pid: %d', 
                    res, channel, process.pid);
            var func = self.cbMap.channel;
            
            if (func) 
            {
                delete self.cbMap.channel;
                func();
            }
        }
        
        if (callback2)  //covers the case of releasing a monitor lock after a process has been put in the wait queue
            callback2();
    });
    logger.debug('Exiting - File: syncConstruct.js, Method: enter, channel: %s', channel);
};


SyncConstruct - exit
Base method for critical section exit.  It evaluates a Redis Lua script passed as a parameter.  For all inherited objects, that script ensure 'fairness' by choosing an already waiting process as next in queue.  If there are no processes in the wait queue, the synchronization object (Redis key) is updated to reflect the critical section is available.

syncConstruct.prototype.exit = function(luaScript, callback)
{
    var self = this;
    logger.debug('Entering - File: syncConstruct.js, Method: exit, self.key: %s, queue: %s', self.key, self.key+':WaitQueue');
    self.client.eval(luaScript, 2, self.key, self.key + ':WaitQueue', function (err, res){
        if (err)
        {
            throw err;
        }
      
        if (res > 0)  
        {
            if (callback)
                callback();
        }
        else  //covers the case that a message is published but no process received it (process died for example)
        {
            logger.error('***File: syncConstruct.js, Method: exit, no process received published message to %s', self.key+':WaitQueue');
            self.exit(luaScript, callback);
        }
    });
    logger.debug('Exiting - File: syncConstruct.js, Method: exit');
};


Semaphore - script to implement the "P" function
This Lua script is called by an Redis eval method.  The script checks the value of a Redis key.  If that key is greater than 0, it decrements the key and returns the key's previous value (something > 0).  If the key is 0, the process's id is pushed on to a Redis list (wait queue) and 0 is returned.

The decision to allow the process to proceed is based on the return value.  Greater than 0, the process proceeds.  Less than or equal 0, the process delays.

var pScript =  'local s = redis.call(\'get\', KEYS[1])\
                if (s and s + 0 > 0) then \
                    redis.call(\'decr\', KEYS[1]) \
                    return s \
                else \
                    redis.call(\'lpush\',KEYS[2], ARGV[1]) \
                    return 0 \
                end';



Semaphore - script to implement the "V" function
This Lua script checks the Redis list representing the wait queue by popping off the rightmost (oldest) member.  If a process was on that queue, a message is published to its specific channel (which is identified by a concatenation of the key name, machine name, and process id.  If nothing was on the wait queue, the Redis key is simply incremented.

var vScript =  'local pId = redis.call(\'rpop\', KEYS[2]) \
                if (pId) then \
                    return redis.call(\'publish\', pId, \'V\') \
                else \
                    return redis.call(\'incr\', KEYS[1]) \
                end';


Lock - script to implement the "acquire" function
This script fetches the Redis key associated with the lock and checks if it is greater than 0.  If so, the key is decremented and the previous value returned (value greater than 0 which allows the process to proceed).  If not, the process is pushed onto the FIFO wait queue.

var acquireScript =  'local s = redis.call(\'get\', KEYS[1]) \
                      if (s and s + 0 > 0) then \
                         redis.call(\'decr\', KEYS[1]) \
                         return s \
                      else \
                         redis.call(\'lpush\',KEYS[2], ARGV[1]) \
                         return 0 \
                      end';

Lock - script to implement the "release" function
This script checks the wait queue for a waiting process.  If one exists, that process is awakened via a published message.  If not, the Redis key associated with the lock is incremented to at most a value of 1.
var releaseScript =  'local pId = redis.call(\'rpop\', KEYS[2]) \
                     if (pId) then \
                        return redis.call(\'publish\', pId, \'release\') \
                     else \
                        local s = redis.call(\'get\', KEYS[1]) + 0 \
                        if (s and s + 0 < 1) then \
                            return redis.call(\'incr\', KEYS[1]) \
                        else \
                            return 1 \
                        end \
                     end';


Condition - script to implement the "wait" function
For condition variables, the entry procedure is very simple:  the process is put immediately onto the wait queue.

var waitScript = 'redis.call(\'lpush\',KEYS[2], ARGV[1]) \
                  return 0';


Condition - script to implement the "signal" function
Script to implement the signal function is equally simple for condition variables.  The wait queue is 'popped'.  If a process was waiting, a message to proceed to published to it.


var signalScript = 'local pId = redis.call(\'rpop\', KEYS[2]) \
                    if (pId) then \
                       return redis.call(\'publish\', pId, \'signal\') \
                    else \
                       return 1 \
                    end';


Condition - script to implement the "signalAll" function
Nearly identical to the above script.  In this case, the entire wait queue is popped and every waiting process is sent a message to proceed. 



var signalAllScript = 'local pId = redis.call(\'rpop\', KEYS[2]) \
                        while (pId) do \
                            redis.call(\'publish\', pId, \'signalAll\')\
                            pId = redis.call(\'rpop\', KEYS[2]) \
                        end \
                        return 1';


Synchronization Implementation #1 - The Dining Philosophers Problem

Dining Philosophers is a classic synchronization problem in computer science.  The gist of the problem is that multiple processes (Philosophers) are competing for insufficient resources (Forks).  If all processes were to acquire and hold 1 resource at the same time , none would be able to proceed (a Philosopher needs 2 forks to eat). Deadlock occurs, or more precisely for this case - all the Philosophers starve to death.

I implemented the solution to this problem using Semaphores.  Each of the 5 forks becomes a semaphore that each of the 5 philosopher processes perform P and V ops for access synchronization.

Figure 3 depicts the organization of the main process flow of the solution.


Figure 3


Below is a code snippet of that flow above.  Async was used to minimize callback nesting.

    async.series([ function (callback1)
                   {    
                        rightFork = new semaphore(rightForkNum, callback1);
                   },
                   function (callback2)
                   {
                       leftFork = new semaphore(leftForkNum, callback2);
                   },
                   function (callback3)
                   {
                       /*
                        * after semaphores have been created, do 10 iterations of getting forks, eating, and thinking
                        */
                       var iteration = 1;
                       d.run(function() {
                           async.whilst(
                                   function() 
                                   { 
                                       return iteration <= 10;
                                   },
                                   function(callback) 
                                   {
                                       logger.info('Iteration %d, Philosopher %d', iteration, cluster.worker.id);
                                       iteration++;
                                       live(rightFork, leftFork, callback);
                                   },
                                   function (err) 
                                   {
                                       if (err)
                                           throw err;
                                       logger.info('Philosopher %d signing off', cluster.worker.id);
                                       rightFork.quit();
                                       leftFork.quit();
                                       logger.debug('Exiting - File: dining.js, Method: initDomain, Philosopher id:%d', cluster.worker.id);
                                       callback3();
                                   }
                           );
                       });
                   }
                  ],
                  function(err)
                  {
                    if (err)
                        throw err;
                    cluster.worker.disconnect();
                  }
    );


Synchronization Implementation #2 - Readers/Writers Problem

Readers/Writers is yet another classic computer science problem.  For this synchronization problem, we have multiple read and write processes competing for the same resource (file, database, etc).  Multiple readers can access the resource concurrently (if there are no writers accessing), but a writer must have exclusive access.

I implemented this solution utilizing monitors.  I used a lock to ensure mutual exclusion to the monitor state, condition variables for process synchronization, and shared variables (Redis keys) for monitor state.

Figure 4 depicts the main process flow for the reader process.
Figure 4
Figure 5 depicts the main process flow for the writer process.
Figure 5

Below is a code snippet of the reader process.  Again, async is used to keep the callback nesting manageable.

    async.series(
                 [function(callback1)
                  {
                      logger.info('Reader %d attempting to gain read access', process.pid); 
                      var writers = 1;
                      async.whilst(  //loop until read access is available
                              function()
                              {
                                  return writers > 0;
                              },
                              function(cb1)
                              {
                                  mutex.acquire(function() {  //acquire monitor lock
                                      numWriters.get(function(res1) {  //fetch numWriters shared variable
                                          writers = res1;
                                          if (res1 == 0)  // if no writers are active, increment the numReaders shared var and release monitor lock
                                          {
                                              numReaders.incr(function(res2) {
                                                  mutex.release(function(){
                                                      cb1();
                                                  });
                                              }); 
                                          }
                                          else  //writers are active, wait on the okToRead cond var and release monitor lock
                                          {
                                              okToRead.wait(mutex, cb1);
                                          }
                                      });
                                  });
                              },
                              function(err)
                              {  
                                  if (err)
                                      throw err;
                                  callback1();  //writers was 0, reader gained access, exiting loop
                              }
                      );
                  },
                  function(callback2) 
                  {
                      logger.info('Reader %d gained read access', process.pid);
                      var readTime = utilities.randomPause(1,3); //1 to 3 seconds of simulated reading
                      setTimeout(function(){ logger.info('Reader %d finished reading', process.pid); callback2(); }, readTime);
                  },
                  function(callback3) 
                  {
                      logger.info('Reader %d releasing read access', process.pid);
                      mutex.acquire(function(){  //acquire monitor lock for the purpose of releasing a reader
                          numReaders.decr(function(res){  //decrement number of readers
                              if (res == 0)  // if num of readers is 0, signal a waiting writer
                              {
                                  okToWrite.signal(function(){
                                      mutex.release(function(){  //release monitor lock
                                         callback3(); //return
                                      });
                                  });
                              }
                              else  //num readers > 0, so just release the monitor lock and return
                              {  
                                  mutex.release(function(){ callback3();});
                              }
                          });
                      });
                  },
                  function(callback4)
                  {
                      logger.info('Reader %d processing data', process.pid);
                      var processingTime = utilities.randomPause(3,8);  //3 to 8 seconds of simulated data processing time
                      setTimeout(function(){ callback4(); }, processingTime);
                  }
                  ], 
                  function(err)
                  {
                     if (err)
                         throw err;
                     logger.debug('Exiting - File: readersWriters.js, Method: read, Process id:%d', process.pid);
                     readCB();
                  }
             );


Finally, code snippet of the writer process.

    async.series(
            [function(callback1)
             {
                 logger.info('Writer %d attempting to gain write access', process.pid); 
                 var writers = 1;
                 var readers = 1;
                 async.whilst(  //loop until write access is available, meaning - no readers or writers active
                         function()
                         {
                             return readers > 0 || writers > 0;
                         },
                         function(cb1)
                         {
                             mutex.acquire(function() {  //acquire monitor lock
                                 numReaders.get(function(res1){ //fetch numReaders shared variable
                                     readers = res1;
                                     if (res1 == 0)  //if no readers active, check number of writers
                                     {
                                         numWriters.get(function(res2){
                                             writers = res2;
                                             if (res2 == 0)  //if no writers active as well, increment numWriters and release monitor lock
                                             {
                                                 numWriters.incr(function(res3){
                                                     mutex.release(function(){
                                                         cb1();
                                                     });
                                                 });
                                             }
                                             else  //active writers are present, release monitor lock and wait
                                                 okToWrite.wait(mutex, cb1);
                                         });
                                     }
                                     else  //active readers are present, release monitor lock and wait
                                         okToWrite.wait(mutex, cb1);
                                 });
                             });
                         },
                         function(err)
                         {  
                             if (err)
                                 throw err;
                             callback1();  //number of readers and writers was 0, writer gained access, exiting loop
                         }
                 );
             },
             function(callback2) 
             {
                 logger.info('Writer %d gained write access', process.pid);
                 var writeTime = utilities.randomPause(1,3); //1 to 3 seconds of simulated wriing
                 setTimeout(function(){ logger.info('Writer %d finished writing', process.pid); callback2(); }, writeTime);
             },
             function(callback3) 
             {
                 logger.info('Writer %d releasing write access', process.pid);
                 mutex.acquire(function(){  //acquire monitor lock for the purpose of releasing a writer
                     numWriters.decr(function(res){  //decrement number of writers
                         okToWrite.signal(function(){  //signal 1 waiting writer
                             okToRead.signalAll(function(){  //signal all waiting readers
                                 mutex.release(function(){
                                     callback3();
                                 });
                             });
                         });
                     });    
                 });
             },
             function(callback4)
             {
                 logger.info('Writer %d processing data', process.pid);
                 var processingTime = utilities.randomPause(3,8);  //3 to 8 seconds of simulated data processing time
                 setTimeout(function(){ callback4(); }, processingTime);
             }], 
             function(err)
             {
                if (err)
                    throw err;
                logger.debug('Exiting - File: readersWriters.js, Method: write, Process id:%d', process.pid);
                writeCB();
             }
        );
Lessons Learned
  1. Developing process synchronization/mutex objects by hand is non-trivial.
  2. Process synchronization in an asynchronous environment (Node) becomes complex quickly.
  3. Attempting to use Redis publisher/subscriber in conjunction with standard Redis key/value objects is prone to race conditions if not carefully thought through.

Full Source Code here.
Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Friday, July 18, 2014

Deriving ACL Wildcard Masks

The topic of ACL masks comes up when you're trying to put together firewall rules.  I'm going to show a semi real-world example at the binary level.  There are plenty of short-cuts/tricks out there to do this but I figure if you understand the low-level method - you can understand the short-cuts.

Scenario

I need to add a firewall rule to allow RTP from a service provider that operates servers in the subnet of 172.28.0.0/14 (non-routable/RFC 1918 range for example purposes only).

Step 1:  Convert the CIDR notation to a subnet mask.

Rewriting 172.28.0.0 into 8-bit quads yields:  10101100.00011100.00000000.0000000

The /14 indicates that the first 14 bits are used as the network prefix (remainder are the host prefix bits).  Network bits highlighted below.

10101100.00011100.00000000.00000000

The subnet mask is obtained by setting each of those 14 bits to 1.  This yields:

11111111.11111100.00000000.00000000, or 255.252.0.0


Step 2:  Convert the subnet mask to an ACL wildcard mask.

Once the subnet mask is obtained, obtaining the ACL wildcard is a simple matter of inverting all 32 bits of the subnet mask (flip 0's to 1's, 1's to 0's).  This yields:

00000000.00000011.11111111.11111111, or 0.3.255.255

Step 3:  Construct the ACL entry.

In this example RTP traffic will be sourced from the following UDP port range:  16384 - 32767.  I use a non-sensical host ip address (1.1.1.1) as the destination, as an example.


router(config)#ip access-list extended rtpACL
router(config-ext-nacl)#10 permit udp 172.28.0.0 0.3.255.255 range 16384 32767 host 1.1.1.1
router(config-ext-nacl)#exit
Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Thursday, May 29, 2014

Distributed Job Scheduling with Redis/Node.js

Summary
I'll be discussing a distributed job scheduling implementation utilizing Node.js as the application engine and Redis for the underlying data structures.

In this implementation, I'll be using an event-driven model (no polling) with a separate Dispatcher process for assigning workers to jobs.  Events are announced via the Redis publisher/subscriber model.  Available job and worker queues are realized via Redis Lists.

Implementation

Figure 1 below depicts my overall approach for this application.
  • Test Runner - Node.js application to kick-off a job generator process.  Jobs are generated at a configurable rate.
  • Job Generator - Node.js  application that inserts jobs into the available job queue and notifies the Dispatcher of the available job.
  • Redis - provides the shared data structures and pub/sub mechanism for notifications.
  • Dispatcher - Node.js application that assigns available workers to jobs.
  • Worker - Node.js application that simulates working on a job.
Figure 1

Figure 2 below depicts the data structures and overall flow of this application.  Redis List structures are used for FIFO queuing.

Figure 2

Code Snippets

main.js - This is the hub of the application.  Node's cluster mechanism is used to create a single dispatcher process and (#CPU's - 1) worker processes.   Each worker is assigned a name that equates to their host machine name and process id on that host.  Various internal counters and data structures are reset on start up (jobId, jobQueue, etc).  Node domains are used for resilience.

var cluster = require('cluster');
var domain = require('domain');
var redis = require('redis');
var os = require('os');
var logger = require('./logger');
var timestamper = require('./timeStamper');
var worker = require('./worker');
var dispatcher = require('./dispatcher');
var utilities = require('./utilities');
var properties = require('./properties');
var numProcesses = os.cpus().length;

if (cluster.isMaster)
{
 
    var client = redis.createClient(properties.redisServer.port, properties.redisServer.host);
    var multi = client.multi();
    multi.del('start');
    multi.del('jobId');
    multi.del('jobQueue');
    multi.del('workerQueue');
    multi.exec(function (err, res){
        if (err)
            throw err;
        cluster.fork({processType : 'dispatcher'});
        numProcesses--;
     
        do
        {
            cluster.fork({processType : 'worker'});
            numProcesses--;
        }
        while (numProcesses > 0);
        multi.quit();
        client.quit();
    });
 
cluster.on('disconnect', function(worker) {  //process died
logger.error('File: main.js, Worker %d died', worker.process.pid);
});
}
else
{
    logger.debug('File: main.js, ProcessType %s launched', process.env.processType);
    initDomain(process.env.processType);
}


function initDomain(processType)
{
    logger.debug('Entering - File: main.js, Method: initDomain, processType:%s, pid:%d', processType, process.pid);
    var d = domain.create();
    var client = redis.createClient(properties.redisServer.port, properties.redisServer.host);
    var subscriber = redis.createClient(properties.redisServer.port, properties.redisServer.host);
 
    d.on('error', function (err) {
        try
        {
            logger.error('Crash: ' + err.message);
            var killtimer = setTimeout(function() {process.exit(1);}, 10000);
            killtimer.unref();
         
            client.quit();
            subscriber.quit();
            cluster.worker.disconnect();
        }
        catch (exc)
        {
            console.log(timestamper() + 'Error encountered during crash recovery: ' + exc.message);
        }
    });
 
    switch (processType)
    {
        case 'dispatcher':
            d.run(function() {
                new dispatcher(client, subscriber).start();
            });
            break;
   
        case 'worker':
            d.run(function() {
                new worker(os.hostname() + ':' + process.pid, client, subscriber).start();
            });
            break;
    }
 
    logger.debug('Exiting - File: main.js, Method: initDomain, processType:%s', processType);

};



worker.js - Below is the start up code for a worker.  A worker uses two Redis client connections: a general-use connection and a second connection dedicated for messaging via pub/sub.

worker.prototype.start = function()
{
    logger.debug('Entering - File: worker.js, Method: start, workerId:%s', this.workerId);
 
    var that = this;
 
    this.client.on('error', function (err){
        logger.error('File: worker.js, Method: start, redis client error: ' + err.message);
        throw err;
    });
    this.subscriber.on('error', function (err){
        logger.error('File: worker.js, Method: start, subscriber client error: ' + err.message);
        throw err;
    });
 
    this.subscriber.subscribe(this.workerId);  //subscribe to a message channel specific to this worker
 
    this.subscriber.on('subscribe', function(channel, count){
        setWorkerAvailable(that.client, that.workerId);
    });
 
    this.subscriber.on('message', function(channel, msg){
        logger.debug('worker %s received job %s', that.workerId, msg);
        doWork(msg, function(){
                setWorkerAvailable(that.client, that.workerId);
                if (msg == properties.numJobs)
                {
                    that.client.get('start', function (err, res){
                        var duration = new Date().getTime() - res;
                        logger.debug('*****duration(ms): ' + duration);
                    });
                }
        });
    });
 
    logger.debug('Exiting - File: worker.js, Method: start, workerId:%s', this.workerId);
};


function setWorkerAvailable(client, workerId)
{
    logger.debug('Entering - File: worker.js, Method: setWorkerAvailable, workerId: %s', workerId);
    
    client.lpush('workerQueue', workerId, function (err, res){
        if (err)
        {
            logger.error('File: worker.js, Method: setWorkerAvailable, client.lpush error: ' + err.message);
            throw err;
        }
        
        utils.notifyDispatcher(client, JSON.stringify({'type' : 'worker', 'id' : workerId}));
    });
    logger.debug('Exiting - File: worker.js, Method: setWorkerAvailable, workerId: %s', workerId);

};

dispatcher.js - The dispatcher process provides the 'brains' of the operation.  It listens for job/worker events and then assigns jobs to available workers, first come - first serve.  A Lua script is used to ensure atomicity of the job/worker assignment step.

var dispatchScript = 'local jobQueueLen = redis.call(\'llen\', KEYS[1]) \
                      local workerQueueLen = redis.call(\'llen\', KEYS[2]) \
                      if (jobQueueLen > 0 and workerQueueLen > 0) then \
                        local job = redis.call(\'rpop\', KEYS[1]) \
                        local worker = redis.call(\'rpop\', KEYS[2]) \
                        return {job, worker} \
                      else \
                        return nil \
                      end';

function messageHandler(client, msg)
{
    logger.debug('Entering - File: dispatcher.js, Method: messageHandler');
    client.eval(dispatchScript, 2, 'jobQueue', 'workerQueue', function (err, res){
        if (err)
        {
            logger.error('File: dispatcher.js, Method: messageHandler, redis eval error: ' + err.message);
            throw err;
        }
        if (res && res.length == 2)
            notifyWorker(client, res[0], res[1]);
    });
    logger.debug('Exiting - File: dispatcher.js, Method: messageHandler');
}; 



generator.js - This process simulates job insertions into the job queue.  It notifies the dispatcher accordingly when a job has become available.

generator.prototype.generate = function()
{
    logger.debug('Entering - File: generator.js, Method: generate');
 
    var that = this;
 
    that.client.incr('jobId', function (err1, jobId){
        if (err1)
        {
            logger.error('File: generator.js, Method: generate, client.incr error: ' + err1.message);
            throw err1;
        }
     
     
        that.client.lpush('jobQueue', jobId, function(err2, res){
            if (err2)
            {
                logger.error('File: generator.js, Method: generate, client.lpush error: ' + err2.message);
                throw err2;
            }
            logger.debug('File: generator.js, Method: generate, jobId %d placed on queue', jobId);
            utils.notifyDispatcher(that.client, JSON.stringify({'type' : 'job', 'id' : jobId}));
        });
    });
 
    logger.debug('Exiting - File: generator.js, Method: generate');
}; 


utilities.js - This code provides a common mechanism for the worker and generator processes to notify the dispatcher of available jobs and/or workers.  A Lua script is used to optimize (and atomize) the dispatcher notification such that a broadcast only goes out if there is at least 1 job and 1 worker currently available.

var notifyScript = 'local jobQueueLen = redis.call(\'llen\', KEYS[1]) \
                    local workerQueueLen = redis.call(\'llen\', KEYS[2]) \
                    if (jobQueueLen > 0 and workerQueueLen > 0) then \
                        return redis.call(\'publish\', KEYS[3], ARGV[1]) \
                    else \
                        return -1 \
                    end';

utilities.notifyDispatcher = function(client, msg)
{
    logger.debug('Entering - File: utilities.js, Method: notifyDispatcher, msg: %s', msg);
    client.eval(notifyScript, 3, 'jobQueue', 'workerQueue', 'queueMessages', msg, function (err, res){
        if (err)
        {
            logger.error('File: utilities.js, Method: notifyDispatcher, client.eval error: ' + err.message);
            throw err;
        }
     
        logger.debug('File: utilities.js, Method: notifyDispatcher, client eval res:' + res);
        if (res == 0)  //resend 1 second later if no Dispatcher received the message
        {
            setTimeout(function(){ utilities.notifyDispatcher(client, msg);}, 1000);
        }
    });
 
    logger.debug('Exiting - File: utilities.js, Method: notifyDispatcher, msg: %s', msg);
};

Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Wednesday, April 16, 2014

Secure Password Storage with MongoDB and Node

Summary

In this Tech Tip I'm going to discuss how to store passwords securely in a mongoDB database utilizing the crypto methods included with Node.js.

Background

I'm going to simulate a database of user info that includes the user's ID and password.  The password will be stored as a hash utilizing the 'key strengthening' algorithm PBKDF2.  I'll use the crypto functions included in the node.js distribution to implement the password hashing function.  A really nice discussion of password hashing is here.

Implementation

PBKDF2 utilizes a salt and multiple iterations of a hash function to create a cryptographically strong hash.  Below I set up some variables for a 512-bit salt, 10000 iterations, and a 512-bit resulting key length.

var saltLengthBytes = 64;
var hashIterations = 10000;
var keyLengthBytes = 64;


Below is the main body of the demo code.  Steps that occur:
  1. Initiate the MongoClient connection with a callback that initiates the remainder of the functions.
  2. Create a user record with an id of '1000' and a password of 'userPa$$word'.
  3. Perform a series of 3 authentication tests.  The first test uses a valid username and password.  The second uses an invalid username.  The third uses a valid username, but invalid password.
  4. Delete the user record.
  5. Close the database.


mongodb.MongoClient.connect('mongodb://localhost:27017/configDb', function(err, db){
  if (!err && db)
  {
  userColl = db.collection('users');
  createUser({_id : 1000, name : 'user1000', password :   'userPa$$word'}, function(result1){
  if (!(result1 instanceof Error))
{
console.log('Authenticating valid user name (1000) and password (userPa$$word)')
authenticateUser(1000, 'userPa$$word', function(result2){
if (!(result2 instanceof Error) && result2 == true)
console.log('Results: User authenticated');
else
console.log('Results: User name or password does not match');

console.log('\n'+ 'Authenticating invalid user name (1111)')
authenticateUser(1111, 'userPa$$word', function(result3){
if (!(result3 instanceof Error) && result3 == true)
console.log('Results: User authenticated');
else
console.log('Results: User name or password does not match');

console.log('\n' + 'Authenticating valid user name (1000) but invalid password (userPassword)')
authenticateUser(1000, 'userPassword', function(result4){
  if (!(result4 instanceof Error) && result4 == true)
console.log('Results: User authenticated');
else
console.log('Results: User name or password does not match');
deleteUser(1000, function(result5){
db.close();
});
  });
  });
  });
  }
  });
  }
}); 


Below is a snippet of the createUser function with the password hashing portions depicted.  Of note, I'm using node's built-in strong random number generator for the salt and its PBKDF2 hash function (node uses SHA-1 for the underlying hash).  I also use Mongo's Binary object for storing the resulting salt and password hash into the database.


function createUser(doc, callback)
{

  try
  {
  crypto.randomBytes(saltLengthBytes, function(err1, buf) {  //create the salt for the hash function
  if (err1)
  {
  console.log(err1.message);
  if (callback)
callback(err1);
  }

   else
  {
  doc.salt = new Binary(buf);  //put salt result in a mongodb Binary object

  //Invoke hash function with salt object
  crypto.pbkdf2(doc.password, doc.salt.read(0, doc.salt.length()), hashIterations, keyLengthBytes, function(err2, key){ 



Below is the authenticate function.  It simply applies the hashing steps again to the user-supplied credentials and then compares them to what is in the database.  If the username and hashed password match, the user is authenticated.  Otherwise, authentication fails.


function authenticateUser(id, password, callback)
{
  try

  {
  readUser(id, function(result) {
  if (result instanceof Error)
  {
  console(result.message);
  if (callback)
  callback(result);
  }
  else
  {
  if (result)
  {
crypto.pbkdf2(password, result.salt.read(0, result.salt.length()), hashIterations, keyLengthBytes, function(err, key){ 


Output

Authenticating valid user name (1000) and password (userPa$$word)
Results: User authenticated

Authenticating invalid user name (1111)
Results: User name or password does not match

Authenticating valid user name (1000) but invalid password (userPassword)

Results: User name or password does not match


Full source code here.
Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Thursday, March 6, 2014

SIP UUI Integration with Genesys Routing

Summary

This is the third in a series of articles I've written on integrating Genesys with SIP UUI.  Part 1 discussed the development of a UUI parsing web service.  Part 2 discussed how to invoke that web service from a Genesys routing strategy developed with IRD.  This last article will bring all the concepts together with an end-to-end example of extracting and depositing data to/from the UUI header with a Genesys routing strategy.

Environmentals

I'm using a Cisco router along with Genesys SIP Server (SIPS) for the SIP traffic.  In this previous article, I discussed the Cisco IOS VoIP trust list configuration necessary to integrate a Cisco router and Genesys SIPS.

In addition to the trust list, I'm going to need to do some hacking on the router to simulate a carrier bringing SIP UUI to Genesys via an IP trunk.  To do this, I'll utilize the Session Border Controller process on the router, aka Cisco Unified Border Element (CUBE).  Specifically, I need to artificially insert the 'User-to-User' header into the SIP INVITE sent to Genesys SIPS.

SIP headers can be modified easily in CUBE using sip-profile commands.  Unfortunately, the headers available for modification don't include the one I need - 'User-to-User'.  I suppose the reason for that is UUI isn't a standard, as of yet.  However, with some simple configuration we can hijack one of the headers that is supported by CUBE to get the UUI header inserted.  Below is the configuration to do just that with the Warning header.  The command below replaces the Warning header with a UUI-formatted data item.

voice class sip-profiles 300
 request INVITE sip-header Warning add "User-to-User:656e676c697368;encoding=hex"


Additionally, I had to add this profile to the dial-peer I'll use as the Genesys route point (5000).  That configuration is below.  The 'session target' is the IP address of Genesys SIPS.


dial-peer voice 30 voip

 destination-pattern 5000

 session protocol sipv2

 session target ipv4:192.168.1.69

 voice-class sip profiles 300

 codec g711ulaw



On the Genesys side, I made modifications to the SIPS application object to cause it to intercept the SIP 'User-to-User' header and insert it into the Genesys TLib message stream.  Figures 1 and 2 below depict the modifications I made to enable this SIP to TLib mapping.

Figure 1

Figure 2

Routing Strategy

Figure 3 is the routing strategy I used for this testing.  It is highly contrived (low to no production reality) and rigged to exercise the full Genesys SIP UUI functionality in a minimum of steps.

  1. Figure 4 is the Assign object used to assign the 'User-to-User' data item to script local variable.  The 'User-to-User' item was set via SIPS extracting that header item from the INVITE (SIP to TLib).
  2. I then call the Decoder web service to convert the hex-encoded data from the header to ASCII.
  3. A script List object is used to extract the first item from the resulting array returned by the web service object and assign it to a local variable.  The original 'User-to-User' item is also detached from the call.
  4. The Attach object then attaches that variable to the call (Figure 5).
  5. The next Web Service object (Figure 6) sets up a TLib to SIP mapping.  I'm hex encoding the string 'gold' and then assigning the result to the local array variable named 'callTypeList' (Figure 7).
  6. In Figure 8, three different function calls are being made.  The first establishes a 'User-to-User' header for the outgoing SIP INVITE.  This is the actual TLib to SIP mapping function.  The second function extracts the first value from the encode web service result array to a local variable.  The third function assigns the value of that local variable to that newly established  'User-to-User' header item.
  7. Finally, I route segment the call to a target that has a skill match with the initial UUI item.  That item was a language skill selection ('english' in this example).  Figure 9

Figure 3
Figure 4

Figure 5
Figure 6
Figure 7
Figure 8
Figure 9

Execution

Below are excerpts of the various logs during the strategy execution.

Cisco Router Log: 

UUI header has been inserted into the INVITE destined for Genesys SIPS.

INVITE sip:5000@192.168.1.69:5060 SIP/2.0
Via: SIP/2.0/UDP 10.10.10.1:5060;branch=z9hG4bK1144B
Remote-Party-ID: <sip:1234567890@10.10.10.1>;party=calling;screen=no;privacy=off
From: <sip:1234567890@10.10.10.1>;tag=A011F674-1EF9
To: <sip:5000@192.168.1.69>
Date: Thu, 06 Mar 2014 21:35:38 GMT
Call-ID: 1849BDBB-A4AE11E3-9987FE55-4FA7E03F@10.10.10.1
Supported: 100rel,timer,resource-priority,replaces,sdp-anat
Min-SE:  1800
Cisco-Guid: 0378126320-2762871267-2575498837-1336401983
User-Agent: Cisco-SIPGateway/IOS-15.2.4.M1
Allow: INVITE, OPTIONS, BYE, CANCEL, ACK, PRACK, UPDATE, REFER, SUBSCRIBE, NOTIFY, INFO, REGISTER
CSeq: 101 INVITE
Max-Forwards: 70
Timestamp: 1394141739
Contact: <sip:1234567890@10.10.10.1:5060>
Expires: 180
Allow-Events: telephone-event
Content-Type: application/sdp
Content-Disposition: session;handling=required
Content-Length: 209
User-to-User:656e676c697368;encoding=hex


SIPS Log:

SIPS receives the INVITE with the UUI header intact.


14:35:39.006: SIPTR: Received [0,UDP] 1131 bytes from 10.10.10.1:58029 <<<<<

INVITE sip:5000@192.168.1.69:5060 SIP/2.0^M

Via: SIP/2.0/UDP 10.10.10.1:5060;branch=z9hG4bK1144B^M

Remote-Party-ID: <sip:1234567890@10.10.10.1>;party=calling;screen=no;privacy=off^M

From: <sip:1234567890@10.10.10.1>;tag=A011F674-1EF9^M

To: <sip:5000@192.168.1.69>^M

Date: Thu, 06 Mar 2014 21:35:38 GMT^M

Call-ID: 1849BDBB-A4AE11E3-9987FE55-4FA7E03F@10.10.10.1^M

Supported: 100rel,timer,resource-priority,replaces,sdp-anat^M
Min-SE:  1800^M
Cisco-Guid: 0378126320-2762871267-2575498837-1336401983^M
User-Agent: Cisco-SIPGateway/IOS-15.2.4.M1^M
Allow: INVITE, OPTIONS, BYE, CANCEL, ACK, PRACK, UPDATE, REFER, SUBSCRIBE, NOTIFY, INFO, REGISTER^M
CSeq: 101 INVITE^M
Max-Forwards: 70^M
Timestamp: 1394141739^M
Contact: <sip:1234567890@10.10.10.1:5060>^M
Expires: 180^M
Allow-Events: telephone-event^M
Content-Type: application/sdp^M
Content-Disposition: session;handling=required^M
Content-Length: 209^M
User-to-User:656e676c697368;encoding=hex^M


URS Log:  

URS receives UUI data in hex format (SIP to TLib mapping).

14:35:39.009_I_I_0005024010bd6075 [01:01] call (2-981e1d0) for Resources created (del 1)
    _T_I_0005024010bd6075 [14:09] add DN TServer_SIPS810 5000 <5000@sips> (CDN 57 0005024010bd6075 97eb5b8) to the call 2-981e1d0 truly:22
received from 65200(TServer_SIPS810)genesys:7070(fd=) message EventRouteRequest
        AttributeCallState      0
        AttributeCallType       2
        AttributePropagatedCallType     2
        AttributeCallID 120
        AttributeConnID 0005024010bd6075
        AttributeCallUUID       '02ACMQ3C6S9GF9CM04000VTAES00003O'
        AttributeUserData       [46] 00 01 00 00..

                'User-to-User'  '656e676c697368;encoding=hex'



URS executes web service call to decode the hex to ASCII.

14:35:39.009_H_I_ [08:0c] SOAP request 2 sent to HTTP Bridge:
        URL:        http://192.168.1.75:8080/sipuui/services/UUITranscoder.UUITranscoderHttpSoap11Endpoint
        Method:     ns:decode
        NameSpace:  ns=http://sipuui
        SOAPaction: urn:decode
        Input:      ns:header:656e676c697368;encoding=hex
        Output:     decodeResponse.return
        HTTPAuthent:1
        SOAPSecrty:
14:35:39.009_I_I_0005024010bd6075 [09:04] <<<<<<<<<<<<suspend interp(WAIT_WEBSERVICE), func:GetWebServiceInfoEx timers:00000
14:35:39.015_H_I_0005024010bd6075 [08:08] OK InfoMessage (-1) is received from server ##HTTPSERVER, refid=2, hint=soap
  key V1 [List] value: (size=20)
    key STRN [String] value: "english"





URS assigns the ASCII value of the UUI item to a local variable and deletes the existing User-to-User item from the call.


14:35:39.015_I_I_0005024010bd6075 [09:05] >>>>>>>>>>>>resume interp(0), func:GetWebServiceInfoEx
    _I_I_0005024010bd6075 [09:04] ASSIGN: __WEBReturn(SCRIPT) <- LIST: V1.STRN:english
    _I_I_0005024010bd6075 [09:04] ASSIGN: languageList(SCRIPT) <- STRING: "1:english"
    _I_I_0005024010bd6075 [09:04] ASSIGN: language(SCRIPT) <- STRING: "english"
request to 65200(TServer_SIPS810) message RequestDeletePair
        AttributeReferenceID    14
        AttributeDataKey        'User-to-User'
        AttributeConnID 0005024010bd6075
        AttributeThisDN '5000'


URS creates/attaches key-value pair to the call.


request to 65200(TServer_SIPS810) message RequestAttachUserData
        AttributeReferenceID    15
        AttributeUserData       [22] 00 01 00 00..
                'LANGUAGE'      'english'
        AttributeConnID 0005024010bd6075
        AttributeThisDN '5000'


URS makes a second web service call to UUI format the string 'gold'.


14:35:39.015_H_I_ [08:0c] SOAP request 3 sent to HTTP Bridge:
        URL:        http://192.168.1.75:8080/sipuui/services/UUITranscoder.UUITranscoderHttpSoap11Endpoint
        Method:     ns:encode
        NameSpace:  ns=http://sipuui
        SOAPaction: urn:encode
        Input:      ns:values:gold
        Output:     encodeResponse.return
        HTTPAuthent:1
        SOAPSecrty:


URS assigns the result of the web service call to a local variable (callType).


14:35:39.018_H_I_0005024010bd6075 [08:08] OK InfoMessage (-1) is received from server ##HTTPSERVER, refid=3, hint=soap
  key V1 [List] value: (size=34)
    key STRN [String] value: "676f6c64;encoding=hex"
14:35:39.018_I_I_0005024010bd6075 [09:05] >>>>>>>>>>>>resume interp(0), func:GetWebServiceInfoEx
    _I_I_0005024010bd6075 [09:04] ASSIGN: __WEBReturn(SCRIPT) <- LIST: V1.STRN:676f6c64;encoding=hex
    _I_I_0005024010bd6075 [09:04] ASSIGN: callTypeList(SCRIPT) <- STRING: "1:676f6c64;encoding=hex"
    _I_I_0005024010bd6075 [09:04] ASSIGN: callType(SCRIPT) <- STRING: "676f6c64;encoding=hex"



URS targets Agent 1001 for the call.  Agent 1001 has a skill of 'english' with level > 1.  New User-to-User item is added to TLib Extensions.


14:35:39.019_T_I_0005024010bd6075 [14:19] send to ts TServer_SIPS810 RequestRouteCall to dn 1001 on  (dnis= )
request to 65200(TServer_SIPS810) message RequestRouteCall
        AttributeReferenceID    19
        AttributeReason [14] 00 01 01 00..
                'RTR'   132
        AttributeRouteType      0 (RouteTypeUnknown)
        AttributeExtensions     [230] 00 0a 00 00..
                'SIP_HEADERS'   'User-to-User'
                'User-to-User'  '676f6c64;encoding=hex'
                'CUSTOMER_ID'   'Resources'
                'AGENT' 'Employee_ID_1001'
                'PLACE' 'Place_1001'
                'DN'    '1001'
                'ACCESS'        '1001'
                'SWITCH'        'sips'
                'NVQ'   1
                'TARGET'        '?:english > 1@statserver.GA'
        AttributeOtherDN        '1001'
        AttributeConnID 0005024010bd6075
        AttributeThisDN '5000'



SIPS Log:  INVITE is sent to Agent 1001's endpoint.  New User-to-User item added to header (TLib to SIP mapping).


14:35:39.096: Sending  [0,UDP] 1306 bytes to 192.168.1.70:8440 >>>>>
INVITE sip:1001@192.168.1.70:8440;rinstance=eb8de7f330f64bc8 SIP/2.0^M
From: sip:1234567890@10.10.10.1;tag=0090D300-6C37-1307-A596-0100007FAA77-19268^M
To: <sip:5000@192.168.1.69:5060>^M
Call-ID: 0090D2D8-6C37-1307-A596-0100007FAA77-19132@192.168.1.69^M
CSeq: 1 INVITE^M
Content-Length: 212^M
Content-Type: application/sdp^M
Via: SIP/2.0/UDP 192.168.1.69:5060;branch=z9hG4bK0090D314-6C37-1307-A596-0100007FAA77-188^M
Contact: <sip:1234567890@192.168.1.69:5060>^M
X-Genesys-CallInfo: routed^M
User-to-User: 676f6c64;encoding=hex^M
Allow: ACK, BYE, CANCEL, INFO, INVITE, MESSAGE, NOTIFY, OPTIONS, PRACK, REFER, UPDATE^M
Remote-Party-ID: <sip:1234567890@10.10.10.1>;party=calling;screen=no;privacy=off^M
Date: Thu, 06 Mar 2014 21:35:38 GMT^M
Cisco-Guid: 0378126320-2762871267-2575498837-1336401983^M
User-Agent: Cisco-SIPGateway/IOS-15.2.4.M1^M


Support Phone Log:  Call arrives with the LANGUAGE attached data.
Figure 10

Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Wednesday, March 5, 2014

Cisco VoIP Trust Lists with Genesys SIP Server


Starting with Cisco IOS release 15.1(2)T, Cisco has changed the behavior of their voice gateways as it relates to SIP sources. The purpose of the change was to add an additional safeguard against toll fraud.

Prior to this release, the default behavior of a Cisco gateway was to allow any VoIP source to initiate call setup on the gateway. Now, you must explicitly configure a trust relationship on the gateway with any VoIP source that isn’t already configured in a dial peer.

Example:

x.x.x.x represents an instance of Genesys SIP server. Below I add that address to the trust list on my gateway.

voice service voip 
ip address trusted list ipv4 x.x.x.x 
allow-connections sip to sip
sip


With this configuration, call setups initiated from endpoints off Genesys SIP server work correctly.

If we remove the Genesys SIPS IP address entry from the trust list and turn up ccsip logging on the gateway, you’ll see the behavior below (403 Forbidden). In this scenario, I have a Counterpath SIP client (1001) registered to Genesys SIPS and attempting to outdial to a TF number.


#debug ccsip messages

.

.

.

Jun  8 07:24:52: //-1/xxxxxxxxxxxx/SIP/Msg/ccsipDisplayMsg:
Received:
INVITE
 sip:8008888756@x:5060 SIP/2.0
From:
 sip:1001@x;tag=0004FCE0-8685-1FBF-B162-0100007FAA77-20169
To: <
sip:8008888756@x:5060>
Call-ID:
 0004FCAE-8685-1FBF-B162-0100007FAA77-20163@x
CSeq: 1 INVITE
Content-Length: 138
Content-Type: application/sdp
Via: SIP/2.0/UDPx:5060;branch=z9hG4bK0004FCF4-8685-1FBF-B162-0100007FAA77-13
Contact: <
sip:1001@x:5060>
Allow: ACK, BYE, CANCEL, INFO, INVITE, MESSAGE, NOTIFY, OPTIONS, PRACK, REFER, UPDATE
User-Agent: X-Lite 4 release 4.1 stamp 63214
Max-Forwards: 69
X-Genesys-CallUUID: 009LS0K6GKFRVCB204000VTAES000004
X-ISCC-CofId: location=sips;cofid=8
Session-Expires: 1800;refresher=uac
Min-SE: 90
Supported: uui,100rel,timer

v=0
o=- 1337954333 1 IN IP4 192.168.1.70
s=CounterPath X-Lite 4.1
c=IN IP4 192.168.1.70
t=0 0
m=audio 61984 RTP/AVP 0 8
a=sendrecv

Jun  8 07:24:52: //41918/29DBBA08AB62/SIP/Msg/ccsipDisplayMsg:
Sent:
SIP/2.0 100 Trying
Via: SIP/2.0/UDP x:5060;branch=z9hG4bK0004FCF4-8685-1FBF-B162-0100007FAA77-13
From:
 sip:1001@x;tag=0004FCE0-8685-1FBF-B162-0100007FAA77-20169
To: <
sip:8008888756@x:5060>
Date: Fri, 08 Jun 2012 13:24:52 GMT
Call-ID:
 0004FCAE-8685-1FBF-B162-0100007FAA77-20163@192.168.1.69
CSeq: 1 INVITE
Allow-Events: telephone-event
Server: Cisco-SIPGateway/IOS-12.x
Content-Length: 0


Jun  8 07:24:52: //41918/29DBBA08AB62/SIP/Msg/ccsipDisplayMsg:
Sent:
SIP/2.0 403 Forbidden
Via: SIP/2.0/UDP 192.168.1.69:5060;branch=z9hG4bK0004FCF4-8685-1FBF-B162-0100007FAA77-13
From:
 sip:1001@x;tag=0004FCE0-8685-1FBF-B162-0100007FAA77-20169
To: <
sip:8008888756@x>;tag=23E3A1E4-2684
Date: Fri, 08 Jun 2012 13:24:52 GMT
Call-ID:
 0004FCAE-8685-1FBF-B162-0100007FAA77-20163@x

CSeq: 1 INVITE
Allow-Events: telephone-event
Server: Cisco-SIPGateway/IOS-12.x
Reason: Q.850;cause=21
Content-Length: 0


Turning up the lower level ccapi debugs reveals the cause of the 403:


#debug voip ccapi inout

Jun  8 07:30:05: //41926/E4D957C1AB72/CCAPI/cc_process_call_setup_ind:
>>>>CCAPI handed cid 41926 with tag 150 to app "_ManagedAppProcess_TOLLFRAUD_APP"
Jun  8 07:30:05: //41926/E4D957C1AB72/CCAPI/ccCallDisconnect:  Cause Value=21, Tag=0x0, Call Entry(Previous Disconnect Cause=0, Disconnect Cause=0)Jun  8 07:30:05: //41926/E4D957C1AB72/CCAPI/ccCallDisconnect:  Cause Value=21, Tag=0x0, Call Entry(Previous Disconnect Cause=0, Disconnect Cause=0)



Net, the trust list/call blocking functionality is default behavior now on IOS releases. If you upgrade a gateway to 15.1(2)T or higher and don’t account for it with additional trust list configuration, calls will be blocked.