Compare commits

..
Author SHA1 Message Date
Dave HortonandClaude Fable 5 560c18ef68 feat: report call count to redis for server-wide drain coordination
A server may run several sbc-inbound and sbc-outbound processes.  When
one of the sbc-inbound processes handles an AWS autoscale drain it must
complete the terminating lifecycle hook only when the whole server is
idle, not just its own process — otherwise the instance is terminated
while sibling processes still have calls in progress.

Each process now writes its own call count to a per-pid redis key
(120s expiry, refreshed every 15s from activeCallIds.size) and
registers that key in a per-host set, which the draining process
enumerates and sums.  See the companion change in sbc-inbound.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 15:47:25 -04:00
Dave Horton fec25d5d15 0.9.9 2026-08-20 09:16:55 -04:00
Dave Horton 5c470b9630 0.9.8 2026-07-20 08:43:34 -04:00
Hoan Luu HuuandClaude Opus 4.8 606792825e feat: honor X-Jambonz-SRTP header for forwarded sip URI calls (#230)
The feature-server sets X-Jambonz-SRTP (from the dial verb's
srtpEncryption option) to request encrypted media on a per-call basis.
Previously SRTP on a forwarded sip URI could only be enabled globally
via JAMBONES_SIPS_FORWARD_SRTP + a sips: scheme; now an application can
opt in per call. The env var remains as a global fallback. The internal
header is stripped before the INVITE is sent to the target.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 07:24:14 -04:00
5 changed files with 62 additions and 5 deletions
+13 -1
View File
@@ -70,7 +70,8 @@ const {
isMemberOfSet,
addKey,
deleteKey,
retrieveKey
retrieveKey,
addToSet
} = require('@jambonz/realtimedb-helpers')({}, logger);
const activeCallIds = new Map();
@@ -124,6 +125,17 @@ const {getRtpEngine, setRtpEngines} = require('@jambonz/rtpengine-utils')([], lo
});
srf.locals.getRtpEngine = getRtpEngine;
/* report our call count to redis so a draining sbc-inbound process can count
calls across all sbc-inbound and sbc-outbound processes on this server */
if (!process.env.K8S && 'test' !== process.env.NODE_ENV) {
srf.locals.callCountReporter = require('./lib/call-count-reporter')({
logger,
addKey,
addToSet,
getCount: () => activeCallIds.size
});
}
if (process.env.DRACHTIO_HOST && !process.env.K8S) {
const cidrs = process.env.JAMBONES_NETWORK_CIDR
.split(',')
+31
View File
@@ -0,0 +1,31 @@
const os = require('os');
/**
* Periodically report this process's count of calls in progress to redis.
* A server may host several sbc-inbound and sbc-outbound processes; when one
* of them handles an autoscale drain it needs to know when the entire server
* has no calls in progress, not just its own process. Each process writes
* its own count under a per-pid key (with a short expiry, so keys from dead
* processes evaporate) and registers that key in a per-host set that the
* draining process can enumerate.
*/
const REPORT_INTERVAL = 15000;
const KEY_EXPIRY_SECS = 120;
module.exports = ({logger, addKey, addToSet, getCount}) => {
const prefix = process.env.JAMBONES_CLUSTER_ID || 'default';
const setName = `${prefix}:call-count-keys:${os.hostname()}`;
const key = `${prefix}:call-count:${os.hostname()}:${process.pid}`;
const report = () => {
addKey(key, `${getCount()}`, KEY_EXPIRY_SECS)
.catch((err) => logger.error({err}, 'call-count-reporter: error writing call count'));
};
addToSet(setName, key)
.catch((err) => logger.error({err}, `call-count-reporter: error adding ${key} to ${setName}`));
setInterval(report, REPORT_INTERVAL);
report();
return {key, setName};
};
+15 -1
View File
@@ -237,7 +237,20 @@ class CallSession extends Emitter {
this.logger.info(`sending call to registered user ${destUri}`);
}
else if (this.req.locals.target === 'forward') {
if (process.env.JAMBONES_SIPS_FORWARD_SRTP && this.req.uri.startsWith('sips:')) {
/* the feature-server sets X-Jambonz-SRTP (from the dial verb's srtpEncryption
option) to request encrypted media on a per-call basis */
const srtpMode = this.req.get('X-Jambonz-SRTP');
if (srtpMode) {
/* SDES (RTP/SAVP): pass teams=true to select the SDES srtp profile, matching
the proven carrier tls/srtp path above. This offers a=crypto to the target
(what SIP endpoints such as LiveKit expect), not DTLS-SRTP. */
this.logger.info({uri: this.req.uri, srtpMode},
'using SRTP (SDES) for forwarded call per X-Jambonz-SRTP');
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, false, true);
encryptedMedia = true;
}
else if (process.env.JAMBONES_SIPS_FORWARD_SRTP && this.req.uri.startsWith('sips:')) {
/* legacy global opt-in for sips: forwards (uses the DTLS srtp profile) */
this.logger.info({uri: this.req.uri}, 'using SRTP/TLS for forwarded sips: call');
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, false, false);
encryptedMedia = true;
@@ -531,6 +544,7 @@ class CallSession extends Emitter {
'-X-Preferred-From-Host',
'-X-Jambonz-FS-UUID',
'-X-Voip-Carrier-Sid',
'-X-Jambonz-SRTP',
'-X-SIP-Proxy'
],
proxyResponseHeaders: [
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sbc-outbound",
"version": "0.9.7",
"version": "0.9.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sbc-outbound",
"version": "0.9.7",
"version": "0.9.9",
"license": "MIT",
"dependencies": {
"@babel/helpers": "^7.26.10",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sbc-outbound",
"version": "0.9.7",
"version": "0.9.9",
"main": "app.js",
"engines": {
"node": ">= 20.0.0"