Docs Ask each call's rtpengine engine
Ask each call's rtpengine engine
With several rtpengine engines behind one proxy, map each engine's SDP media address and port range to its control address, ask each call's own engine, and find a session left on the old engine.
On this page
- What sipnab asks, and where
- Before you start
- How sipnab finds the engine in the SDP
- Write the engine map
- Ask about a call
- List the calls every engine holds
- See whether media reaches each call’s engine
- Find a session left on the old engine
- Use the SDP address without a map
- Check that it worked
- When it does not work
- See also
Several rtpengine engines can sit behind one SIP proxy. The proxy picks an
engine for each call, and it can move a call to another engine in the middle
of the call. --rtpengine-control names one engine, so a question about a
call that another engine carries goes to the wrong engine.
Each engine writes its own media address into the SDP it rewrites. sipnab can read that address from the call’s SDP and ask the engine that carries the call. Use this page when:
- the relay sessions view shows
Unknown call-idfor calls that do have media; - you need the media counters of one call, and the call is on another engine;
- a call moved to another engine, and you suspect the old engine still holds a session for it.
To ask one relay about itself, read Ask a media relay about itself first. This page adds the engine choice to the questions described there.
What sipnab asks, and where
| Question | Asked of |
|---|---|
One call: GET /v1/relay/stats/call/{call_id}, compare/{call_id}, holdings/{call_id}, the MCP query_relay, relay_stats and relay_compare tools with a call_id, the TUI relay view of a call, --relay-compare, and each query the sessions view sends | The engine the call’s newest SDP names |
Which calls each engine holds: --relay-holdings, GET /v1/relay/holdings, query_relay without a call_id, and the list of the sessions view (--relay-sessions, GET /v1/relay/sessions, query_relay with sessions: true, the TUI’s H) | --rtpengine-control and every engine an engine row names, each answer shown on its own |
The relay’s own counters: --relay-stats, --relay-stats-list, --relay-stats-interval, GET /v1/relay/stats and stats/names, relay_stats without a call_id | --rtpengine-control only |
| Startup reconciliation, which names the calls behind media already flowing (Let sipnab name rtpengine’s media) | --rtpengine-control only |
| A call whose SDP sipnab has not seen, in the sessions view | The engine whose list names the call, or --rtpengine-control when none does |
| A call whose SDP sipnab has not seen, any other question | --rtpengine-control |
Each answer says why it went where it went. --relay-stats-call and
--relay-sessions ask before sipnab reads any SIP, so they see no SDP. A TUI,
REST or MCP run that captures the signaling sees it.
Before you start
- On each rtpengine engine:
- a
listen-ngaddress that the sipnab host can reach. rtpengine’s control protocol has no authentication, so allow only the sipnab host to reach it. - the media address the engine writes into SDP. That is its
interfacesetting, or the advertised address after!in it, for exampleinterface = 192.0.2.5!203.0.113.5. - a port range,
port-minandport-max. Engines that advertise one public address need port ranges that do not overlap. sipnab tells such engines apart by port alone. - nothing else. sipnab sends only
listandquery, andstatisticsto--rtpengine-controlfor the relay-statistics requests. None of them changes a call.
- a
- On the SIP proxy: nothing to change. OpenSIPS with several engines in
its
rtpengine_sockset, or Kamailio with severalrtpenginenodes, picks the engine as before. - sipnab must see the SIP on both sides of the proxy: the message that
reaches the proxy and the message the proxy sends on. Capture on the proxy’s
host (
-d <device>), or have the proxy send HEP to a sipnab listener (-L <addr>). A capture of one side only sees the engine’s address in one direction, which matters only for--rtpengine-trust-sdp-address. - A live source. Asking transmits, so sipnab asks only on a run that captures an interface or listens for HEP. A run that reads a capture file never asks.
--rtpengine-control <listen-ng address>of one engine. It answers the questions in the table that are not about one call.- For REST, start the server with
--api-allow-relay-query. For MCP, start it with--mcp-allow-relay-query. - Build. Every release binary has it. A source build needs the
nativefeature, which the default build includes.
The examples use these addresses. Replace them with yours.
| Media address in SDP | Ports | listen-ng | |
|---|---|---|---|
| Engine A | 203.0.113.5 | 30000-39999 | 192.0.2.5:2223 |
| Engine B | 203.0.113.5 | 40000-49999 | 192.0.2.6:2223 |
How sipnab finds the engine in the SDP
A call through an anchoring relay carries four SDP bodies at the proxy: the
caller’s own, the engine’s rewrite of it sent to the callee, the callee’s own,
and the engine’s rewrite of that sent to the caller. Measured on
tests/fixtures/opensips-proxy-signaling.pcap, a call through OpenSIPS and
one rtpengine:
| Message | SDP media |
|---|---|
| INVITE, caller to proxy | 198.51.100.21:6000, the caller |
| INVITE, proxy to callee | 198.51.100.10:30020, the engine |
| 200 OK, callee to proxy | 198.51.100.20:6000, the callee |
| 200 OK, proxy to caller | 198.51.100.10:30018, the engine |
sipnab reads the c= address and the port of the first m= line of each
body, in the order it captured them.
- With an engine map, a body belongs to an engine when a row claims its address and port. The parties’ own addresses match no row.
- The engine of the newest body a row claims is the call’s engine. After a re-INVITE moves the call, that is the new engine.
- The engine of the newest earlier body with another
listen-ngaddress is the call’s previous engine. - A call whose SDP names no address a row claims is
engine unknown (no map row). sipnab asks no engine about it, not even--rtpengine-control: the map is the list of engines sipnab may ask.
Write the engine map
Give one row per engine and port range, as
MEDIA-ADDRESS LOW-HIGH CONTROL-ADDRESS:PORT. On the command line, quote each
row:
sudo sipnab -d eth0 \
--rtpengine-control 192.0.2.5:2223 \
--rtpengine-engine '203.0.113.5 30000-39999 192.0.2.5:2223' \
--rtpengine-engine '203.0.113.5 40000-49999 192.0.2.6:2223'
Or keep the rows in the config file, [relay] engines:
[relay]
engines = [
"203.0.113.5 30000-39999 192.0.2.5:2223",
"203.0.113.5 40000-49999 192.0.2.6:2223",
]
--rtpengine-enginerows add to[relay] engines.- sipnab accepts a
->between the port range and the control address, as in203.0.113.5 30000-39999 -> 192.0.2.5:2223. - Write an IPv6 media address plain and an IPv6 control address in
brackets:
2001:db8::5 30000-39999 [2001:db8::10]:2223. - One engine can have several rows, for several media addresses.
- sipnab refuses two rows that claim one address and port, because a call there would have two engines.
--rtpengine-controlcan be one of the engines in the map, or another one. sipnab lists it with the others either way, and asks it once.
Ask about a call
The output in this section came from this build of sipnab with two scripted
engines on loopback, the fake ng responder sipnab’s own tests use, on
127.0.0.2:2223 (engine A) and 127.0.0.3:2223 (engine B). Each answered
query with a recorded rtpengine 12.5.1 reply. The SIP reached sipnab over
HEP: one call’s first offer and answer on engine A’s ports, then a re-INVITE
that moved it to engine B’s. The commands use the addresses in the table
above. The output shows the loopback addresses it came from, 127.0.0.2:2223
for engine A and 127.0.0.3:2223 for engine B.
Start a server that sees the SIP, with the map and the REST opt-in:
sudo sipnab -N -d eth0 \
--rtpengine-control 192.0.2.5:2223 \
--rtpengine-engine '203.0.113.5 30000-39999 192.0.2.5:2223' \
--rtpengine-engine '203.0.113.5 40000-49999 192.0.2.6:2223' \
--api 127.0.0.1:18764 --api-allow-relay-query
Ask for one call’s counters. The relay label says which engine answered and which row chose it:
curl -s "http://127.0.0.1:18764/v1/relay/stats/call/moved-1@192.0.2.21" | jq '{outcome, relay}'
{
"outcome": "ok",
"relay": "relay (rtpengine at 127.0.0.3:2223), chosen by engine row 203.0.113.5 40000-49999 127.0.0.3:2223 for SDP media 203.0.113.5:40012, call moved-1@192.0.2.21"
}
The call’s newest SDP named 203.0.113.5:40012, which the second row claims,
so sipnab asked engine B and not --rtpengine-control.
A call whose SDP names media no row claims is not asked of any engine:
{
"outcome": "not_configured",
"detail": "call other-1@192.0.2.21: engine unknown (no map row): no engine row claims the call's SDP media 192.0.2.21:6000, 203.0.113.9:30010, 192.0.2.20:6000, 203.0.113.9:30012, 203.0.113.9:40010, 203.0.113.9:40012; nothing was asked"
}
compare/{call_id}, holdings/{call_id}, the MCP tools, the TUI and
--relay-compare choose the same way. Over MCP, relay_stats with
{"call_id": "moved-1@192.0.2.21"} answered:
{
"relay_address": "127.0.0.3:2223",
"outcome": "ok",
"engine": "chosen by engine row 203.0.113.5 40000-49999 127.0.0.3:2223 for SDP media 203.0.113.5:40012"
}
relay_address is the engine asked. engine says why. The answer leaves it
out when the run has no engine rule.
List the calls every engine holds
sipnab -N -L 127.0.0.1:0 --rtpengine-control 192.0.2.5:2223 \
--rtpengine-engine '203.0.113.5 30000-39999 192.0.2.5:2223' \
--rtpengine-engine '203.0.113.5 40000-49999 192.0.2.6:2223' \
--relay-holdings 2>/dev/null
Holdings of relay (rtpengine at 127.0.0.2:2223), asked 2026-11-05T17:59:20.119782970+00:00
1 call held.
Call-ID
------------------
moved-1@192.0.2.21
Holdings of relay (rtpengine at 127.0.0.3:2223), asked 2026-11-05T17:59:20.119782970+00:00
2 calls held.
Call-ID
-------------------
moved-1@192.0.2.21
only-b-1@192.0.2.21
Held by several engines: moved-1@192.0.2.21
- Each engine gets a
listand a block of its own. An engine that does not answer gets a line saying so, and the others’ blocks still print. - The run exits 0 when at least one engine answered, and 1 when none did.
--jsonandGET /v1/relay/holdingsaddengines, each engine’s own answer, andheld_by_several_engines, besidecall_ids, which holds every Call-ID once.query_relaywithout acall_idcarries the same three.
See whether media reaches each call’s engine
The sessions view lists every engine, then queries each call on its own engine:
sipnab -N -L 127.0.0.1:0 --rtpengine-control 192.0.2.5:2223 \
--rtpengine-engine '203.0.113.5 30000-39999 192.0.2.5:2223' \
--rtpengine-engine '203.0.113.5 40000-49999 192.0.2.6:2223' \
--relay-sessions 2>/dev/null
Sessions of relay (rtpengine at 127.0.0.2:2223), relay (rtpengine at 127.0.0.3:2223), asked 2026-11-05T18:10:16.347476084+00:00 (relay_reported)
reachable; 2 calls listed; queried 2 of 2 (scan)
Engine relay (rtpengine at 127.0.0.2:2223): reachable; 1 call listed
Engine relay (rtpengine at 127.0.0.3:2223): reachable; 2 calls listed
Note: a relay keeps a call it was told to delete for its delete delay (30 s by default) and its answer does not mark it; a call sipnab did not see end in SIP may have ended up to about 30 s ago.
Note: stalled means no packet for more than 2 s while another socket of the same call received one; silence suppression, comfort noise, hold and loss bursts were not measured and can also produce it.
moved-1@192.0.2.21 held by several engines from callee-tag-1: flowing; from caller-tag-1: flowing
only-b-1@192.0.2.21 from callee-tag-1: flowing; from caller-tag-1: flowing
Call moved-1@192.0.2.21 (relay_reported)
Asked relay (rtpengine at 127.0.0.2:2223) (sipnab has seen no SDP for this call; this engine's list names it).
Held by several engines.
That is the start of the answer. Each call’s socket table follows.
--relay-sessions asks before it reads any SIP, so it has seen no SDP: it
asks each call of the first engine whose list names it. held by several engines marks a Call-ID that two engines list. A run that sees the SIP asks
each call of the engine its SDP names, as below.
Find a session left on the old engine
After a re-INVITE moves a call, the new engine holds it, and the old engine
should have released it. Add --relay-previous-engine to a run that sees the
SIP, and the sessions view asks both:
sudo sipnab -N -d eth0 \
--rtpengine-control 192.0.2.5:2223 \
--rtpengine-engine '203.0.113.5 30000-39999 192.0.2.5:2223' \
--rtpengine-engine '203.0.113.5 40000-49999 192.0.2.6:2223' \
--relay-previous-engine \
--api 127.0.0.1:18764 --api-allow-relay-query
curl -s "http://127.0.0.1:18764/v1/relay/sessions?call_id=moved-1@192.0.2.21" | jq '.calls[0].engine'
{
"asked": "relay (rtpengine at 127.0.0.3:2223)",
"chosen_by": "map_row",
"held_by_several_engines": true,
"listed_by": [
"relay (rtpengine at 127.0.0.2:2223)",
"relay (rtpengine at 127.0.0.3:2223)"
],
"previous": {
"chosen_by": "map_row",
"holds": true,
"outcome": "ok",
"reason": "chosen by engine row 203.0.113.5 30000-39999 127.0.0.2:2223 for SDP media 203.0.113.5:30012",
"relay": "relay (rtpengine at 127.0.0.2:2223)"
},
"reason": "chosen by engine row 203.0.113.5 40000-49999 127.0.0.3:2223 for SDP media 203.0.113.5:40012"
}
askedis the engine the newest SDP names, andpreviousis the engine the SDP named before the move.previous.holdsis true: the old engine still holds the call.held_by_several_enginesis true when two engines hold the Call-ID, by their lists or by their answers.- In the TUI, the call’s view prints the previous engine on a line that
starts
Previous engineand names its relay and row, and endsstill holds this call; a session was left on the old engine., thenHeld by several engines. - The answer has
engineonly when the run has an engine rule. Each queried call has one, and so does each call innot_queried, withlisted_byandheld_by_several_engines. The top level addsengines, each engine’s own answer tolist.
rtpengine keeps a deleted call for its delete delay, 30 s by default. A call
that moved a few seconds ago can still read held by several engines. Ask
again after the delay before you treat the old engine’s session as left
behind.
Use the SDP address without a map
--rtpengine-trust-sdp-address <CONTROL-PORT> asks the engine at the media
address the call’s SDP names, on the control port you give. Use it only when
every SDP writer on the captured network is yours.
Risk. sipnab then sends list and query to an address it read from
captured traffic. Anyone who can put SDP on that network can choose where
sipnab sends them. A map does not have this risk: sipnab asks only the engines
its rows name.
- An address counts as an engine’s only when sipnab saw it in both a request’s
SDP and a response’s. That is what an anchoring relay’s rewrite looks like
at the proxy. sipnab sees a party’s own address one way only. A capture of
one side of the proxy sees the engine one way, and the call reads
engine unknown (no address seen both ways). - sipnab asks the engine of the newest such sighting, and the one before a move is the previous engine, as with a map.
- It needs
--rtpengine-control, and sipnab refuses it beside--rtpengine-engineor[relay] engines. There is no config key. listgoes to--rtpengine-controlonly. sipnab never sendslistto an address it learned from SDP.
With SDP that named 127.0.0.3 both ways, and --rtpengine-trust-sdp-address 2223:
{
"outcome": "ok",
"relay": "relay (rtpengine at 127.0.0.3:2223), chosen from SDP media 127.0.0.3:40012, trusted as the engine's address, call only-b-1@192.0.2.21"
}
Check that it worked
- A question about a captured call names the engine and the row that chose
it, for example
chosen by engine row 203.0.113.5 40000-49999 192.0.2.6:2223 for SDP media 203.0.113.5:40012. --relay-holdingsprints oneHoldings ofblock per engine, and every engine in your map is there.- A call you know is on engine B reads
Asked relay (rtpengine at 192.0.2.6:2223)in the sessions view, not engine A.
When it does not work
call other-1@192.0.2.21: engine unknown (no map row): no engine row claims the call's SDP media 192.0.2.21:6000, 203.0.113.9:30010, 192.0.2.20:6000, 203.0.113.9:30012, 203.0.113.9:40010, 203.0.113.9:40012; nothing was asked. The call’s SDP names media no row claims. The message lists every address and port sipnab saw for the call. Add a row for the engine’s address and port range. If only the parties’ addresses are listed, sipnab saw no message the engine rewrote: check that the capture sees both sides of the proxy, or that the proxy anchors the call’s media at all.the default engine (--rtpengine-control): sipnab has seen no SDP for this callorsipnab has seen no SDP for this call; this engine's list names it. sipnab holds no SDP for the call: the run started after the call’s last offer and answer, the call’s SIP went past another capture point, or the run is--relay-sessionsor--relay-stats-call, which ask before sipnab reads any SIP. Ask from a TUI, REST or MCP run that sees the call’s next re-INVITE, or ask that engine with--rtpengine-control.Unknown call-idfrom the engine sipnab chose. The engine does not hold the call. Check that each row’s port range is that engine’s ownport-minandport-max; a range that belongs to another engine sends the question to the wrong one.Engine relay (rtpengine at 127.0.0.9:2223): unreachable: relay (rtpengine at 127.0.0.9:2223) did not answer (no reply from 127.0.0.9:2223 within 2s); asked, nothing came backin the sessions status. That engine did not answer itslist. The calls the other engines hold are still in the answer. Check that the sipnab host can reach thatlisten-ngaddress.--rtpengine-engine (or [relay] engines) needs --rtpengine-control as well: that engine answers the questions that are not about one call (the relay's statistics, the poll, startup reconciliation) and about a call whose SDP sipnab has not seen. Name one engine's listen-ng address, for example --rtpengine-control 192.0.2.5:2223and exit 2 (1 for[relay] engines). Add--rtpengine-controlwith one engine’slisten-ngaddress.--rtpengine-engine / [relay] engines: engine rows "203.0.113.5 30000-40000 192.0.2.5:2223" and "203.0.113.5 40000-49999 192.0.2.6:2223" overlap: both claim media 203.0.113.5 port 40000, so a call there has no single engineand exit 2 (1 when both rows are in[relay] engines). Make the port ranges the engines’ own, which do not overlap.error: invalid value '203.0.113.5 40000 192.0.2.6:2223' for '--rtpengine-engine <MEDIA LOW-HIGH CONTROL>': engine row "203.0.113.5 40000 192.0.2.6:2223": "40000" is not a port range LOW-HIGH with 1 <= LOW <= HIGH <= 65535and exit 2. The row is not three fields, or one field does not parse. The message names the field. A bad row in[relay] enginesreads, for example,invalid config value: [relay] engines: engine row "203.0.113.5 39999-30000 192.0.2.5:2223": "39999-30000" is not a port range LOW-HIGH with 1 <= LOW <= HIGH <= 65535, and exits 1.error: the following required arguments were not provided: --rtpengine-control <ADDR>with--relay-holdingsor--relay-sessions. Those flags need--rtpengine-control, with or without a map.--relay-previous-engine needs an engine rule to name a previous engine: add --rtpengine-engine rows, [relay] engines, or --rtpengine-trust-sdp-addressand exit 2. Add engine rows, or drop the flag.--rtpengine-trust-sdp-address and an engine map (--rtpengine-engine or [relay] engines) are two rules for one question; the map is an allowlist and trusting SDP is none. Give one of themand exit 2. Keep one rule.
Error messages lists every refusal with its exit code.
See also
- Ask a media relay about itself: the questions this page routes.
- Let sipnab name rtpengine’s media: startup
reconciliation, which asks
--rtpengine-controlonly. - CLI reference:
--rtpengine-engine,--rtpengine-trust-sdp-addressand--relay-previous-engine. - Config reference:
[relay] engines.