add server discovery to options (#142)

This commit is contained in:
Sam Machin
2026-07-09 10:30:50 -04:00
committed by GitHub
parent c61eb8523d
commit 1841110ef7
4 changed files with 113 additions and 1 deletions
+19
View File
@@ -28,6 +28,25 @@ Configuration is provided via environment variables:
|ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |yes|
|JAMBONES_REGBOT_DEFAULT_EXPIRES_INTERVAL| default expire value for outbound registration in seconds (default 3600) |no|
|JAMBONES_REGBOT_MIN_EXPIRES_INTERVAL| minimum expire value for outbound registration in seconds (default 30) |no|
|JAMBONES_SERVER_CONTROL| set to a truthy value ('1', 'true', 'yes') to enable server-control features such as topology discovery via OPTIONS (see below) |no|
## Server control
When `JAMBONES_SERVER_CONTROL` is enabled, the SBC exposes additional server-control features.
### Topology discovery
An OPTIONS request carrying the header `X-Jambonz-Discover: true` (from any IP, including external ones) is answered with a `200 OK` whose JSON body lists the current cluster topology read from redis:
```json
{
"featureServers": ["10.0.0.10:5060"],
"sipServers": ["1.2.3.4"],
"rtpServers": ["10.0.0.20"]
}
```
`featureServers`, `sipServers` and `rtpServers` are the IPs (feature servers include the port) of the active feature servers, SIP servers and RTP servers respectively. When `JAMBONES_SERVER_CONTROL` is not set, the discovery header is ignored and OPTIONS behaves as normal.
## CLI Management
+5 -1
View File
@@ -45,6 +45,9 @@ const JAMBONES_REGBOT_USER_AGENT = process.env.JAMBONES_REGBOT_USER_AGENT ;
const JAMBONES_REGBOT_FAILURE_RETRY_INTERVAL = process.env.JAMBONES_REGBOT_FAILURE_RETRY_INTERVAL;
const JAMBONES_REGBOT_REGISTER_FAILURE_THRESHOLD = process.env.JAMBONES_REGBOT_REGISTER_FAILURE_THRESHOLD;
/* Server control - external topology discovery and other server-control features (disabled unless truthy) */
const JAMBONES_SERVER_CONTROL = process.env.JAMBONES_SERVER_CONTROL;
module.exports = {
JAMBONES_MYSQL_HOST,
JAMBONES_MYSQL_USER,
@@ -81,5 +84,6 @@ module.exports = {
REGISTER_RESPONSE_REMOVE,
JAMBONES_REGBOT_USER_AGENT,
JAMBONES_REGBOT_FAILURE_RETRY_INTERVAL,
JAMBONES_REGBOT_REGISTER_FAILURE_THRESHOLD
JAMBONES_REGBOT_REGISTER_FAILURE_THRESHOLD,
JAMBONES_SERVER_CONTROL
};
+22
View File
@@ -1,5 +1,6 @@
const debug = require('debug')('jambonz:sbc-options-handler');
const { isDrained } = require('./cli/feature-server-config');
const serverControl = require('./server-control');
const {
EXPIRES_INTERVAL,
CHECK_EXPIRES_INTERVAL,
@@ -111,6 +112,27 @@ module.exports = ({srf, logger}) => {
return async(req, res) => {
/* server-control: topology discovery request (X-Jambonz-Discover: true).
Deliberately answerable from any (external) IP - it is gated only by the
JAMBONES_SERVER_CONTROL env var and the presence of the discovery header. */
if (serverControl.isEnabled() && serverControl.isDiscoverRequest(req)) {
try {
const topology = await serverControl.discoverServers(req.srf);
logger.info({source_address: req.source_address, topology},
'responding to X-Jambonz-Discover OPTIONS request');
res.send(200, {
body: JSON.stringify(topology),
headers: {
'Content-Type': 'application/json'
}
});
} catch (err) {
logger.error({err}, 'Error handling discovery OPTIONS');
res.send(503);
}
return req.srf.endSession(req);
}
/* OPTIONS ping from internal FS or RTP server? */
const internal = req.has('X-FS-Status') || req.has('X-RTP-Status');
if (!internal) {
+67
View File
@@ -0,0 +1,67 @@
const debug = require('debug')('jambonz:sbc-server-control');
const { JAMBONES_SERVER_CONTROL, JAMBONES_CLUSTER_ID } = require('../config');
/* SIP header a client sets (X-Jambonz-Discover: true) to request a topology discovery response */
const DISCOVER_HEADER = 'X-Jambonz-Discover';
/**
* Whether the server-control features are enabled.
* Gated behind the JAMBONES_SERVER_CONTROL env var; treats the usual truthy
* strings ('1', 'true', 'yes') as enabled, everything else as disabled.
*/
const isEnabled = () => {
if (!JAMBONES_SERVER_CONTROL) return false;
return /^(1|true|yes)$/i.test(`${JAMBONES_SERVER_CONTROL}`.trim());
};
/* redis set names holding the active servers of each type for this cluster */
const _setNames = () => {
const prefix = JAMBONES_CLUSTER_ID || 'default';
return {
featureServers: `${prefix}:active-fs`,
sipServers: `${prefix}:active-sip`,
rtpServers: `${prefix}:active-rtp`
};
};
/**
* Return true if the request is a discovery request, i.e. it carries the
* `X-Jambonz-Discover: true` header. Header presence/value is checked
* case-insensitively.
*/
const isDiscoverRequest = (req) => {
if (!req.has(DISCOVER_HEADER)) return false;
return /^true$/i.test(`${req.get(DISCOVER_HEADER)}`.trim());
};
/**
* Read the current cluster topology from redis: the IPs of the feature
* servers, SIP servers and RTP servers.
* @param {object} srf - drachtio srf instance (uses srf.locals.retrieveSet)
* @returns {Promise<{featureServers: string[], sipServers: string[], rtpServers: string[]}>}
*/
const discoverServers = async(srf) => {
const { retrieveSet } = srf.locals;
const { featureServers, sipServers, rtpServers } = _setNames();
const [fs, sip, rtp] = await Promise.all([
retrieveSet(featureServers),
retrieveSet(sipServers),
retrieveSet(rtpServers)
]);
const topology = {
featureServers: fs || [],
sipServers: sip || [],
rtpServers: rtp || []
};
debug({topology}, 'discovered cluster topology from redis');
return topology;
};
module.exports = {
DISCOVER_HEADER,
isEnabled,
isDiscoverRequest,
discoverServers
};