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.

Which method can you use?

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"]
Loading

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.


1. The endpoint writes a key log

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.log

The 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".

2. Check what you have before going further

sudo sipnab --uprobe-list

Installs 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.

3. No keys at all: read the plaintext in the process

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-tls

sipnab 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_ex

Read 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.

4. Plaintext and the peer address

sudo sipnab -N --uprobe-tls --uprobe-backend bpf --portrange 0-65535

Adds 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.

5. Lift keys from a daemon you cannot restart

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-watch

The 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-window moves 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_RANDOM line 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.

6. The old RSA case

sipnab -I capture.pcap --tls-key server.key

Works 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.

Media as well as signaling

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-keylog at 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.

When the keys arrive after the call starts

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.

What does not work, and why

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.

Still stuck?

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.

⚠️ **GitHub.com Fallback** ⚠️