Docs Follow one call across its legs

Follow one call across its legs

List the other legs of a call through a B2BUA, SBC or PBX, read how sipnab tied each one, and make the tie an exact match.

On this page

A call through a proxy keeps its Call-ID, so sipnab sees one call. A call through a back-to-back user agent (B2BUA) does not. A PBX such as Asterisk or FreeSWITCH, an SBC, and OpenSIPS with topology hiding end the call on one side and start a new one on the other, each with its own Call-ID. sipnab then holds two calls, or more across several hops, and ties them together.

This page shows how to list a call’s other legs, how to read how sipnab tied each one, and what to change on your SIP servers so the tie is an exact match rather than a guess.

What you need

  • A sipnab build with the default features, for everything except decoding OpenSIPS topology-hiding Call-IDs, which needs the tls feature. Every release package has it.
  • Reading a capture file needs no privileges. A live capture needs root or CAP_NET_RAW.
  • A capture that holds every leg. Capture where the legs meet, on the B2BUA itself or on a link that carries both sides. sipnab cannot join a leg the capture does not hold.
  • A change on your SIP servers only if you want an exact match where there is none today. Make the match exact says what to change for each server.

List the other legs of a call

Name one leg with --correlate. This capture holds one call through an Asterisk PBX: the caller’s leg, and the leg Asterisk placed to the callee.

sipnab -I tests/pcap-samples/b2bua-asterisk.pcapng --correlate b2bua-caller-synth@203.0.113.1 --no-cli-print --quiet
Legs correlated with b2bua-caller-synth@203.0.113.1: 1 leg
Leg                                 Strategy          Method  Score  Match  Gap ms
----------------------------------------------------------------------------------
b2bua-leg-synth@203.0.113.101:5060  timing_heuristic  timing  50     guess  3
Every leg here is a timing guess: no identifier ties it to this call.
  • Strategy names the rule that tied the leg. Method says what sipnab compared the two legs on.
  • Match is identifier when both legs carry the same identifier, and guess when only their timing and addresses tie them.
  • Gap ms is how far apart the two legs started, for a guess.

Here Asterisk wrote nothing into the callee’s leg that names the caller’s, so the tie is a guess. A guess can join unrelated calls on a busy server. Make the match exact changes it to an identifier match.

--report lists the Call-IDs a capture holds. The Call-ID must match exactly.

Walk a call across several hops

A call through an SBC and then a PBX has three legs or more. --call-tree follows the ties leg by leg and prints the whole call. This capture’s calls cross one hop, so each tree has two legs:

sipnab -I tests/fixtures/session-id/opensips-4.0-th-session-id.pcap --call-tree 1-sid-call@example.com --no-cli-print --quiet
Call tree from 1-sid-call@example.com: 2 legs, depth 1, 13 messages, 0 timing guesses
Leg                                       Strategy    Method  Score  Match       Followed
-----------------------------------------------------------------------------------------
1-sid-call@example.com                    root        -       -      -           yes
  DLGCH_fl0WBzdkMzIjHCULKyg9IyMVSw08JA--  session_id  header  100    identifier  yes
  • Each hop indents the leg two more spaces.
  • Followed is no on a guess: sipnab lists the leg but does not look for that leg’s own matches, so one wrong guess cannot pull in unrelated calls.

Read how sipnab tied each leg

sipnab tries each strategy, and reports a leg under the highest-scoring one that matched.

StrategyMethodScoreWhat both legs carryWhat it needs
session_idheader100The same RFC 7989 Session-IDEndpoints or a server that send it. Nothing in sipnab
th_callid_decodeddecoded100A Call-ID that OpenSIPS topology_hiding("C") encoded, which decodes to the other leg’sThe OpenSIPS secret in [sip] th_callid_secret_file
th_callid_mappingmapping100Nothing: OpenSIPS sends sipnab the pairAn OpenSIPS event to sipnab’s HEP listener
x_call_idheader100A header, named in [sip] xcid_headers, holding the other leg’s Call-IDA header your server adds, and its name in sipnab’s configuration
charging_vector_related_icidheader95An RFC 7315 P-Charging-Vector whose related-icid names the other leg’s icid-valueAn IMS or carrier B2BUA that sends it
sdp_originsdp90The same SDP origin lineA server that passes SDP through unchanged
charging_vector_icidheader85The same P-Charging-Vector icid-valueAn intermediary that copied it onto the second leg
via_branchheader80A shared Via branchA proxy hop, not a B2BUA
timing_heuristictiming50Nothing: the legs started close together and share an addressNothing. A guess

