freeswitch-sofia-trace-parser
Rust library and CLI for parsing FreeSWITCH mod_sofia SIP trace dump files.
[]
= "0"
Overview
FreeSWITCH logs SIP traffic to dump files at
/var/log/freeswitch/sip_traces/{profile}/{profile}.dump (rotated as .dump.1.xz, etc.).
This library provides a streaming, multi-level parser:
- Level 0 — Raw dump: FreeSWITCH frame headers (
recv/sent N bytes from/to <transport>/<addr> at <ts>:) and payloads delimited by\x0B\nboundaries - Level 1 — Frames: Split raw bytes on
\x0B\nboundaries, parse frame headers - Level 2 — Messages: Reassemble TCP segments, split aggregated messages by Content-Length
- Level 3 — Parsed SIP: Extract method/status, headers, body, and multipart MIME parts
At a Glance
Point the CLI at a dump file and filter — full reference under CLI Tool:
Library Usage
Raw messages (Level 2)
use File;
use ;
let file = open?;
for result in new
Parsed SIP messages (Level 3)
use File;
use ParsedMessageIterator;
let file = open?;
for result in new
socket_addr() gives the remote address typed, family and port preserved, on
Frame, SipMessage and ParsedSipMessage alike.
Headers live in a Headers newtype over (name, value) pairs in wire order —
it derefs to the slice for iteration, and header_value(name) on
ParsedSipMessage, MimePart and SipFragment is the case-insensitive
lookup. Typed accessors (call_id(), content_type(), …) resolve SIP compact
forms; header_value() does not.
Multipart body splitting (SDP + EIDO/PIDF)
body_parts() returns None for a body that does not split. Use
body_as_parts() to handle every body the same way: it yields the multipart
children, or a single part for a body that has none. media_type() gives the
type with parameters stripped and lowercased, ready to match on.
use File;
use ParsedMessageIterator;
let file = open?;
for result in new
A part split from a multipart body carries the headers the sender wrote between
the boundary and the blank line, whatever they are. The single part for a body
that does not split has no such block on the wire, so body_as_parts()
fabricates one: the body verbatim, and the message's Content-* headers copied
down — compact c/e expanded to their full names, Content-Length
left off because it counts the message body and stops describing the part once
you rewrite it. So part.content_transfer_encoding() answers the same on both
kinds, and a loop that dispatches on media_type() cannot mistake an encoded
body for the text its type claims.
A body typed multipart/* that carries no boundary parameter, or one the body
never uses, is a body that does not split: it comes back as that single part,
still typed multipart/*, rather than as nothing at all.
Splitting descends one level. A nested multipart/* comes back as a part with
its type intact; call part.body_parts() on it to go deeper — the depth is
yours to choose.
Content-type-aware body access
ParsedSipMessage provides three methods for body access:
body_data()— raw bytes as UTF-8 (no processing, exact wire representation)body_text()— for JSON content types, unescapes RFC 8259 string escape sequences (\r\n→ CRLF,\t→ tab,\"→",\uXXXX→ Unicode including surrogate pairs); passthrough for all other content typesjson_field(key)— parses body as JSON, returns unescaped string value for a top-level key; returnsNoneif content type is not JSON, body is invalid, key is missing, or value is not a string
JSON-aware behavior activates for application/json and any application/*+json subtype (e.g., application/emergencyCallData.AbandonedCall+json). Matching is case-insensitive; media type parameters like charset=utf-8 are ignored.
use File;
use ParsedMessageIterator;
let file = open?;
for result in new
Pcap export (feature: pcap)
[]
= { = "0", = ["pcap"] }
use File;
use ;
let dump = open?;
let pcap = create?;
let mut w = new?;
for result in new
PcapWriter synthesizes a libpcap-classic file from parsed frames or
reassembled messages. The dump only carries the remote endpoint, so the
caller supplies a local endpoint via PcapConfig (defaults: RFC 5737
192.0.2.1:5060 and RFC 3849 [2001:db8::1]:5060 documentation
addresses). TLS/WSS payloads are decrypted SIP in the dump and are
emitted as plain TCP on the original ports.
PcapLayer::Transport (default) emits SLL + IP + UDP/TCP + SIP for
direct Wireshark dissection. PcapLayer::Network emits LINKTYPE_RAW + IP
- SIP-as-IP-payload (proto 253) for tooling that consumes raw IP.
Streaming from pipes
use ;
use MessageIterator;
let child = new
.arg
.stdout
.spawn?;
for msg in new
Concatenating multiple files
use File;
use FrameIterator;
let f1 = open?;
let f2 = open?;
let chain = chain;
for frame in new
Edge Cases Handled
- Truncated first frame (rotated files,
xzgrepextracts, pipe mid-stream) \x0Bin XML/binary content (not a boundary unless followed by valid header)- Multiple SIP messages aggregated in one TCP read
- TCP segment reassembly (consecutive same-direction same-address frames)
- File concatenation (
cat dump.2 dump.1 | parser) - Non-UTF-8 content (works on
&[u8]) - EOF without trailing
\x0B\n - Multipart MIME bodies (SDP + PIDF/EIDO splitting for NG-911)
- Nested multipart parts (split on request, one level per call)
message/sipfragbodies without a start line or trailing CRLF (RFC 3420)- JSON body unescaping for
application/jsonandapplication/*+jsoncontent types - TLS keep-alive whitespace (RFC 5626 CRLF probes, sofia-sip bare
\n) - Logrotate replay detection (partial frame re-written at start of new file)
- Incomplete frames at EOF (byte_count exceeds available content)
- Byte-level input coverage tracking (
ParseStatswith unparsed region reporting)
Validated Against Production Data
Tested against 83 production dump files (~12GB) from FreeSWITCH NG-911 infrastructure:
| Profile | Files | Frames | Messages | Multi-frame | byte_count mismatches |
|---|---|---|---|---|---|
| TCP IPv4 | 14 | 6.2M | 6.0M | 21,492 (max 7) | 0 |
| UDP IPv4 | 13 | 4.8M | 4.8M (1:1) | 0 | 0 |
| TLS IPv6 | 18 | 5.9M | 5.9M | 108 | 0 |
| TLS IPv4 | 5 | 660K | 660K | 70 | 0 |
| TCP IPv6 | 3 | 327K | 327K | - | 0 |
| UDP IPv6 | 3 | 301K | 301K (1:1) | 0 | 0 |
| Internal TCP v4 | 13 | 723K | - | - | 0 |
| Internal TCP v6 | 13 | 836K | - | - | 0 |
- Zero byte_count mismatches across all frames
- 99.99%+ of reassembled messages start with a valid SIP request/response line
- Level 3 SIP parsing: 100% on all tested profiles (TCP, UDP, TLS)
- Multipart body splitting: 1,223 multipart messages, 2,446 parts (SDP + PIDF), 0 failures
- File concatenation (
cat dump.29 dump.28 |): 965,515 frames, zero mismatches
Input coverage tracking
Every sample file is verified for byte-level parse coverage. Each unparsed region is
classified by SkipReason:
PartialFirstFrame— truncated frame at start of file (logrotate, pipe, grep extract), capped at 65535 bytesOversizedFrame— skipped region exceeds 65535 bytes (corrupt or non-dump content)ReplayedFrame— logrotate wrote a partial frame tail at the start of the new fileMidStreamSkip— unrecoverable bytes skipped mid-stream (e.g., TCP reassembly edge case)IncompleteFrame— frame at EOF with fewer bytes than declared in the headerInvalidHeader— data starts withrecv/sentbut header fails to parse
ParseStats exposes bytes_read, bytes_skipped, and detailed UnparsedRegion records
with offset, length, and skip reason for each region.
Performance and Memory
184–500 MB/s per core parsing every message, depending on CPU and message mix,
in constant memory for inputs of any size — multi-day dump chains (50GB+)
included. Measured figures, the constant-memory design, and the protocol for
benchmarking a change to the parser are in
docs/performance.md.
A consumer keeping a small fraction of a trace should reject before Level 3
rather than filter after it. SipMessage::method reads the method
from the reassembled bytes with no parse, and answers None rather than guess
where the bytes do not settle it, so filtering on it drops only what it has
classified. On an OPTIONS-dominated trace this cuts a filtered run roughly in
half; the CLI's --method/--exclude use it.
CLI Tool
OPTIONS keepalives are excluded by default (use --all-methods to include them).
# One-line summary (OPTIONS excluded by default)
# Pipe from xzcat
|
# Bulk scanning: skipping the CRC64 check cuts decompression by up to a third on
# ARM64 (see docs/performance.md), at the cost of detecting no corruption
|
# Filter by method — shows INVITE requests and their 100/180/200 responses
# Filter by Call-ID regex
# Header regex — all sent INVITEs from a specific extension
# Grep for a string anywhere in the SIP message (headers + body)
# Body grep — match only in message body (SDP, EIDO XML, etc.)
# Extract SDP body from a specific call's INVITEs
# Full SIP message output
# Statistics: method and status code distribution
# Multiple files (concatenated in order)
# Raw frames (level 1) or reassembled messages (level 2)
# Export matched messages to pcap (stdout)
Dialog mode
Use -D to expand matched messages to full Call-ID conversations. When any message
matches, all messages sharing its Call-ID are output. Single pass — works with stdin/pipes.
# Find dialogs containing INVITEs, show full call flow
# Find all dialogs related to an incident ID (across profiles)
# Find dialogs by phone number anywhere in message
# Find dialogs by body content (EIDO XML, PIDF)
# Works with stdin/pipes
|
Terminated dialogs (BYE + 200 OK) that never matched are pruned during processing to limit memory usage. Unmatched Call-IDs with only OPTIONS traffic are never buffered.
Filter options
| Flag | Description |
|---|---|
-m, --method <VERB> |
Include method (request + responses via CSeq), repeatable |
-x, --exclude <VERB> |
Exclude method (request + responses), repeatable |
-c, --call-id <REGEX> |
Match Call-ID by regex |
-d, --direction <DIR> |
Filter by direction (recv/sent) |
-a, --address <REGEX> |
Match address by regex |
-H, --header <NAME=REGEX> |
Match header value by regex, repeatable |
-g, --grep <REGEX> |
Match regex against full reconstructed SIP message |
-b, --body-grep <REGEX> |
Match regex against message body only |
-D, --dialog |
Expand matches to full Call-ID conversations |
--all-methods |
Include OPTIONS (excluded by default) |
Output modes
| Flag | Description |
|---|---|
| (default) | One-line summary per message |
--full |
Full SIP message with metadata header |
--headers |
Headers only, no body |
--body |
Body only (for SDP/PIDF extraction) |
--raw |
Raw reassembled bytes (level 2) |
--frames |
Raw frames (level 1) |
--stats |
Method and status code distribution + input coverage |
--pcap-export |
Emit libpcap-classic to stdout (use with --pcap-layer 3|4) |
--pcap-layer N |
3 = Level-1 frames as IP+SIP (proto 253); 4 = Level-2 messages as IP+UDP/TCP+SIP (default) |
--unparsed |
Report unparsed input regions to stderr (combinable with any mode) |
FreeSWITCH Setup
See docs/freeswitch-setup.md for the required patches, SIP profile configuration, and log rotation setup.
Building
Testing
# Unit tests (no external files needed)
# Integration tests (requires production samples in samples/)
Integration tests validate at each parser level:
- Level 1: Frame parsing, transport detection, address format, byte_count accuracy, and parse stats coverage (max 1 partial first frame per file, zero invalid header skips)
- Level 2: TCP reassembly, UDP pass-through, interleaved multi-address reassembly, frame accounting, and parse stats delegation
- Level 3: SIP request/response parsing, Call-ID/CSeq extraction, multipart MIME splitting, method distribution, and parse stats delegation
The all_samples_consistent_frame_counts test iterates all sample files per profile and asserts parse stats on each individually.
See CLAUDE.md for test architecture details.
Related crates
FreeSWITCH and SIP crates by the same author, usable independently:
freeswitch-log-parser— parses FreeSWITCH's own logs and ships thefslogCLI. Complements this crate: it gives you the channel-level view, this one gives you the SIP messages behind it.freeswitch-types— typed FreeSWITCH enums (call direction, channel/call state, hangup causes).freeswitch-esl-tokio— async ESL client, for reading events off a live switch.sip-uri— RFC 3261 SIP/SIPS, RFC 3966tel:, RFC 8141 URN parser. Zero dependencies.sip-header— SIP header field parsers (name-addr, Call-Info, History-Info, Geolocation, conference-info).
License
LGPL-2.1-or-later