Trace where sipnab learned a media endpoint, with three MCP tools
Three MCP tools report how sipnab learned the address of a call's audio, whether anything authenticated the path that delivered it, and why an audio stream has no call attached.
In short
sipnab may report that a call’s audio went to a given address. That address can come from three places:
- the phones’ own call setup messages;
- a media relay that sipnab asked directly;
- a relay message sipnab picked up from the network. sipnab cannot check who sent it.
Three tools on sipnab’s MCP server (the interface an AI agent uses) report:
- which of these applies to each address,
- how much trust the path that delivered it deserves,
- why an audio stream in the capture has no call attached to it.
Who this is for
- Engineers who run sipnab beside an rtpengine media relay.
- They need to know how much weight to give an address before quoting it in an incident review.
- Anyone writing an agent that calls these tools.
What it does
A media relay such as rtpengine carries the audio (RTP) of a call between the two parties. sipnab can learn which relay ports belong to which call in two ways:
- by reading the relay’s control messages (the
ngprotocol); - by asking the relay itself with
--rtpengine-control.
The three tools below report how each piece of that knowledge arrived.
explain_attribution: where each endpoint of one call came from
Give it a Call-ID (the identifier SIP gives a call). It returns one row per media endpoint with these fields:
asserted_by:signaledwhen the address came from the parties’ own SDP. SDP is the part of a SIP message that describes the audio.media-relaywhen a relay named it.
input_origin:- which capture source carried it (
wire,heporuprobe); - nothing when sipnab asked the relay directly.
- which capture source carried it (
delivery_trust: how much trust the delivery path deserves, as one of the values below.delivery_note: the same verdict as one sentence.
The response also carries unauthenticated_endpoints. That is the number of
rows whose delivery_trust is port-gated-only.
delivery_trust | Meaning |
|---|---|
asked | sipnab asked the relay over its control port, so no third party could have answered. |
hmac-verified | Delivered to sipnab’s HEP listener, authenticated with a per-message HMAC token. |
plain-secret | Delivered to sipnab’s HEP listener with a shared secret sent as-is; anyone who captures one packet can reuse the secret. |
port-gated-only | Accepted because it arrived on the expected port, and for no other reason. Nothing authenticated the sender. |
not-relay-asserted | The parties’ own claim in SDP, not a relay’s. |
The table runs from the most to the least trustworthy relay path. The parties’ own claim comes last.
A relay may deliver an endpoint over a capture path. Then delivery_trust
comes from how the run’s HEP listener (--hep-listen) checks its senders:
hmac-verifiedwhen the listener has a secret (--hep-auth-fileor--hep-auth) and--hep-auth-mode hmac;plain-secretwhen it has a secret and the defaultplainmode;port-gated-onlywhen sipnab checks no secret.- This includes every run that does not listen for HEP.
That posture covers only what the listener delivered. A HEP datagram read off
a capture interface or a file, including one --hep-parse unwraps, reached no
listener, and nothing checked its secret. It is port-gated-only on every run.
The listener drops a packet that fails its check.
- So every stored assertion passed the configured check.
- The
server_capabilitiestool reports the same value for the whole run inruntime.hep_delivery_trust.
decode_ng: one relay control message, and how it arrived
Give it a pointer to one frame of a capture (<file>#<ordinal>, counting from
0). It decodes the relay control message in that frame and reports:
delivery:hepfor a message wrapped in HEP. HEP is the Homer Encapsulation Protocol rtpengine uses to mirror its control traffic.sniffed-udpfor a bare control message read off the network.
delivery_trustanddelivery_note, on the same scale as above.delivery_trustis alwaysport-gated-onlyhere. The frame comes from a capture file, so no listener on this run checked it.
on_believed_mirror_port: for a HEP-wrapped message, whether it arrived on UDP port 9060.- sipnab reads a relay mirror it picks up from the network only on that port.
- The port is the only check applied to it.
reconcile_orphans: why an audio stream has no call
An orphan is an audio stream that no call in the capture claims. For each
one, this tool gives a reason:
reason | Meaning |
|---|---|
relay-asserted-but-no-dialog | A relay named this endpoint and no captured call claims it: the call setup messages are missing, not the audio. |
signaled-but-no-dialog | SDP named this endpoint and no call holds it. |
never-named | Nothing in the capture named this endpoint. |
The response carries relay_was_consulted.
- A
never-namedstream withrelay_was_consulted: falsemeans sipnab did not ask a relay about it. - It does not mean that a relay answered and did not know the stream.
How to turn it on
The tools are part of sipnab’s MCP server, in builds with the mcp feature.
Start the server on a capture. Call them from an MCP client:
sipnab --mcp -N --quiet -I tests/fixtures/rtpengine-ng-hep.pcap
You can let sipnab ask a relay directly. That is the only way to reach
asked.
- Add
--rtpengine-control <ADDR>on a live capture. - sipnab does not ask while reading a file.
- The rtpengine guide covers that setup.
Example: trace the endpoints of one call
The examples send two things to sipnab over standard input:
- an MCP client’s opening handshake,
- one tool call.
They use jq to print the fields discussed above. Run them from the root of a
sipnab checkout.
This capture holds:
- rtpengine control messages mirrored over HEP to UDP port 9060,
- then the call’s audio,
- no SIP messages.
Ask where each endpoint of a call came from
Ask where the endpoints of the call km-670bd208@sipnab came from:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"demo","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"explain_attribution","arguments":{"call_id":"km-670bd208@sipnab"}}}' | sipnab --mcp -N --quiet -I tests/fixtures/rtpengine-ng-hep.pcap 2>/dev/null | jq -r 'select(.id==2) | .result.structuredContent | "unauthenticated_endpoints: \(.unauthenticated_endpoints)", (.endpoints | sort_by(.address, .port) | .[] | "\(.address):\(.port) \(.asserted_by) \(.input_origin) \(.delivery_trust)")'
unauthenticated_endpoints: 4
192.0.2.40:38156 media-relay wire port-gated-only
192.0.2.40:38664 media-relay wire port-gated-only
192.0.2.60:40001 media-relay wire port-gated-only
192.0.2.60:40002 media-relay wire port-gated-only
- A relay named all four endpoints.
- sipnab read them from the network (
wire). - So the only check was the port they arrived on.
Decode one relay control message
Decode the first frame of the same capture. decode_ng opens the file again.
So --mcp-file-root must name the directory that holds it:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"demo","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"decode_ng","arguments":{"frame_ref":"rtpengine-ng-hep.pcap#0"}}}' | sipnab --mcp -N --quiet -I tests/fixtures/rtpengine-ng-hep.pcap --mcp-file-root tests/fixtures 2>/dev/null | jq -c 'select(.id==2) | .result.structuredContent | {command, call_id, delivery, delivery_trust, on_believed_mirror_port}'
{"command":"offer","call_id":"km-670bd208@sipnab","delivery":"hep","delivery_trust":"port-gated-only","on_believed_mirror_port":true}
Find out why a stream has no call
This capture holds the same audio with no control messages and no SIP. Ask why each stream has no call:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"demo","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"reconcile_orphans","arguments":{}}}' | sipnab --mcp -N --quiet -I tests/fixtures/rtpengine-media-only.pcap 2>/dev/null | jq -c 'select(.id==2) | .result.structuredContent | {total_orphans, relay_was_consulted, reasons: (.orphans | map(.reason) | unique)}'
{"total_orphans":4,"relay_was_consulted":false,"reasons":["never-named"]}
Limits
- sipnab decides
delivery_trustfor a relay-delivered endpoint once per run.- It decides from the HEP listener’s settings, not per packet.
- A run may both read an interface (
-d) and listen for HEP with a secret. Then an assertion sniffed from the interface reports the listener’s value.
--hep-auth-mode hmacworks only between two sipnab instances.- A stock Homer or Kamailio peer uses the
plainform.
- A stock Homer or Kamailio peer uses the
--hep-allowrestricts the senders the HEP listener accepts.- It does not apply to a relay mirror sipnab reads from the network.
- No sipnab socket receives that traffic.
- sipnab asks a relay only with
--rtpengine-controlon a live source.- It sends only the
listandquerycommands. - It stops after 66 control transactions per run.
- On a run reading a file it does not ask.
- It sends only the
Terms used here
- SIP: the protocol phones and servers use to set up and end calls.
- Call-ID: the identifier SIP gives each call.
- SDP: the part of a SIP message that says where and how to send audio.
- RTP: the protocol that carries call audio.
- Media endpoint: an IP address and port that sends or receives a call’s audio.
- Media relay: a server, such as rtpengine, that carries audio between the parties.
- ng protocol: rtpengine’s control protocol, used to open, query and close relay ports.
- HEP: Homer Encapsulation Protocol, a format for copying SIP and control messages to a collector such as Homer or sipnab.
- HMAC: a keyed checksum that proves a message came from a holder of the shared secret and was not altered.
- MCP: Model Context Protocol, the interface an AI agent uses to call sipnab’s tools.
- Orphan: an audio stream that no call in the capture claims.
- Capture: a recording of network packets, here a pcap file.
Related guides
- How much a
media-relayassertion is worth and Telling a relay’s answer from a party’s in the rtpengine guide. - Name a call that was already up when you started and Read a run that attributed nothing.
- Let an agent ask the relay.
- The tool reference entries for
explain_attribution,decode_ng,reconcile_orphansandserver_capabilities.