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.
Memory Profile
The parser is designed for constant-memory streaming of arbitrarily large inputs,
including multi-day dump file chains (50GB+). Memory behavior was validated using
jemalloc heap profiling (_RJEM_MALLOC_CONF=prof:true) and gdb inspection of live
data structures during processing of 50+ chained dump files.
Parser internals at runtime (gdb-verified):
FrameIterator::buf— 64KB capacity, ~200 bytes used (single read buffer, never grows)MessageIterator::buffers— 0 entries (TCP reassembly buffers evicted after message extraction)MessageIterator::ready— 0 entries, capacity 10 (drained each iteration)
Design choices that maintain constant memory:
SkipTrackingdefaults toCountOnly— no allocation for unparsed region tracking unless opted in- TCP connection buffers are eagerly removed after complete message extraction
- Stale buffers (>2h inactive) are evicted via time-based sweep to handle TLS ephemeral port accumulation
flush_all()clears the entire buffer map at EOF
Consumers processing many files should open files lazily (one at a time) rather
than using Read::chain() upfront, which keeps all file handles and decompression
state alive for the entire run. With 50+ XZ-compressed dump files, eager chaining
consumed 172MB of LZMA decoder state alone.
CLI Tool
OPTIONS keepalives are excluded by default (use --all-methods to include them).
# One-line summary (OPTIONS excluded by default)
# Pipe from xzcat
|
# 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