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
tlsfeature. 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.
Strategynames the rule that tied the leg.Methodsays what sipnab compared the two legs on.Matchisidentifierwhen both legs carry the same identifier, andguesswhen only their timing and addresses tie them.Gap msis 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.
Followedisnoon 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.
| Strategy | Method | Score | What both legs carry | What it needs |
|---|---|---|---|---|
session_id | header | 100 | The same RFC 7989 Session-ID | Endpoints or a server that send it. Nothing in sipnab |
th_callid_decoded | decoded | 100 | A Call-ID that OpenSIPS topology_hiding("C") encoded, which decodes to the other leg’s | The OpenSIPS secret in [sip] th_callid_secret_file |
th_callid_mapping | mapping | 100 | Nothing: OpenSIPS sends sipnab the pair | An OpenSIPS event to sipnab’s HEP listener |
x_call_id | header | 100 | A header, named in [sip] xcid_headers, holding the other leg’s Call-ID | A header your server adds, and its name in sipnab’s configuration |
charging_vector_related_icid | header | 95 | An RFC 7315 P-Charging-Vector whose related-icid names the other leg’s icid-value | An IMS or carrier B2BUA that sends it |
sdp_origin | sdp | 90 | The same SDP origin line | A server that passes SDP through unchanged |
charging_vector_icid | header | 85 | The same P-Charging-Vector icid-value | An intermediary that copied it onto the second leg |
via_branch | header | 80 | A shared Via branch | A proxy hop, not a B2BUA |
timing_heuristic | timing | 50 | Nothing: the legs started close together and share an address | Nothing. 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 =~ '.'" --reportlists the calls that carry it. - FreeSWITCH writes it when a Sofia profile sets the
rfc-7989parameter totrue. FreeSWITCH reads the parameter insrc/mod/endpoints/mod_sofia/sofia.cof 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
| Where | How |
|---|---|
| Command line | --correlate <CALL-ID>, --call-tree <CALL-ID>, each with --json |
| REST | GET /v1/dialogs/{call_id}/correlated, GET /v1/dialogs/{call_id}/tree |
| MCP | find_correlated, get_call_tree |
| Terminal interface | In the call flow, x or F4 draws the related legs in one ladder |
| vCon | The 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 see | Why | What to do |
|---|---|---|
--correlate prints no legs | The capture does not hold the other leg, or nothing ties it | Capture 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 file | Name 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 all | The B2BUA placed the second leg later than the window | Widen --leg-correlation-window, or add an identifier |
| Unrelated calls appear as legs | Timing guesses on a busy server | Read Match. Add an identifier, so the guess is not needed |
Exit 1 and no dialog with Call-ID | The Call-ID is not in the capture, or is not exact | Copy it from --report |
A topology-hiding leg reports th_callid undecodable or unmatched | The secret or prefix is wrong, or the caller’s leg is missing | See Calls through topology hiding |