Files
sbc-sip-sidecar/README.md
T
Dave HortonandClaude Opus 5.5 f5c675952a feat: per-SBC registration Call-ID, and hand off the regbot role on AWS scale-in (#156)
The Call-ID of outbound registrations was the sip_gateway_sid, the same on
every SBC. When the regbot role moved to the other SBC, the registrar saw a
refresh of an existing binding from a different source address and Contact.
Some registrars 200 such a refresh without updating their routing, so inbound
calls to the registered trunk fail with 404 until the binding is recreated.
The Call-ID is now sip_gateway_sid@<sending SBC public IP>: stable across
refreshes and restarts of one SBC, new when the role moves, so a move looks
like a new registration. register_status also records the sending SBC as
sbcAddress.

With AWS_LIFECYCLE_DRAIN enabled the sidecar polls IMDS
autoscaling/target-lifecycle-state (the signal inbound drains on; detection
only, inbound completes the lifecycle hook). When the instance is being scaled
in, the regbot holder releases the lease while still running instead of after
the instance is gone; until now the draining SBC kept the registrations, so
carriers kept sending registration-trunk calls to an SBC that answers new
INVITEs with 503. It never claims the role back. Once another SBC has claimed
it, the draining SBC un-REGISTERs (Expires: 0) the bindings whose Contact
carries its own IP. Bindings with an AoR or realm Contact are left alone: the
new SBC sends the same Contact, and its REGISTER has already replaced ours.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:33:36 -04:00

171 lines
9.3 KiB
Markdown

# sbc-sip-sidecar ![Build Status](https://github.com/jambonz/sbc-sip-sidecar/workflows/CI/badge.svg)
This application provides a part of the SBC (Session Border Controller) functionality of jambonz platform. It handles incoming/outgoing REGISTER requests from/to clients/servers (including both sip softphones and WebRTC client applications), incoming OPTIONS. Register Authentication is delegated to customer-side logic via a web callback configured for the account in the jambonz database. Information about active registrations is stored in a redis database.
## Configuration
Configuration is provided via environment variables:
| variable | meaning | required?|
|----------|----------|---------|
|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|
|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_PORT| mysql port |no|
|JAMBONES_MYSQL_CONNECTION_LIMIT| mysql connection limit |no|
|JAMBONES_CLUSTER_ID| cluster id |no|
|JAMBONES_REDIS_HOST| redis host|yes|
|JAMBONES_REDIS_PORT|redis port|no|
|JAMBONES_TIME_SERIES_HOST| influxdb host |yes|
|CHECK_EXPIRES_INTERVAL| servers expiration check interval |no|
|EXPIRES_INTERVAL| servers expire |no|
|JWT_SECRET| secret for signing JWT token |yes|
|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|
|AWS_LIFECYCLE_DRAIN| set to a truthy value ('1', 'true', 'yes') on AWS Auto Scaling deployments: when IMDS reports this instance is being scaled in, the SBC hands the outbound-registration role to another SBC and removes its own IP-based bindings (see below) |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|
## Outbound registration across SBCs
One SBC in a cluster at a time sends REGISTERs for carriers that require registration; it holds a lease in redis, and another SBC takes over if the lease lapses. The Call-ID of each registration is `<sip_gateway_sid>@<sending SBC public IP>`: stable across refreshes and restarts of one SBC, but new when the role moves to another SBC, so a registrar sees a new registration rather than a refresh from a different address. The `register_status` of a carrier records the sending SBC in `sbcAddress`.
With `AWS_LIFECYCLE_DRAIN` enabled, an SBC that is being scaled in gives up the lease as soon as the scale-in starts, instead of after the instance is gone. Once another SBC has claimed it, the draining SBC sends un-REGISTERs (Expires: 0) for bindings whose Contact carries its own IP (`register_public_ip_in_contact`); other bindings use the same Contact on both SBCs and have already been replaced by the new SBC's registrations.
## 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
{
"jambonzVersion": "v11.1.4",
"drachtioVersion": "v0.9.3",
"featureServers": ["10.0.0.10:5060"],
"sipServers": ["1.2.3.4"],
"rtpServers": ["10.0.0.20"]
}
```
`jambonzVersion` is read from the `schema_version` table in the database and `drachtioVersion` is the version reported by the drachtio server on connect; either is `null` if it could not be determined. `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
The SBC provides a CLI tool for runtime management of feature servers. Use the CLI to drain/undrain feature servers during maintenance or scaling operations.
### Installation
```bash
npm install
```
### Usage
```bash
# Show all commands
npm run cli
# Drain a feature server (remove from active pool)
npm run cli fs drain 192.168.1.10
# Undrain a feature server (add back to active pool)
npm run cli fs undrain 192.168.1.10
# List currently drained servers
npm run cli fs drained
# List all available feature servers
npm run cli fs active
# Show all servers with status
npm run cli fs list
```
### Examples
```bash
# During maintenance - drain server before updates
npm run cli fs drain 10.0.1.5
# After maintenance - bring server back online
npm run cli fs undrain 10.0.1.5
# Check which servers are available for draining
npm run cli fs active
```
The CLI connects via Unix socket (`/tmp/sbc-sip-sidecar.sock`) and requires admin access to the server.
## Registrar database
A redis database is used to hold active registrations. When a register request arrives and is authenticated, the following values are parsed from the request:
- the address of record, or "aor" (e.g, daveh@drachtio.org),
- the sip uri, or "contact" that this user is advertising (e.g. sip:daveh@3.44.3.12:5060)
- the source address and port that sent the REGISTER request to the server
- the transport protocol that should be used to contact the user (e.g. udp, tcp, wss etc)
- the sip address of the drachtio server that received the REGISTER request, and
- the expiration of the registration, in seconds.
- the application callback that should be invoked when a call is placed from this registered device
- the application status callback that should invoked for call events on calls placed from this registered device
A hash value is created from these values and stored with an expiry value equal to the number of seconds granted to the registration (note that when a sip client is detected as being behind a firewall, the application will reduce the granted expires value to 30 seconds, in order to force the client to re-register frequently, however the expiry in redis is set to the longer, originally requested expires value).
The hash value is inserted with a key being the aor:
```
aor => {contact, source, protocol, sbcAddress, call_hook, call_status_hook}, expiry = registration expires value
```
## http callback
Authenticating users is the responsibility of the client by exposing an http callback. A POST request will be sent to the configured callback (i.e. the value in the `accounts.registration_hook` column in the associated sip realm value in the REGISTER request). The body of the POST will be a json payload including the following information:
```
{
"method": "REGISTER",
"expires": 3600,
"scheme": "digest",
"username": "john",
"realm": "jambonz.org",
"nonce": "157590482938000",
"uri": "sip:172.37.0.10:5060",
"response": "be641cf7951ff23ab04c57907d59f37d",
"qop": "auth",
"nc": "00000001",
"cnonce": "6b8b4567",
"algorithm": "MD5"
}
```
It is the responsibility of the customer-side logic to retrieve the associated password for the given username and to then authenticate the request by calculating a response hash value (per the algorithm described in [RFC 2617](https://tools.ietf.org/html/rfc2617#section-3.2.2)) and comparing it to the response property in the http body.
For example code showing how to calculate the response hash given the above inputs, [see here](https://github.com/jambonz/customer-auth-server/blob/master/lib/utils.js).
For a simple, full-fledged example server doing the same, [see here](https://github.com/jambonz/customer-auth-server).
The customer server SHOULD return a 200 OK response to the http request in all cases with a json body indicating whether the request was successfully authenticated.
The body MUST include a `status` field with a value of either `ok` or `fail`, indicating whether the request was authenticated or not.
```
{"status": "ok"}
```
Additionally, in the case of failure, the body MAY include a `msg` field with a human-readable description of why the authentication failed.
```
{"status": "fail", "msg": "invalid username"}
```
In the case of success, the body MAY include an `expires` value which specifies the duration of time, in seconds, to grant for this registration. If not provided, the expires value in the REGISTER request is used; if provided, however, the value provided must be less than or equal to the duration requested.
```
{"status": "ok", "expires": 300}
```
Additionally in the case of success, the body SHOULD include `call_hook` and `call_status_hook` properties that reference the application URLs to use when calls are placed from this device. If these values are not provided, outbound calling from the device will not be allowed.
## Running the test suite
To run the included test suite, you will need to have a mysql server installed on your laptop/server. You will need to set the MYSQL_ROOT_PASSWORD env variable to the mysql root password before running the tests. The test suite creates a database named 'jambones_test' in your mysql server to run the tests against, and removes it when done.
```
MYSQL_ROOT_PASSWORD=foobar npm test
```