Run MCP across an estate explains what each identifier proves and where it stops working.

Make the match exact

Pick one of these, in this order of preference.

Pass RFC 7989 Session-ID through

Session-ID is a pair of identifiers that the two endpoints of a session exchange, made to survive a B2BUA. sipnab reads it with no configuration, and matches the two halves in either order.

This capture holds three calls through OpenSIPS with topology_hiding("C"), which gives the callee’s leg a new Call-ID. The endpoints send Session-ID, and OpenSIPS passes it through unchanged:

sipnab -I tests/fixtures/session-id/opensips-4.0-th-session-id.pcap --correlate 1-sid-call@example.com --no-cli-print --quiet
Legs correlated with 1-sid-call@example.com: 1 leg
Leg                                     Strategy    Method  Score  Match       Gap ms
-------------------------------------------------------------------------------------
DLGCH_fl0WBzdkMzIjHCULKyg9IyMVSw08JA--  session_id  header  100    identifier  -

Which servers send it:

  • Endpoints and SBCs. Check whether yours send it: sipnab -N -I capture.pcap --filter "header.Session-ID =~ '.'" --report lists the calls that carry it.
  • FreeSWITCH writes it when a Sofia profile sets the rfc-7989 parameter to true. FreeSWITCH reads the parameter in src/mod/endpoints/mod_sofia/sofia.c of FreeSWITCH 1.11.3 (read on 2026-11-03). Not tested with sipnab.
  • OpenSIPS passes the header through unchanged, as the capture above shows for OpenSIPS 4.0.2 through topology hiding. A proxy forwards the header fields it does not act on (RFC 3261 section 16.6), so Kamailio as a proxy passes it too. Not tested here with Kamailio.
  • A search of the OpenSIPS, Kamailio and Asterisk source code on GitHub on 2026-11-03 found no code that writes the header.

Copy the caller’s Call-ID into a header

Have the B2BUA add a header to the callee’s INVITE that holds the caller’s Call-ID, then tell sipnab its name. This header names nothing standard, so sipnab looks for none until you name one.

This header discloses the caller’s Call-ID to the callee’s side. Add it only on legs inside your own network, or remove it where the call leaves.

On the server

Asterisk 22. A pre-dial handler sets the header on the outbound channel, from the inbound channel’s Call-ID. In extensions.conf:

[from-caller]
exten => 100,1,Dial(PJSIP/callee,30,b(add-xcid^s^1(${CHANNEL(pjsip,call-id)})))
 same => n,Hangup()

[add-xcid]
exten => s,1,Set(PJSIP_HEADER(add,X-CID)=${ARG1})
 same => n,Return()

Tested on 2026-11-03 with Asterisk 22.11.0: the callee’s INVITE carried X-CID: 1-xcid-call@example.com. The capture below is that test.

OpenSIPS with topology hiding. Add append_hf("X-CID: $ci\r\n"); after topology_hiding("C"). Run sipnab beside OpenSIPS shows the script and the tests that run it. OpenSIPS can also tell sipnab the pair with no header at all. That section describes both.

Kamailio with the topology-hiding module and mask_callid = 1, which encodes the Call-ID (module documentation, read on 2026-11-03). Add the header in the request route before the request leaves, with append_hf:

append_hf("X-CID: $ci\r\n");

Not tested: the Kamailio build on this project’s test host lacks that module. Check on the wire that the callee’s INVITE carries the caller’s Call-ID before you rely on it.

FreeSWITCH. Set a sip_h_ channel variable before the bridge. FreeSWITCH adds a header named after the prefix to the outbound INVITE, and sip_call_id holds the inbound leg’s Call-ID (channel variables, read on 2026-11-03):

