Capture SIP over TLS - NormB/sipnab GitHub Wiki
You have SIP on port 5061, sipnab shows you nothing useful, and you want to see the signaling. This page picks the method for you.
For many production deployments this is not an advanced topic β it is the only topic. A hardened SIP server often does not listen on 5060 at all, so every call is TLS and a capture that reads only plaintext reads nothing. If that is your situation, start at the table below rather than with the plaintext quick-start elsewhere in these docs.
The short version. The wire alone never yields encrypted traffic β modern TLS uses forward secrecy, so even the server's private key does not open a captured session. Something on a machine you control has to hand over the session keys, or hand over the plaintext. Every method below is one of those two, and they differ only in what access each demands.
Answer these in order. Each branch ends at one numbered method below, and the question that separates two branches is always an access question rather than a protocol one.
flowchart TD
S["SIP on 5061, and sipnab decodes nothing"] --> Q1{"Can you set SSLKEYLOGFILE<br/>on an endpoint and restart it?"}
Q1 -->|yes| M1["Method 1: --keylog<br/>full decryption, live or from a pcap"]
Q1 -->|no| Q2{"Can you get root on the SIP host?"}
Q2 -->|no| Q6{"Old TLS 1.2 server on RSA key exchange,<br/>and you hold its private key?"}
Q6 -->|yes| M6["Method 6: --tls-key<br/>non-PFS handshakes only"]
Q6 -->|no| X["Nothing opens it.<br/>Read What does not work, and why"]
Q2 -->|yes| L["Method 2: sudo sipnab --uprobe-list<br/>names the TLS libraries this host runs"]
L --> Q3{"Do you want keys you can keep,<br/>or plaintext right now?"}
Q3 -->|keys| M5["Method 5: eCapture, then --keylog<br/>daemon untouched, and the pcap stays readable"]
Q3 -->|plaintext| Q4{"BTF kernel, and a build<br/>carrying the bpf feature?"}
Q4 -->|yes| M4["Method 4: --uprobe-tls --uprobe-backend bpf<br/>plaintext with the real 5-tuple"]
Q4 -->|no| M3["Method 3: --uprobe-tls<br/>plaintext, no peer addresses"]
The table says the same thing in rows. Read down the first column and stop at the first row you can satisfy.
| If you can⦠| Use | Needs | Gets you |
|---|---|---|---|
Set SSLKEYLOGFILE on the endpoint (a soft client, test UA, or a daemon you can restart) |
--keylog |
Nothing special | Full decryption, live or from a pcap |
| Get root on the SIP host, but not restart the daemon | --uprobe-tls |
Linux, root | Plaintext, no peer addresses |
| Get root and the kernel has BTF | --uprobe-tls --uprobe-backend bpf |
Linux, root, BTF, a bpf build |
Plaintext with the real 5βtuple |
| Run a helper on the SIP host but prefer keys to plaintext | eCapture β --keylog |
Linux, root | Full decryption, daemon untouched |
| Only reach an old TLS 1.2 server using RSA key exchange | --tls-key |
The server's private key | Decryption, non-PFS handshakes only |
| None of the above | β | β | Nothing. See what does not work |
Most people want row 1 or row 3. Row 1 if you are testing and control an endpoint. Row 3 if you are on a production box and cannot restart anything β that is the case sipnab exists for, and the one people assume is impossible.
The ordinary case, and the cheapest. Any TLS library built on OpenSSL, NSS or
GnuTLS writes session keys to the file named by SSLKEYLOGFILE.
# Run all of these, in order.
export SSLKEYLOGFILE=/tmp/sip-keys.log
# start your softphone or daemon from this shell, then:
sudo sipnab -d eth0 --keylog /tmp/sip-keys.log --keylog-watch--keylog-watch re-reads the file as it grows, so keys minted after sipnab
starts still decrypt. Without it you get only the sessions whose keys were
already written.
Same flag reads a capture you already recorded:
sipnab -I tls-capture.pcap --keylog /tmp/sip-keys.logThe full recipes, including exporting a decrypted pcap for Wireshark and keeping keys off disk entirely, are cookbook recipes 7a to 7f, under recipe 7, "Decrypt SIP/TLS via SSLKEYLOGFILE".
sudo sipnab --uprobe-listInstalls nothing. It prints the TLS libraries running on this host and which processes use them β which tells you whether rows 3 and 4 are even available before you spend time on them.
This is the method the long-standing upstream feature request asks for β TLS visibility with no recompile and no certificate β and it is what sipnab does here.
sudo sipnab -N --uprobe-tlssipnab attaches a uprobe to the TLS library's write function and reads the bytes before the library encrypts them. It recovers no key, decrypts nothing, and touches no other machine.
Dialogs carry no addresses and port 0, labeled uprobe:<process>/<pid>.
A uprobe sees the bytes an application handed its TLS library and nothing
about the socket underneath, so sipnab names the process rather than inventing
a peer. If you need the addresses, use method 4.
If it attaches and reports zero messages, the symbol is almost certainly
wrong β OpenSSL 3 applications increasingly call SSL_write_ex rather than
SSL_write:
# Run all of these, in order.
nm -D --undefined-only /usr/sbin/opensips | grep -i ssl_write
sudo sipnab -N --uprobe-tls --uprobe-symbol SSL_write_exRead the security implications before using this on a production host. It reads process memory, so anyone who can run it can read every SIP session on that machine, credentials included.
sudo sipnab -N --uprobe-tls --uprobe-backend bpf --portrange 0-65535Adds a kernel probe on tcp_sendmsg and pairs it with the write by thread, so
dialogs carry the real 5-tuple. Needs a kernel with CONFIG_DEBUG_INFO_BTF=y
and a sipnab that actually holds the kernel programs β --features bpf compiles
the loader, and the programs themselves need a nightly toolchain and
bpf-linker on the build host. A build missing either still succeeds and
produces a binary that lists bpf and refuses at attach time. The uprobe
walkthrough linked at the foot of this page carries the message and the
build-time switch that turns that gap into a build failure.
sipnab refuses rather than falling back in silence, because a silent downgrade would leave you with no peers and no reason given.
Widen --portrange. The port a uprobe reports is whatever the socket used,
which is usually ephemeral rather than 5061.
If you would rather have keys than plaintext β keys decrypt a pcap you keep, and plaintext does not β eCapture reads them out of a running process and sipnab consumes them unchanged:
# Run all of these, in order.
# eCapture picks the TLS library to instrument by looking at curl. Your SIP
# daemon may well map a different one, so name the daemon's explicitly β this
# is the single most common reason a keylog stays empty.
LIBSSL=$(sudo awk '/libssl/ {print $6; exit}' /proc/"$(pgrep -o opensips)"/maps)
sudo ecapture tls -m keylog --libssl="$LIBSSL" --keylogfile=/tmp/keys.log &
sudo sipnab -d eth0 --keylog /tmp/keys.log --keylog-watchThe uprobe attaches to the library, not to one process, so a forking daemon
needs no --pid β it covers every worker that maps that path, and --pid
would restrict you to one of them. Check the path it chose in eCapture's own
startup line (openssl_path=) before trusting the run.
Start the capture before the connections you want to read. This is the single biggest trap on long-lived trunks, and it bites differently in each TLS version:
-
TLS 1.3 (RFC 8446) numbers each record with a counter both endpoints keep privately.
Nothing on the wire carries it, so sipnab searches for it β about a million
records, roughly a day of a trunk ticking over at ten records a second. The
search costs one AEAD tag check per candidate and runs once per direction per
session, so it is a one-off of about a second, not a per-record cost. Past
that, the keys are right and the records still do not open.
--tls-lockon-windowmoves that ceiling. Raising it costs nothing on a connection captured from its handshake, because the search widens only as records fail to open, so raise it for a carrier trunk held open for days. A separate per-run trial budget β four full windows β stops one session whose keys belong to some other connection from spending a window on every record it gets offered. -
TLS 1.2 (RFC 5246) is worse: a
CLIENT_RANDOMline gives the master secret, and the server random and cipher suite that expand it into record keys are in the ServerHello. Miss the handshake and there is no way to use the secret at all, now or later.
sipnab says which of these happened, with counts. The fix is at capture time either way: bounce the connection, or the far end, while capturing, so the capture catches the stream from its handshake and its first record.
Cookbook recipe 7e, "Decrypt traffic from a daemon you cannot restart" has the full sequence, and recipe 7f, "Decrypt without writing the keys to disk" shows feeding keys through a pipe so they never reach disk.
sipnab -I capture.pcap --tls-key server.keyWorks only for TLS 1.2 handshakes that used RSA key exchange. Any ECDHE/DHE handshake β which is every modern configuration, and mandatory in TLS 1.3 β has forward secrecy, and the private key does not decrypt a recorded session. If this produces nothing, your traffic is almost certainly PFS and you need method 1, 3, 4 or 5.
SRTP keys arrive two ways, and sipnab reads both:
- SDES β keys travel in the SDP, so decrypting the signaling decrypts the media with it. Nothing extra to do.
-
DTLS-SRTP β a DTLS handshake carries the keys. Point
--dtls-keylogat an NSS-format key log and sipnab runs the RFC 5764 exporter over it to reach the SRTP keys, cookbook recipe 7d, "Decrypt SRTP from a DTLS keylog".
--dtls-keylog is a separate flag from --keylog because the two name
different handshakes. If you already hold SRTP master keys rather than a
handshake, --srtp-keys takes them directly.
Methods 3, 4 and 5 share one race. You attach the key source to a daemon that is already running, a call arrives, and the first records reach sipnab before the first key does. On a SIP session the first record is the INVITE, so the symptom is not "a few records are unreadable" β it is a call with no offer in it, which sipnab then reports as a media mismatch or a NAT problem, because from the dialog's point of view that is exactly what it looks like.
sipnab holds application data it cannot open yet and retries it as soon as a
key for that session turns up. No flag switches this on: it is what --keylog
and --keylog-watch already do. A run that recovered records says so:
TLS late decrypt: recovered 3 record(s) that arrived before their keys
The hold has limits, because a peer whose keys never arrive would otherwise grow it without end:
| Bound | Value |
|---|---|
| Total held, across every session | 4 MiB |
| Records per direction | 16 |
| Directions | 4096 |
| Age | 5 seconds |
Those bounds are the reason to start the key source before the capture wherever you can. Recovery closes a gap measured in packets, not one measured in minutes.
The run also says what the hold could NOT recover, because "we never had the keys for those records" and "we had them and had already discarded the ciphertext" are different problems with different fixes:
sipnab discarded 3 TLS record(s) held from before a key arrived: the
late-decrypt hold is bounded (4 MiB total, 16 record(s) per direction, 5s)
That one argues for starting the key source earlier, or for a bigger bound. A line reporting records still waiting when the run ended argues for neither -- the key source never produced those secrets, and capturing sooner would not have helped.
Stated plainly, because time spent here is time people lose:
- A packet capture alone. No amount of sipnab flags decrypts a pcap with no keys. If the handshake had forward secrecy, the information required is not in the capture and never was.
- The server's private key, on modern TLS. See method 6.
- A mirror port or tap, by itself. It gives you the same ciphertext as a local capture. You still need keys or plaintext from an endpoint.
- Any of methods 3β5 against a machine you do not control. They read process memory on the host they run on. That is the whole boundary.
- Attaching to a long-lived connection and expecting the back catalog. Keys extracted from a running process decrypt records captured from that point on, provided the capture catches the stream early enough. sipnab does recover records that reached it in the moments before the key did, described above, but that closes a gap of packets rather than one of history: traffic that went past before the capture started is not in the capture, and nothing recovers it.
| Symptom | Likely cause |
|---|---|
--uprobe-list finds no TLS library |
It reads /proc/<pid>/maps, and an unprivileged run sees only its own processes. Re-run with sudo
|
--uprobe-list marks a library (UNREACHABLE)
|
The path lives in another mount namespace, almost always a container. Run as root |
| Attaches, reports 0 messages | Wrong symbol β try --uprobe-symbol SSL_write_ex (see method 3) |
needs this kernel's BTF |
No CONFIG_DEBUG_INFO_BTF; use --uprobe-backend tracefs
|
no kernel programs |
Binary carries the bpf feature and not the programs β the build host had no bpf-linker. Use tracefs, or rebuild somewhere that has it |
| Keylog present, still encrypted | Keys minted after start β add --keylog-watch
|
| Call decodes, but its INVITE is missing and sipnab reports a media or NAT problem | The keys arrived after the INVITE did. sipnab retries records held from before the key, within the bounds above; start the key source before the capture |
| Keylog stays empty | eCapture attached to a TLS library the daemon never calls β pass --libssl with the path from /proc/<daemon-pid>/maps
|
| Keys load, sessions listed, nothing decrypts | The capture joined TLS 1.3 connections already running, past the record numbers sipnab searches. Restart the connection while capturing, or raise --tls-lockon-window
|
| Keys load, no sessions listed | TLS 1.2 without the handshake β the ServerHello never got captured, and the master secret alone cannot make record keys. Restart the connection while capturing |
| TLS 1.2, right keys, still nothing | A CBC suite. sipnab refuses to emit record plaintext it cannot MAC-verify, so a forged capture cannot inject "decrypted" SIP. Configure an AES-GCM suite |
Addresses show 0.0.0.0:0
|
Expected on the tracefs backend; use method 4 for peers |
More in Troubleshooting and the uprobe walkthrough.