├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── app.js ├── config.js.example ├── lib └── proxy.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | simple-sip-proxy.sublime-workspace 2 | simple-sip-proxy.sublime-project 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 30 | node_modules 31 | 32 | #my config settings are private! 33 | config.js 34 | 35 | .DS_Store 36 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "eqeqeq": true, 6 | "immed": true, 7 | "latedef": "nofunc", 8 | "newcap": true, 9 | "noarg": true, 10 | "regexp": true, 11 | "undef": true, 12 | "smarttabs": true, 13 | "asi": true, 14 | "debug": true 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dave Horton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-sip-proxy 2 | 3 | A [drachtio](https://github.com/davehorton/drachtio)-based load balancing sip proxy 4 | 5 | This is a simple sip proxy that is designed to be used for scenarios where you want to distribute incoming SIP INVITE messages across a bank of application servers; for example, to load balance calls across multiple freeswitch servers or the like. While simple, it does offer a few desirable features: 6 | 7 | * supports configurable OPTIONS pings to detect the health of the application servers, dynamically removing and re-integrating those servers as appropriate based on their responses. 8 | 9 | * Simple [configuration file](config.js.example) can be edited while running to add or remove application servers or adjust parameters, and will immediately take affect 10 | 11 | * Supports configurable white list of originating servers that are allowed to send INVITEs to the proxy 12 | 13 | Currently, the proxy only handles INVITE messages, though it would be trivial to add support for REGISTER or other messages that would be useful in an access environment. 14 | 15 | ## Installing 16 | 17 | You will first need to install [drachtio-server](https://github.com/davehorton/drachtio-server) on the server (or on another server on your network). After that (and presuming [node.js](https://nodejs.org) is installed on your server, do the following: 18 | 19 | ````bash 20 | git clone https://github.com/davehorton/simple-sip-proxy.git 21 | cd simple-sip-proxy 22 | npm install 23 | cp config.js.example config.js 24 | # edit config.js as appropriate for your environment 25 | node app.js 26 | ```` 27 | 28 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var drachtio = require('drachtio') ; 4 | var app = drachtio() ; 5 | var config = app.config = require('./config'); 6 | var fs = require('fs') ; 7 | var rangeCheck = require('range_check'); 8 | var debug = app.debug = require('debug')('simple-sip-proxy') ; 9 | 10 | // Expose app 11 | exports = module.exports = app; 12 | 13 | //connect to drachtio server 14 | app.connect({ 15 | host: config.drachtioServer.address, 16 | port: config.drachtioServer.port, 17 | secret: config.drachtioServer.secret, 18 | }) ; 19 | 20 | app.on('connect', function(err, hostport) { 21 | if( err ) throw err ; 22 | console.log('connected to drachtio server at %s', hostport) ; 23 | }) 24 | .on('error', function(err){ 25 | console.warn(err.message ) ; 26 | }) ; 27 | 28 | app.use(function checkSender(req, res, next) { 29 | if( !rangeCheck.inRange( req.source_address, app.config.authorizedSources) ) { return res.send(403) ; } 30 | next() ; 31 | }) ; 32 | app.use(function onlyInvites( req, res, next ) { 33 | if( -1 === ['invite','cancel','prack','ack'].indexOf( req.method.toLowerCase() ) ) { return res.send(405); } 34 | next() ; 35 | }); 36 | 37 | require('./lib/proxy.js')(app); 38 | 39 | //handle any errors thrown while executing middleware stack 40 | app.use(function myErrorHandler(err, req, res, next) { 41 | debug('caught error generated from middleware: ', err); 42 | res.send(500, { 43 | headers: { 44 | 'X-Error-Info': err.message || 'unknown application error' 45 | } 46 | }) ; 47 | }) ; 48 | 49 | 50 | -------------------------------------------------------------------------------- /config.js.example: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | // copy to config.js and add your info 5 | // ================================= 6 | module.exports = { 7 | 8 | //drachtio server to connect to 9 | drachtioServer: { 10 | address: '127.0.0.1', 11 | port: 9022, 12 | secret: 'cymru' 13 | }, 14 | 15 | //array of one or more sip servers to send to 16 | targets: [ 17 | { 18 | address: 'ip-address-server1', //ip address of server 19 | port: 6060, //sip port (optional: defaults to 5060 if not provided) 20 | optionsPing: true, //send OPTIONS request to test if server is up (optional: defaults to false) 21 | enabled: true //if false, skip this destination (note: you can edit while running and it will take affect) 22 | }, 23 | { 24 | address: 'ip-address-server2', //ip address of server 25 | optionsPing: true, //send OPTIONS request to test if server is up (optional: defaults to false) 26 | enabled: true 27 | } 28 | ], 29 | 30 | optionsPingInterval: 60000, //interval in millisseconds between OPTIONS ping to a server when server is up 31 | 32 | pingIntervalWhenOffline: 5000, //ping interval in millseconds when server is offline 33 | 34 | proxyWhenAllTargetsOffline: false, //if true, when all targets appear down try proxying to one anyways rather than return failure 35 | 36 | authorizedSources: ['68.64.80.0/24'] // array of CIDRs for servers that are allowed to send to us 37 | 38 | } ; -------------------------------------------------------------------------------- /lib/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs') ; 4 | var path = require('path') ; 5 | var _ = require('lodash') ; 6 | var allPossibleTargets = [] ; 7 | var dest = [] ; 8 | var debug ; 9 | var config ; 10 | 11 | function createTargets( config ) { 12 | allPossibleTargets = _.filter( config.targets, function(t) { return t.enabled === true; }) ; 13 | } 14 | function isValidTarget( target ) { 15 | return _.find( allPossibleTargets, function(t) {return t.address === target.address && t.port === target.port; }) ; 16 | } 17 | 18 | function pingTarget( app, target ) { 19 | var dest = 'sip:' + target.address + ':' + (target.port || 5060) ; 20 | if( target.markedForDeath ) { 21 | console.log('removing target %s due to re-loading config file: ', dest) ; 22 | return; 23 | } 24 | 25 | //start a timer - if we don't get a response within a second we'll mark it down 26 | var timeoutHandler = setTimeout( function() { 27 | //if we get here we timed out 28 | if( !target.offline ) { 29 | target.offline = true ; 30 | console.error('taking target %s offline because it did not respond to ping: ', dest) ; 31 | } 32 | }, 1500 ) ; 33 | 34 | //send the OPTIONS ping 35 | console.log('OPTIONS %s', dest) ; 36 | app.request({ 37 | uri: dest, 38 | method: 'OPTIONS', 39 | headers: { 40 | 'User-Agent': 'phone.com internal proxy' 41 | } 42 | }, function( err, req ) { 43 | if( err ) { 44 | setTimeout( function() { 45 | if( isValidTarget( target ) && !target.markedForDeath ) { 46 | pingTarget( app, target); 47 | } 48 | else { 49 | console.log('stop sending OPTIONS ping to %s; it has been removed from the configuration file', dest) ; 50 | } 51 | }, config.pingIntervalWhenOffline || config.optionsPingInterval) ; 52 | return console.error('Error pinging %s', target) ; 53 | } 54 | 55 | req.on('response', function(res){ 56 | 57 | console.log('%s: %d %s', dest, res.status, res.reason) ; 58 | clearTimeout( timeoutHandler ) ; 59 | if( 200 === res.status ) { 60 | if( target.offline ) { 61 | target.offline = false ; 62 | console.log('bringing target back online: %s', dest) ; 63 | } 64 | } 65 | else if( 503 === res.status ) { 66 | if( !target.offline) { 67 | target.offline = true ; 68 | console.log('taking target offline due to 503 response: ', dest) ; 69 | } 70 | } 71 | setTimeout( function() { 72 | if( isValidTarget( target ) && !target.markedForDeath ) { 73 | pingTarget( app, target); 74 | } 75 | else { 76 | console.log('stop sending OPTIONS ping to %s; it has been removed from the configuration file', dest) ; 77 | } 78 | }, target.offline === true ? (config.pingIntervalWhenOffline || config.optionsPingInterval) : config.optionsPingInterval) ; 79 | }) ; 80 | }) ; 81 | } 82 | 83 | function stopPinging() { 84 | console.log('stopping OPTIONS pings..') ; 85 | allPossibleTargets.forEach(function(t) { t.markedForDeath = true; }) ; 86 | } 87 | 88 | function startPinging( app, config ) { 89 | var pingTargets = _.filter( allPossibleTargets, function(t) {return t.optionsPing === true; }) ; 90 | console.log('start OPTIONS pings to targets at %d ms interval: ', config.optionsPingInterval, JSON.stringify(pingTargets)) ; 91 | pingTargets.forEach( function(t) {pingTarget(app,t); }) ; 92 | } 93 | 94 | module.exports = function(app) { 95 | debug = app.debug ; 96 | config = app.config ; 97 | 98 | createTargets(config) ; 99 | app.on('connect', function(err) { 100 | startPinging(app,config); 101 | }); 102 | 103 | //min allowed ping interval is 2 seconds 104 | config.optionsPingInterval = Math.max(config.optionsPingInterval || 5000, 2000) ; 105 | console.log('options ping interval will be %d milliseconds', config.optionsPingInterval) ; 106 | 107 | createTargets(config) ; 108 | startPinging(app, config) ; 109 | 110 | //watch config file for changes - we allow the user to dynamically add or remove targets 111 | var configPath = path.resolve(__dirname) + '/../config.js' ; 112 | fs.watchFile(configPath, function () { 113 | try { 114 | console.log('config.js was just modified...') ; 115 | 116 | delete require.cache[require.resolve(configPath)] ; 117 | config = require(configPath) ; 118 | stopPinging() ; 119 | createTargets( config ) ; 120 | 121 | console.log('proxy targets are now: ', 122 | _.map( allPossibleTargets, function(t) { return _.pick(t, ['address','port','enabled','optionsPing']);})); 123 | 124 | startPinging(app, config) ; 125 | 126 | } catch( err ) { 127 | console.error('Error re-reading config.js after modification; check to ensure there are no syntax errors: ', err) ; 128 | } 129 | }) ; 130 | 131 | app.invite( function(req,res) { 132 | 133 | //filter out offline servers 134 | var onlineServers = _.filter( allPossibleTargets, function(t) { return !t.offline; }) ; 135 | 136 | if( 0 === onlineServers.length ) { 137 | if( config.proxyWhenAllTargetsOffline === true ) { 138 | onlineServers = allPossibleTargets ; 139 | console.log('all servers seem down, but attempting to proxy anyways') ; 140 | } 141 | else { 142 | console.error('there are no servers online currently') ; 143 | return res.send(480) ; 144 | } 145 | } 146 | 147 | var dest = _.map( onlineServers, function(t) { return t.address + ':' + (t.port || 5060); }) ; 148 | 149 | debug('proxy destinations are: ', dest) ; 150 | 151 | //finally....here's the magic: proxy the request, attempting destinations in order until we connect or all fail 152 | req.proxy({ 153 | remainInDialog: false, 154 | destination: dest 155 | }, function(err, results) { 156 | if( err ) console.error('Error proxying: ', err) ; 157 | 158 | //this is voluminous, but if you want to know what happened all the detail is here.. 159 | debug('proxy result: ', JSON.stringify(results)) ; 160 | }) ; 161 | 162 | //round-robin the next starting server in the list 163 | var item = allPossibleTargets.shift() ; 164 | allPossibleTargets.push( item ) ; 165 | }) ; 166 | } ; 167 | 168 | 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-sip-proxy", 3 | "version": "1.0.0", 4 | "description": "Simple load balancing sip proxy", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/davehorton/simple-sip-proxy.git" 12 | }, 13 | "keywords": [ 14 | "sip", 15 | "drachtio", 16 | "proxy" 17 | ], 18 | "author": "Dave Horton", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/davehorton/simple-sip-proxy/issues" 22 | }, 23 | "homepage": "https://github.com/davehorton/simple-sip-proxy#readme", 24 | "dependencies": { 25 | "debug": "*", 26 | "drachtio": "davehorton/drachtio.git", 27 | "lodash": "~2.4.1", 28 | "range_check": "0.0.5" 29 | } 30 | } 31 | --------------------------------------------------------------------------------