<action application="set" data="sip_h_X-CID=${sip_call_id}"/>
<action application="bridge" data="user/1000"/>

Not tested with sipnab.

In sipnab

Name the header in the [sip] section of the configuration file:

[sip]
xcid_headers = ["X-CID"]

Without it, the Asterisk capture’s legs tie only by timing:

sipnab -I tests/fixtures/xcid/asterisk-22-xcid.pcap --correlate 1-xcid-call@example.com --no-cli-print --quiet
Legs correlated with 1-xcid-call@example.com: 1 leg
Leg                                   Strategy          Method  Score  Match  Gap ms
------------------------------------------------------------------------------------
207e9af6-c60c-4f7f-99ae-328d569004c6  timing_heuristic  timing  50     guess  5
Every leg here is a timing guess: no identifier ties it to this call.

With it, they tie on the header:

sipnab -I tests/fixtures/xcid/asterisk-22-xcid.pcap --config sipnab.toml --correlate 1-xcid-call@example.com --no-cli-print --quiet
Legs correlated with 1-xcid-call@example.com: 1 leg
Leg                                   Strategy   Method  Score  Match       Gap ms
----------------------------------------------------------------------------------
207e9af6-c60c-4f7f-99ae-328d569004c6  x_call_id  header  100    identifier  -

xcid_headers takes a list, so one sipnab can read the headers of several servers.

Use OpenSIPS’s own help with topology hiding

OpenSIPS with topology_hiding("C") can tie its legs for sipnab in two more ways: sipnab decodes the hidden Call-ID with OpenSIPS’s secret, or OpenSIPS sends sipnab each pair as an event. Calls through topology hiding gives the steps for both.

Widen the guess for a slow PBX

When no identifier crosses a hop, the timing guess is what remains. It ties two legs that started within two seconds of each other and share an address. A PBX that looks a number up before it places the second leg (an LNP or ENUM query, a routing list) can start it later than that, and the guess then misses. Widen the window, in milliseconds:

sipnab -I tests/pcap-samples/b2bua-asterisk.pcapng --leg-correlation-window 8000 --correlate b2bua-caller-synth@203.0.113.1 --no-cli-print --quiet

The configuration key is [sip] leg_correlation_window_ms. A wider window also ties more unrelated calls on a busy server, which Gap ms shows.

Ask the same question elsewhere

WhereHow
Command line--correlate <CALL-ID>, --call-tree <CALL-ID>, each with --json
RESTGET /v1/dialogs/{call_id}/correlated, GET /v1/dialogs/{call_id}/tree
MCPfind_correlated, get_call_tree
Terminal interfaceIn the call flow, x or F4 draws the related legs in one ladder
vConThe sipnab-related-legs attachment of an exported call

Every surface reports the same strategy and method for each leg. Under --json, --correlate prints the answer the REST route returns:

sipnab -I tests/fixtures/session-id/opensips-4.0-th-session-id.pcap --correlate 1-sid-call@example.com --json --no-cli-print --quiet | jq -c '.legs[] | {call_id, strategy, method, identifier_match}'
{"call_id":"DLGCH_fl0WBzdkMzIjHCULKyg9IyMVSw08JA--","strategy":"session_id","method":"header","identifier_match":true}

When it does not work

What you seeWhyWhat to do
--correlate prints no legsThe capture does not hold the other leg, or nothing ties itCapture where both legs pass. Check with --report that both are there
The legs tie with timing_heuristic although the server adds a header[sip] xcid_headers does not name the header, or sipnab did not read the configuration fileName the header. Pass --config with the file, and check that sipnab does not print that it ignores a configuration file
The legs tie with timing_heuristic and a large Gap ms, or not at allThe B2BUA placed the second leg later than the windowWiden --leg-correlation-window, or add an identifier
Unrelated calls appear as legsTiming guesses on a busy serverRead Match. Add an identifier, so the guess is not needed
Exit 1 and no dialog with Call-IDThe Call-ID is not in the capture, or is not exactCopy it from --report
A topology-hiding leg reports th_callid undecodable or unmatchedThe secret or prefix is wrong, or the caller’s leg is missingSee Calls through topology hiding