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

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-id for 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

QuestionAsked 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 sendsThe 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 viewThe 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-ng address 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 interface setting, or the advertised address after ! in it, for example interface = 192.0.2.5!203.0.113.5.
    • a port range, port-min and port-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 list and query, and statistics to --rtpengine-control for the relay-statistics requests. None of them changes a call.
  • On the SIP proxy: nothing to change. OpenSIPS with several engines in its rtpengine_sock set, or Kamailio with several rtpengine nodes, 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 native feature, which the default build includes.

The examples use these addresses. Replace them with yours.

Media address in SDPPortslisten-ng
Engine A203.0.113.530000-39999192.0.2.5:2223
Engine B203.0.113.540000-49999192.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:

MessageSDP media
INVITE, caller to proxy198.51.100.21:6000, the caller
INVITE, proxy to callee198.51.100.10:30020, the engine
200 OK, callee to proxy198.51.100.20:6000, the callee
200 OK, proxy to caller198.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-ng address 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-engine rows add to [relay] engines.
  • sipnab accepts a -> between the port range and the control address, as in 203.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-control can 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 list and 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.
  • --json and GET /v1/relay/holdings add engines, each engine’s own answer, and held_by_several_engines, beside call_ids, which holds every Call-ID once. query_relay without a call_id carries 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"
}
  • asked is the engine the newest SDP names, and previous is the engine the SDP named before the move. previous.holds is true: the old engine still holds the call.
  • held_by_several_engines is 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 engine and names its relay and row, and ends still holds this call; a session was left on the old engine., then Held by several engines.
  • The answer has engine only when the run has an engine rule. Each queried call has one, and so does each call in not_queried, with listed_by and held_by_several_engines. The top level adds engines, each engine’s own answer to list.

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-engine or [relay] engines. There is no config key.
  • list goes to --rtpengine-control only. sipnab never sends list to 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-holdings prints one Holdings of block 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 call or sipnab 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-sessions or --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-id from the engine sipnab chose. The engine does not hold the call. Check that each row’s port range is that engine’s own port-min and port-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 back in the sessions status. That engine did not answer its list. The calls the other engines hold are still in the answer. Check that the sipnab host can reach that listen-ng address.
  • --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:2223 and exit 2 (1 for [relay] engines). Add --rtpengine-control with one engine’s listen-ng address.
  • --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 engine and 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 <= 65535 and exit 2. The row is not three fields, or one field does not parse. The message names the field. A bad row in [relay] engines reads, 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-holdings or --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-address and 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 them and exit 2. Keep one rule.

Error messages lists every refusal with its exit code.

See also