mirror of
https://github.com/jambonz/sbc-outbound.git
synced 2026-10-04 10:14:29 +00:00
Compare commits
17
Commits
v0.7.9
...
v0.8.3-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95bd061e2d | ||
|
|
2f53c0bb4f | ||
|
|
f03308b11d | ||
|
|
7c91ac9d68 | ||
|
|
fcffa1041c | ||
|
|
a6d5b25e40 | ||
|
|
5cba07ee44 | ||
|
|
579e21b5bf | ||
|
|
8864ab1430 | ||
|
|
2187b03ca6 | ||
|
|
0808eeeda4 | ||
|
|
f841e88bef | ||
|
|
b7873654b8 | ||
|
|
b6f9f214c3 | ||
|
|
dbb93732bf | ||
|
|
a553105e55 | ||
|
|
36b2c752da |
@@ -6,10 +6,10 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: lts/*
|
||||
- run: npm ci
|
||||
- run: npm run jslint
|
||||
- run: npm test
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile --tag $IMAGE_NAME
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM --platform=linux/amd64 node:18.12.1-alpine3.16 as base
|
||||
FROM --platform=linux/amd64 node:18.14.1-alpine3.16 as base
|
||||
|
||||
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
||||
|
||||
@@ -20,4 +20,4 @@ ARG NODE_ENV
|
||||
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
|
||||
CMD [ "node", "app.js" ]
|
||||
CMD [ "node", "app.js" ]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# sbc-outbound 
|
||||
|
||||
This application provides a part of the SBC (Session Border Controller) functionality of jambonz. It handles outbound INVITE requests from the cpaas application server that is going to carrier sip trunks or registered sip users/devices, including webrtc applications.
|
||||
This application provides a part of the SBC (Session Border Controller) functionality of jambonz platfrom. It handles outbound INVITE requests from the cpaas application server that is going to carrier sip trunks or registered sip users/devices, including webrtc applications.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -11,22 +11,25 @@ Configuration is provided via environment variables:
|
||||
|DRACHTIO_HOST| ip address of drachtio server (typically '127.0.0.1')|yes|
|
||||
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|
||||
|DRACHTIO_SECRET| shared secret|yes|
|
||||
|ENABLE_METRICS| if 1, metrics will be generated|no|
|
||||
|HTTP_PORT| tcp port listen port |no|
|
||||
|JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no|
|
||||
|JAMBONES_MYSQL_HOST| mysql host|yes|
|
||||
|JAMBONES_MYSQL_USER| mysql username|yes|
|
||||
|JAMBONES_MYSQL_PASSWORD| mysql password|yes|
|
||||
|JAMBONES_MYSQL_DATABASE| mysql data|yes|
|
||||
|JAMBONES_MYSQL_CONNECTION_LIMIT| mysql connection limit |no|
|
||||
|DTMF_LISTEN_PORT| DTMF listening port |no|
|
||||
|JAMBONES_NG_PROTOCOL| rtpengine NG protocol |no|
|
||||
|RTPENGINE_PORT| rtpengine port |no|
|
||||
|JAMBONES_CLUSTER_ID| cluster id |no|
|
||||
|JAMBONES_NETWORK_CIDR| CIDR of private network that feature server is running in (e.g. '172.31.0.0/16')|yes|
|
||||
|JAMBONES_REDIS_HOST| redis host|yes|
|
||||
|JAMBONES_REDIS_PORT|redis port|yes|
|
||||
|JAMBONES_REDIS_PORT|redis port|no|
|
||||
|JAMBONES_RTPENGINES| commans-separated list of ip:ng-port for rtpengines (e.g. '172.31.32.10:22222')|yes|
|
||||
|JAMBONES_SBCS| list of IP addresses (on the internal network) of SBCs, comma-separated|yes|
|
||||
|STATS_HOST| ip address of metrics host (usually '127.0.0.1' since telegraf is installed locally|no|
|
||||
|STATS_PORT| listening port for metrics host|no|
|
||||
|STATS_PROTOCOL| 'tcp' or 'udp'|no|
|
||||
|STATS_TELEGRAF| if 1, metrics will be generated in telegraf format|no|
|
||||
|JAMBONES_TIME_SERIES_HOST| influxdb host |yes|
|
||||
|JAMBONES_RECORD_ALL_CALLS| enable auto record calls |no|
|
||||
|K8S| service running as kubernetes service |no|
|
||||
|K8S_RTPENGINE_SERVICE_NAME| rtpengine service name(required for K8S) |no|
|
||||
|
||||
### running under pm2
|
||||
Typically, this application runs under [pm2](https://pm2.io) using an [ecosystem.config.js](https://pm2.keymetrics.io/docs/usage/application-declaration/) file similar to this:
|
||||
@@ -58,17 +61,13 @@ module.exports = {
|
||||
JAMBONES_MYSQL_CONNECTION_LIMIT: 10,
|
||||
JAMBONES_REDIS_HOST: 'jambonz.zzzzzzz.0001.usw1.cache.amazonaws.com',
|
||||
JAMBONES_REDIS_PORT: 6379,
|
||||
ENABLE_METRICS: 1,
|
||||
STATS_HOST: '127.0.0.1',
|
||||
STATS_PORT: 8125,
|
||||
STATS_PROTOCOL: 'tcp',
|
||||
STATS_TELEGRAF: 1,
|
||||
JAMBONES_TIME_SERIES_HOST: '172.31.32.11',
|
||||
JAMBONES_NETWORK_CIDR: '172.31.0.0/16'
|
||||
}
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
#### Running the test suite
|
||||
To run the included test suite, you will need to have docker installed on your laptop.
|
||||
```
|
||||
|
||||
@@ -7,6 +7,7 @@ assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var'
|
||||
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var');
|
||||
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
|
||||
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_NETWORK_CIDR env var');
|
||||
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
|
||||
|
||||
const Srf = require('drachtio-srf');
|
||||
const srf = new Srf('sbc-outbound');
|
||||
@@ -60,11 +61,13 @@ const {
|
||||
retrieveSet,
|
||||
isMemberOfSet
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
|
||||
const activeCallIds = new Map();
|
||||
const Emitter = require('events');
|
||||
const idleEmitter = new Emitter();
|
||||
|
||||
srf.locals = {...srf.locals,
|
||||
stats,
|
||||
@@ -76,6 +79,7 @@ srf.locals = {...srf.locals,
|
||||
AlertType,
|
||||
queryCdrs,
|
||||
activeCallIds,
|
||||
idleEmitter,
|
||||
dbHelpers: {
|
||||
ping,
|
||||
performLcr,
|
||||
@@ -179,8 +183,9 @@ if (process.env.K8S || process.env.HTTP_PORT) {
|
||||
if ('test' !== process.env.NODE_ENV) {
|
||||
/* update call stats periodically */
|
||||
setInterval(() => {
|
||||
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:outbound']);
|
||||
}, 5000);
|
||||
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:outbound',
|
||||
`instance_id:${process.env.INSTANCE_ID || 0}`]);
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
const lookupRtpServiceEndpoints = (lookup, serviceName) => {
|
||||
@@ -238,4 +243,20 @@ else {
|
||||
|
||||
pingMsTeamsGateways(logger, srf);
|
||||
|
||||
process.on('SIGUSR2', handle.bind(null));
|
||||
process.on('SIGTERM', handle.bind(null));
|
||||
|
||||
function handle(signal) {
|
||||
logger.info(`got signal ${signal}`);
|
||||
if (process.env.K8S) {
|
||||
if (0 === activeCallIds.size) {
|
||||
logger.info('exiting immediately since we have no calls in progress');
|
||||
process.exit(0);
|
||||
}
|
||||
else {
|
||||
idleEmitter.once('idle', () => process.exit(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {srf};
|
||||
|
||||
+9
-2
@@ -77,6 +77,7 @@ class CallSession extends Emitter {
|
||||
this.logger = logger.child({callId: req.get('Call-ID')});
|
||||
this.useWss = req.locals.registration && req.locals.registration.protocol === 'wss';
|
||||
this.stats = this.srf.locals.stats;
|
||||
this.idleEmitter = this.srf.locals.idleEmitter;
|
||||
this.activeCallIds = this.srf.locals.activeCallIds;
|
||||
this.writeCdrs = this.srf.locals.writeCdrs;
|
||||
|
||||
@@ -519,8 +520,13 @@ class CallSession extends Emitter {
|
||||
dlg.other = null;
|
||||
other.other = null;
|
||||
|
||||
this.logger.info(`call ended with normal termination, there are ${this.activeCallIds.size} active`);
|
||||
if (this.srsClient) {
|
||||
this.srsClient.stop();
|
||||
this.srsClient = null;
|
||||
}
|
||||
|
||||
this.logger.info(`call ended with normal termination, there are ${this.activeCallIds.size} active`);
|
||||
if (this.activeCallIds.size === 0) this.idleEmitter.emit('idle');
|
||||
this.srf.endSession(this.req);
|
||||
});
|
||||
});
|
||||
@@ -758,7 +764,7 @@ Duration=${payload.duration} `
|
||||
}
|
||||
}
|
||||
else if (dlg.type === 'uac' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) {
|
||||
const arr = /Signal=\s*([1-9#*])/.exec(req.body);
|
||||
const arr = /Signal=\s*([0-9#*])/.exec(req.body);
|
||||
if (!arr) {
|
||||
this.logger.info({body: req.body}, '_onInfo: invalid INFO dtmf request');
|
||||
throw new Error(`_onInfo: no dtmf in body for ${contentType}`);
|
||||
@@ -848,6 +854,7 @@ Duration=${payload.duration} `
|
||||
this.logger.info('call ended with normal termination');
|
||||
this.rtpEngineResource.destroy();
|
||||
this.activeCallIds.delete(this.req.get('Call-ID'));
|
||||
if (this.activeCallIds.size === 0) this.idleEmitter.emit('idle');
|
||||
this.uas.other.destroy();
|
||||
this.srf.endSession(this.req);
|
||||
});
|
||||
|
||||
+2
-2
@@ -194,8 +194,8 @@ module.exports = (srf, logger, opts) => {
|
||||
logger.info(`received outbound INVITE to ${req.uri} from server at ${req.server.hostport}`);
|
||||
const uri = parseUri(req.uri);
|
||||
const desiredRouting = req.get('X-Jambonz-Routing');
|
||||
|
||||
if (!uri || !uri.user || !uri.host) {
|
||||
const validUri = uri && uri.user && uri.host;
|
||||
if (['user', 'sip'].includes(desiredRouting) && !validUri) {
|
||||
logger.info({uri: req.uri}, 'invalid request-uri on outbound call, rejecting');
|
||||
res.send(400, {
|
||||
headers: {
|
||||
|
||||
Generated
+475
-3368
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sbc-outbound",
|
||||
"version": "v0.7.9",
|
||||
"version": "v0.8.2",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
@@ -27,13 +27,13 @@
|
||||
"jslint": "eslint app.js lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.7.3",
|
||||
"@jambonz/realtimedb-helpers": "^0.5.7",
|
||||
"@jambonz/db-helpers": "^0.7.4",
|
||||
"@jambonz/realtimedb-helpers": "^0.7.0",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "0.2.2",
|
||||
"@jambonz/rtpengine-utils": "^0.4.3",
|
||||
"@jambonz/siprec-client-utils": "^0.2.0",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/siprec-client-utils": "^0.2.4",
|
||||
"@jambonz/stats-collector": "^0.1.8",
|
||||
"@jambonz/time-series": "^0.2.5",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.4",
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<!DOCTYPE scenario SYSTEM "sipp.dtd">
|
||||
|
||||
<scenario name="UAC with media">
|
||||
|
||||
<send retrans="500">
|
||||
<![CDATA[
|
||||
|
||||
INVITE sip:16173333456@sbc-sip:5060 SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:16173333456@sbc-sip:5060>
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 INVITE
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
|
||||
X-Call-Sid: fff49e33-e771-403a-8c99-1780eabbc803
|
||||
X-Jambonz-Routing: phone
|
||||
Subject: uac-pcap-carrier-success
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
v=0
|
||||
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
|
||||
s=-
|
||||
c=IN IP[local_ip_type] [local_ip]
|
||||
t=0 0
|
||||
m=audio [auto_media_port] RTP/AVP 8 101
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtpmap:101 telephone-event/8000
|
||||
a=fmtp:101 0-11,16
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<recv response="100" optional="true">
|
||||
</recv>
|
||||
|
||||
<recv response="180" optional="true">
|
||||
</recv>
|
||||
|
||||
<recv response="200" rtd="true" crlf="true">
|
||||
</recv>
|
||||
|
||||
<send>
|
||||
<![CDATA[
|
||||
|
||||
ACK sip:sip:+16173333456@sbc-sip:5060 SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:sip:+16173333456@sbc-sip:5060>[peer_tag_param]
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 ACK
|
||||
Subject: uac-pcap-carrier-success
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<!-- Play a pre-recorded PCAP file (RTP stream) -->
|
||||
<nop>
|
||||
<action>
|
||||
<exec play_pcap_audio="pcap/g711a.pcap"/>
|
||||
</action>
|
||||
</nop>
|
||||
|
||||
<!-- Pause briefly -->
|
||||
<pause milliseconds="2000"/>
|
||||
|
||||
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
|
||||
<send retrans="500">
|
||||
<![CDATA[
|
||||
|
||||
BYE sip:sip:+16173333456@sbc-sip:5060 SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:sip:+16173333456@sbc-sip:5060>[peer_tag_param]
|
||||
Call-ID: [call_id]
|
||||
CSeq: 2 BYE
|
||||
Max-Forwards: 70
|
||||
Subject: uac-pcap-carrier-success
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<recv response="200" crlf="true">
|
||||
</recv>
|
||||
|
||||
</scenario>
|
||||
+8
-4
@@ -42,7 +42,11 @@ test('sbc-outbound tests', async(t) => {
|
||||
|
||||
/* call to PSTN with no lcr configured */
|
||||
await sippUac('uac-pcap-carrier-success.xml');
|
||||
t.pass('successfully completed outbound call to configured sip trunk');
|
||||
t.pass('successfully completed outbound call to sip trunk');
|
||||
|
||||
/* call to PSTN with request uri we see in kubernetes */
|
||||
await sippUac('uac-pcap-carrier-success-k8s.xml');
|
||||
t.pass('successfully completed outbound call to sip trunk (k8S req uri)');
|
||||
|
||||
// re-rack test data
|
||||
execSync(`mysql -h 127.0.0.1 -u root --protocol=tcp -D jambones_test < ${__dirname}/db/jambones-sql.sql`);
|
||||
@@ -80,11 +84,11 @@ test('sbc-outbound tests', async(t) => {
|
||||
await sippUac('uac-pcap-carrier-fail-limits.xml');
|
||||
t.pass('fails when max calls in progress');
|
||||
|
||||
await waitFor(10);
|
||||
await waitFor(25);
|
||||
|
||||
const res = await queryCdrs({account_sid: 'ed649e33-e771-403a-8c99-1780eabbc803'});
|
||||
//console.log(`cdrs: ${JSON.stringify(res)}`);
|
||||
t.ok(res.total === 5, 'wrote 5 cdrs');
|
||||
console.log(`cdrs: ${JSON.stringify(res)}`);
|
||||
t.ok(res.total === 6, 'wrote 6 cdrs');
|
||||
|
||||
srf.disconnect();
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user