-
Notifications
You must be signed in to change notification settings - Fork 2
Chan Sofia
chan_sofia is the Sofia-SIP based SIP channel driver for GABPBX. It is the
modern SIP implementation in the project and is designed to replace chan_sip
while preserving the public compatibility surface that existing systems depend
on.
The driver uses the Sofia-SIP NUA API for SIP stack handling and integrates that stack with the GABPBX channel, RTP, realtime, AMI, CLI, dialplan function, SRTP and bridging layers.
chan_sofia is a drop-in replacement, but it is not limited to what chan_sip
did — a real SIP stack underneath let it pick up modern SIP features that
chan_sip never had. Highlights (each is documented in its own section below):
- Registration & routing: SIP Outbound (RFC 5626), Path (RFC 3327), Service-Route (RFC 3608) and GRUU — the registration extensions that make a PBX work cleanly behind edge proxies and SBCs.
- Presence & voicemail: outbound PUBLISH (RFC 3903) to a central presence server, a generic outbound SUBSCRIBE for any event package, an outbound MWI SUBSCRIBE watcher, and both solicited and unsolicited MWI.
- Call signalling: reliable provisionals / PRACK (RFC 3262), in-dialog UPDATE (RFC 3311), Q.850 Reason on BYE/CANCEL (RFC 3326), REFER transfer and out-of-dialog SIP MESSAGE.
- Security: mutual TLS (client-cert verification), TLS hardening, and digest auth with SHA-256 alongside MD5.
-
Diagnostics: per-call SIP history with a verbose call-analysis pass
(outcome, failure reason, timeline, negotiated codecs) and a capture filter,
plus
sip notify,sip qualify peer,sip show channelandsip show channelstats.
These are not on the chan_sip feature list — they are the reason to move.
The primary goal is simple: make chan_sofia a drop-in chan_sip
replacement without copying avoidable historical bugs.
Operationally that means:
- Existing dialplans keep using
SIP. - Existing peers can stay in the
sippeersrealtime family. - Existing
sip show ...CLI habits continue to work. - Existing SIP AMI action names continue to work.
- Existing dialplan functions keep their
SIP...names. - New behavior should use Sofia-SIP where the library already solves the SIP protocol problem better than local hand-written parsing.
All figures below are traceable to the source of each driver. They are intended as a deployment-planning baseline, not a benchmark.
chan_sofia spawns exactly four module-level threads at load time, in
load_module at channels/chan_sofia.c:17370-17457:
| Thread | Role |
|---|---|
sofia_thread (sofia_thread_func calling su_root_run) |
NUA event loop. All inbound and outbound SIP traffic — INVITE, REGISTER, OPTIONS, NOTIFY, PRACK, BYE, CANCEL — is dispatched here. Sofia-SIP's tport_* layer multiplexes UDP, TCP, TLS, WS and WSS sockets on the same loop. |
sofia_sched (ast_sched_thread_create) |
Managed scheduler thread for timed callbacks: T.38 5-second reINVITE abort, REFER transferer-leg BYE defer (32 s safety net), qualify retry. |
sofia_reg_thread (sofia_reg_thread_func) |
Outbound REGISTER refresh when chan_sofia acts as a UAC against upstream providers. |
sofia_qualify_tid (sofia_qualify_thread) |
Periodic OPTIONS keepalive scheduling. |
There are no per-peer, per-call, per-contact or per-connection thread
creations anywhere else in chan_sofia. grep -nE 'ast_pthread_create|pthread_create'
against the source returns only the four sites above.
chan_sip for comparison creates:
- 1 monitor thread (
do_monitor,channels/chan_sip.c:27246) — analogous tosofia_thread. - A detached worker thread per persistent SIP TCP/TLS session
(
sip_tcp_worker_fnatchannels/chan_sip.c:26924). This thread stays alive for the lifetime of the TLS connection. A deployment with N registered TLS endpoints carries N additional threads here. - A detached short-lived thread per Park (
sip_park_threadatchannels/chan_sip.c:22777) and per Pickup (sip_pickup_threadatchannels/chan_sip.c:22818). These do not accumulate.
Practical consequence: scaling to several thousand TLS endpoints multiplies
the chan_sip thread count by that endpoint count plus a constant. On
chan_sofia the thread count is constant. Each Linux thread carries an 8 KB
minimum pthread stack plus kernel scheduler state, so the per-thread cost
adds up at large endpoint counts.
struct sofia_peer (channels/chan_sofia.c:1267-1735) carries:
-
AST_DECLARE_STRING_FIELDS(...)with about two dozen string fields (name,secret,md5secret,context,host,defaultuser,fromuser,fromdomain,regexten,callbackextension,subscribecontext,accountcode,disallowed_methods,callerid,cid_name,cid_num,cid_tag,nonce,outboundproxy,srtpcipher,mohinterpret,mohsuggest,language,parkinglot,lockuseragent_prefixes). Pooled storage: empty fields point at a shared empty string; populated fields uselength + 1bytes from the per-struct pool. Typical pool footprint: 256-1024 bytes depending on the peer's configuration. - A few dozen
intfields covering NAT mode, DTMF mode, codec preferences, encryption, call counters, session timer parameters, transfer policy, MWI, qualify and RTP timeouts. - Three
struct ast_sockaddrslots (addr,src_addr,defaddr), 136 bytes each. - Three
struct timevalslots for qualify and response timing. - One
ast_mutex_t. - Pointers to optional structures:
struct ast_ha *ha,struct ast_ha *contactha,struct ast_ha *directmediaha,struct ast_dnsmgr_entry *dnsmgr,struct ast_variable *chanvars,struct ao2_container *contacts,nua_handle_t *nh,nua_handle_t *qualify_nh,nua_handle_t *mwi_subscription_handle.
Headline estimate for a configured peer that is not yet registered and has no auxiliary lists: about 2-3 KB.
Each registered contact is a separate struct sofia_contact
(channels/chan_sofia.c:1112-1121):
struct sofia_contact {
char contact_uri[256];
char host[64];
int port;
char transport[8];
char user_agent[64];
time_t expires;
struct ast_sockaddr src_addr;
int active_calls;
};About 640 bytes per contact including the ao2 ref-count header and hash
slot in peer->contacts. Six contacts under a peer therefore add roughly
3.8 KB on top of the bare peer cost.
Optional per-peer structures add memory only when used:
- Each
permitordenyACL rule inha,contacthaordirectmediahais roughly 24 bytes. - A
dnsmgrentry exists only when the peer'shost=is a hostname rather than an IP literal. - Each
setvar=orheader=entry on a peer adds anast_variablenode (about 40 bytes plus the name and value strings).
Sofia-SIP allocates nua_handle_t storage in its own su_home. A registered
peer typically owns one main handle. A peer with qualify=yes carries a
second handle for OPTIONS. A peer with mailboxes carries a handle for MWI
SUBSCRIBE. Each handle is on the order of 1-2 KB inside Sofia-SIP.
chan_sip's struct sip_peer (channels/sip/include/sip.h:1203) defines
around 60 typed fields plus its own AST_DECLARE_STRING_FIELDS block and
embedded buffers. Bare cost is similar in order of magnitude (a few KB),
with the main difference being that chan_sip inlines a single contact-
oriented address into sip_peer while chan_sofia separates the contact
into its own refcounted object.
struct sofia_pvt (channels/chan_sofia.c:1743-1869) carries about 40
typed fields, an AST_DECLARE_STRING_FIELDS block with around 20 strings,
several embedded ast_sockaddr slots, fork bookkeeping, scheduler IDs, and
pointers to the RTP and SRTP contexts. Direct cost: about 3-5 KB.
Each active call also carries:
- An
ast_channelfrom the GABPBX core (about 4-6 KB). - An
ast_rtp_instancefor audio (about 4-8 KB). - A second
ast_rtp_instancewhen video is in the SDP. - One
nua_handle_t(about 1-2 KB inside Sofia-SIP). - An
srtpand optionalvsrtpcontext whenencryption=yes(about 2 KB each).
Total per active audio call: roughly 15-25 KB.
When chan_sofia forks an outbound INVITE to multiple live contacts (the
typical desk-phone-plus-softphone case), the master sofia_pvt is created
once and child sofia_pvt objects are spawned per contact at
channels/chan_sofia.c:6151-6353. Each child carries its own nua_handle_t
and SDP/SRTP state but does not own the ast_channel; only the master
participates in the peer->inUse counter. A ring to six contacts therefore
costs roughly six times the per-child overhead (about 35 KB transient
extra) and counts as one ringing call against call-limit.
Both drivers serialise SIP request dispatch through a single thread
(do_monitor in chan_sip, sofia_thread in chan_sofia). That means SIP
throughput per process is CPU-bound on one core in both cases. Sofia-SIP's
implementation is generally faster than the equivalent hand-rolled paths in
chan_sip and avoids the per-connection thread context-switch cost on
TCP/TLS, but the architectural ceiling is the same: one core per SIP loop.
Deployments that need to scale SIP throughput beyond what one CPU core provides typically front the PBX with an SBC and shard endpoints across PBX instances. That model is unchanged by the choice of SIP driver.
The single-thread ceiling above is exactly why the per-event cost on that thread
matters. chan_sofia keeps that cost low the way Kamailio keeps its location and
dialog stores fast — O(1) data structures — and is beginning to take blocking work
off the thread the way Kamailio (asynchronous usrloc/workers) and FreeSWITCH
(thread pools) do.
O(1) peer and dialog lookups. Peers and active dialogs are held in real hash tables with proper hash functions, so a deployment with thousands of registered peers and a high call rate never degrades the per-event cost to a linear scan. The bucket caps are sized for a load factor well below 1 into tens of thousands of peers and high concurrent-dialog/presence volume (all prime — primes give an even bucket distribution and drop the modulo's dependence on the hash's low bits):
#define MAX_PEER_BUCKETS 65521 /* prime < 2^16: ~50k peers at load factor < 1 */
#define MAX_PEER_IPPORT_BUCKETS 16381 /* prime: by-source-IP trunk index (static-host trunks only) */
#define MAX_DIALOG_BUCKETS 32749 /* prime: concurrent-dialog headroom */
/* presence 65521, publications 32749, blacklist 4093 — all prime */The bucket array is allocated once and lives for the process; because it is
calloc'd, an empty large table costs almost no resident memory (the unused
buckets stay on lazy zero pages) and fills in only as it is used.
Fast string hashing — XXH3-64. Every one of those tables (and every other
ao2 container in the core: device state, channels, formats, …) is keyed through
ast_str_hash. GabPBX hashes it with XXH3 (64-bit, xxHash 0.8.3) instead of the
classic byte-at-a-time multiply hash:
| classic byte-wise hash | XXH3-64 (this build) | |
|---|---|---|
| processing | one byte per step | wide 8-byte words + strong final avalanche |
| short-key speed | baseline | markedly faster (fewer steps per key) |
| distribution | good at prime sizes, weak in the low bits | excellent at every table size |
On real-world short keys (peer names, contexts, device names) XXH3-64 is the
fastest of the candidates measured while matching the others on collision rate, so
lookups get cheaper with no distribution trade-off. The implementation is a small,
self-contained, dependency-free header (include/gabpbx/xxh3_64.h, XXH3-64 only)
with the hot path fully inlined — no external library and no extra build flags. The
case-insensitive variant (ast_str_case_hash) folds keys to ASCII-lowercase in
fixed stack windows, so it never allocates.
Measure it on your own build and hardware:
*CLI> core test hash [iterations]
String hash benchmark
algorithm : XXH3-64 (xxHash 0.8.3)
iterations : 5000000 (cycling 20 sample keys, avg 12.1 bytes)
ast_str_hash : 104.28 Mhash/s 1257 MB/s 9.59 ns/hash
ast_str_case_hash : 56.03 Mhash/s 675 MB/s 17.85 ns/hash
You can inspect live table health (load, longest chain, collisions, chi-squared)
with core show ao2 and, for chan_sofia's own tables, sip show hashstats.
Bounded REGISTER offload pool (register_pool, default OFF). The one place a
slow back end can stall the whole signaling thread is the realtime-database write
that follows each REGISTER: under a registration storm against a slow database,
every queued INVITE and OPTIONS waits behind those writes. With register_pool=yes
the realtime-DB writes for REGISTER are handed to a small fixed pool of worker
lanes keyed by AOR — per-AOR ordering is preserved while the signaling thread
returns immediately. The pool is bounded (fixed lane count and a fixed in-flight
cap, so it cannot grow without limit under load), ships behind a kill-switch that
is OFF by default (behavior is byte-for-byte the legacy inline path until you
enable it), and is reversible at runtime with sofia reload.
This is Phase 1 — it offloads only the database write, and it ships "dark" so the win can be measured on a real deployment before it is trusted. The direction is a signaling thread that never blocks on the database, which is what lets Kamailio and FreeSWITCH absorb carrier-scale registration churn; later phases move more of the REGISTER path onto the bounded pool as the Phase 1 numbers justify it.
- Large fleets of TLS endpoints —
chan_sipcarries a thread per connection,chan_sofiadoes not. - Phones behind misbehaving NATs that rotate the source port on every
REGISTER —
chan_sofiastores each Contact in a separate refcounted object and evicts the oldest whenmax_contactsis reached, while still counting the call as one regardless of how many parallel rings fire. - Multi-device users on the same SIP credentials —
chan_sofiaforks the outbound INVITE in parallel and the first device that answers wins; thecall-limitcounter sees the call as one, not as the number of contacts.
- Single-core SIP throughput, since both drivers ultimately funnel through one thread.
- Per-peer config size for a peer with rich
setvar/header/ACL lists — both drivers allocate variable-size auxiliary lists; the bulk of the cost is the user-supplied content, not the driver.
chan_sofia and chan_sip must not be loaded at the same time.
They register the same public names:
- Channel technology:
SIP - CLI commands:
sip show peers,sip show peer,sip set debug, etc. - AMI actions:
SIPpeers,SIPshowpeer,SIPqualifypeer, etc. - Realtime family:
sippeers - Dialplan functions:
SIPPEER,SIPCHANINFO,SIP_HEADER,CHECKSIPDOMAIN
Use one SIP driver per GABPBX process.
Recommended modules.conf:
noload => chan_sip.so
load => chan_sofia.soRestart GABPBX after changing the active SIP driver. Do not rely on a simple
module reload when switching between chan_sip and chan_sofia.
chan_sofia runs all SIP signalling on a single Sofia-SIP event-loop thread.
That thread owns the mutable peer and dialog state, and configuration reload is
dispatched onto the same thread, so signalling and reload never run at the same
time. Every other thread — dialplan, CLI, AMI, bridge/RTP, the scheduler, and
the registration and qualify helpers — only reads that state, and does so under
a lock.
The lock order is hard and never inverted:
channel-lock -> pvt-lock -> peer-family (the peer ao2 lock and the peer mutex)
The model is written down as a single authoritative block at the top of
channels/chan_sofia.c; every inline lock note in the file is an instance of
it. The rules that keep the driver safe under load:
- Freeable peer data (string fields, ACL lists, channel variables) read off the
SIP thread is copied under the peer lock before use, because a configuration
reload can free and rebuild it. The two global ACL lists,
localnetand the contact ACL, have dedicated read-write locks for the same reason — sosip reloadis safe while calls are active. - An in-dialog request or response carries its dialog as the handle context, but a hangup on the channel thread can free that dialog at the same moment. The dispatcher revalidates the dialog and holds a reference for the whole handler, so the re-INVITE, REFER, INFO, ACK, MESSAGE, BYE and CANCEL handlers can never dereference a freed dialog.
- A lock is never held across a blocking or channel-locking call (the SIP stack, DNS, music-on-hold, channel-variable application). Such values are copied first, the lock is released, then the call is made.
These paths are verified with a repeatable stress procedure: a SIP call flood
together with a sip reload loop, run under a thread-debug (DEBUG_THREADS)
build so held locks and lock order can be inspected with core show locks. The
current driver passes that load with no crash, deadlock or memory error.
chan_sofia is built as one loadable module (chan_sofia.so) from a driver core plus a set of
cohesive subsystem modules under channels/sofia/. The build links chan_sofia.o together with
every channels/sofia/*.c automatically (a wildcard rule in channels/Makefile), so a new module
file is picked up without touching the build system.
channels/chan_sofia.c is the driver core: the channel technology vtable (call/answer/hangup/
indicate/read/write/bridge), module load/unload, the single sofia_thread bootstrap, the
sofia_event_callback dispatcher (the handle-magic discriminator that keeps call dialogs, peers and
subscriptions from being confused — the central safety seam), digest authentication, the call/dialog
request handlers (INVITE/ACK/BYE/CANCEL/RE-INVITE/REFER/INFO/MESSAGE), inbound REGISTER processing,
and the configuration and peer lifecycle. These share the per-call and per-peer state too closely to
separate, so they stay together.
The subsystems that form clean functional boundaries live in their own files:
channels/chan_sofia.c Driver core (vtable, event dispatch, auth, call/register/config)
channels/sofia/sofia_sdp.c SDP offer/answer: parse, generate, hold, crypto attributes
channels/sofia/sofia_t38.c T.38 fax negotiation state machine
channels/sofia/sofia_presence.c Presence / BLF SUBSCRIBE -> NOTIFY engine
channels/sofia/sofia_publish.c Outbound PUBLISH (RFC 3903) publisher
channels/sofia/sofia_subscribe.c Outbound MWI SUBSCRIBE (watcher) -> local MWI inject
channels/sofia/sofia_ami.c AMI manager actions (SIPpeers/SIPshowpeer/SIPshowregistry/...)
channels/sofia/sofia_cli.c CLI commands (sip show peers/settings/registry, set debug, ...)
channels/sofia/sofia_blacklist.c Failed-auth blacklist engine
channels/sofia/sdp_crypto.c SDP a=crypto (SDES) attribute handling
channels/sofia/srtp.c SRTP key/policy lifecycle
channels/sofia/include/ Per-module API headers + chan_sofia_internal.h (shared model)
configs/sofia.conf.sample
doc/realtime_sippeers.txt
doc/realtime_sipregs.txt
configs/res_pgsql.conf.sample
channels/sofia/include/chan_sofia_internal.h holds the types and declarations shared across the
driver core and the subsystem modules (the peer, per-call and configuration structs, the model
constants and enums, and the cross-module function prototypes). Each module keeps its own internal
state private and exposes only the narrow API the rest of the driver calls.
Supporting subsystems used by chan_sofia:
main/rtp_engine.c
main/udptl.c
main/manager.c
main/pbx.c
main/channel.c
main/bridging.c
main/dnsmgr.c
main/dsp.c
res/res_srtp.c
codecs/codec_opus.c
chan_sofia links against Sofia-SIP:
lsofia-sip-ua
The current make rules use:
-I/usr/local/include/sofia-sip-1.13
-L/usr/local/lib -lsofia-sip-ua
Install Sofia-SIP into /usr/local unless you also adjust the build rules.
Typical Sofia-SIP build:
cd ../sofia-sip
./configure --prefix=/usr/local
make
make install
ldconfigIn the internal source layout, Sofia-SIP is kept beside GABPBX:
/home/sources/voip/futurepbx/sofia-sip
From the GABPBX tree, that is normally:
../sofia-sip
If you see this build error:
[CC] chan_sofia.c -> chan_sofia.o
chan_sofia.c:120:10: fatal error: sofia-sip/nua.h: No such file or directory
120 | #include <sofia-sip/nua.h>
| ^~~~~~~~~~~~~~~~~
compilation terminated.
make[1]: *** [/opt/gabpbx/Makefile.rules:108: chan_sofia.o] Error 1
make: *** [Makefile:369: channels] Error 2
then Sofia-SIP headers are missing from the compiler include path. Install
Sofia-SIP to /usr/local, then verify:
test -f /usr/local/include/sofia-sip-1.13/sofia-sip/nua.h
test -f /usr/local/lib/libsofia-sip-ua.so || test -f /usr/local/lib/libsofia-sip-ua.a
ldconfig -p | grep sofia-sip-uaAfter that, build GABPBX again:
cd /opt/gabpbx
./configure
make menuselect
make -j$(nproc)
make installInside make menuselect, enable or verify before running the final build:
Channel Drivers -> chan_sofia
For Opus media with chan_sofia, also enable:
Codec Translators -> codec_opus
Start GABPBX:
gabpbx -vvvcOr connect to an existing instance:
gabpbx -rUseful checks:
*CLI> module show like sofia
*CLI> sip show settings
*CLI> sip show peers
*CLI> sip show peer <peer>
*CLI> sip show channels
If chan_sofia.so does not load, check:
-
chan_sip.sois not already loaded. - Sofia-SIP headers are in
/usr/local/include/sofia-sip-1.13. -
libsofia-sip-uais in/usr/local/lib. -
ldconfighas been run after installing Sofia-SIP. -
res_srtp.sois loaded if SRTP/encryption is required.
Main file:
/etc/gabpbx/sofia.conf
Sample:
configs/sofia.conf.sample
The file follows the sip.conf style. The intent is to let operators migrate
from chan_sip with minimal changes.
Minimal configuration:
[general]
context=default
bindaddr=0.0.0.0
bindport=5060
allowguest=no
realm=gabpbx
srvlookup=yes
[phone100]
type=peer
host=dynamic
secret=change-this-secret
context=internal
disallow=all
allow=opus
allow=alaw
allow=ulaw
dtmfmode=auto
nat=force_rport,comedia
directmedia=no
maxcontacts=3
call-limit=3Dialplan use remains compatible:
exten => 100,1,Dial(SIP/phone100,30)Some older comments or examples may refer to SOFIA/...; for the drop-in
driver interface, prefer the public SIP/... technology.
At runtime, chan_sofia is built around these major pieces:
- A Sofia-SIP NUA root and event thread.
- Peer storage in
ao2_container. - Dialog storage in
ao2_container. - Contact storage per peer for multi-contact registration.
- RTP glue registered with the GABPBX RTP engine.
- UDPTL glue for T.38/Fax related paths.
- A scheduler thread for timed operations that must not block the SIP stack.
- Registration and qualify worker threads.
- CLI command registration under the existing
sip ...namespace. - AMI action registration under the existing
SIP...namespace. - Dialplan applications and functions for SIP compatibility.
Threading note:
Sofia-SIP handles and NUA operations must run on the Sofia thread. AMI actions that need to touch Sofia-SIP dispatch work back to the Sofia root thread instead of calling NUA directly from the manager thread.
chan_sofia preserves the important external names:
Channel technology: SIP
Realtime family: sippeers
Config style: sip.conf compatible, using sofia.conf
CLI namespace: sip ...
AMI namespace: SIP...
Dialplan funcs: SIPPEER, SIPCHANINFO, SIP_HEADER, CHECKSIPDOMAIN
Migration rule:
Existing dialplan and monitoring should keep working when it depends on the
public SIP names, but behavior should be verified in staging before production.
Current source implements or routes the important SIP methods through Sofia-SIP:
REGISTERINVITEACKBYECANCELOPTIONSSUBSCRIBENOTIFYREFERMESSAGEINFOPUBLISHPRACKUPDATE
Primary implemented call flows:
-
In-dialog
UPDATE(RFC 3311): an inboundUPDATEon a confirmed dialog is answered directly — a no-SDPUPDATEis a target-refresh (200 OKwith no body), and anUPDATEcarrying an SDP offer is renegotiated like a re-INVITE (validate-then-commit;488if unacceptable) with an SDP answer. The200reflects the peer's session-timer policy (RFC 4028) and carries the GRUU target-refresh Contact whengruuis enabled. Glare (an offer we already have in flight) returns491, and an offer before the dialog is confirmed returns500withRetry-After.UPDATEis advertised inAllow. OutboundUPDATE(using it to send our own session-timer refresh) is a planned follow-up. -
Dynamic endpoint registration.
-
Inbound calls from peers.
-
Outbound calls to peers and trunks.
-
Direct media decisions.
-
RTP setup and codec negotiation.
-
SIP REFER based transfers.
-
Qualify probes.
-
MWI SUBSCRIBE handling.
-
Outbound registration to upstream registrars.
-
Q.850
Reasonheader (RFC 3326) whenuse_q850_reason=yesin[general]: theBYE/CANCELchan_sofia sends carriesReason: Q.850;cause=N;text="..."built from the channel hangup cause, and a received Q.850Reasonon an inboundBYE/CANCELis mapped back to the AST hangup cause for CDR fidelity. Default off; shown insip show settings. (Not added to INVITE-rejection responses — a sofia-sip limitation.)
Supported transports on the listener side:
udp
tcp
tls
ws
wss
For connection-oriented transports (TCP, TLS, WS, WSS) the OS socket is kept alive with
SO_KEEPALIVE (about 30 seconds) by default — the same socket-level keepalive chan_sip
set per session — so a TCP/TLS phone behind NAT keeps a live socket out of the box.
For an application-level keepalive that also refreshes the NAT mapping, enable a periodic CRLF on idle connections:
tcp_keepalive = 30 ; seconds between CRLF keepalives on an idle connection (0 = off)
tcp_pingpong = 10 ; seconds to wait for a pong before closing a dead connection (0 = off)tcp_keepalive sends a double-CRLF ping (RFC 5626 §3.5.1) on idle TCP connections; TLS
and WS rely on the SO_KEEPALIVE socket option above. tcp_pingpong adds dead-connection
detection and requires tcp_keepalive (it is ignored, with a warning, on its own). Both are
off by default; changing either takes effect on the next sip reload.
chan_sofia does not gate per-peer inbound transport. The per-peer
transport= directive is accepted at config-parse and realtime-load time
purely for chan_sip drop-in template compatibility — it is read but not
applied. Operators upgrading from chan_sip can leave existing
transport=udp, transport=udp,tcp, or empty transport= rows in place
without behavioural change and without log noise on cache rebuilds.
Two layers actually determine which transport a SIP message uses:
-
Per-listener bindings at
[general]—bindport,tcpbindaddr,tlsbindaddr,wsbindaddr,wssbindaddrand their*_enabledflags. These define which sockets the server opens and accepts on. Configure only the listeners the deployment needs. -
Per-Contact at REGISTER-time — the inbound
Contact:URL scheme (sip:/sips:/;transport=tlsparameter) carries the transport the registering UA wants you to use for outbound messages to it.chan_sofiastores the per-contact transport derived from the URL and uses it when routing back to that contact.
If a deployment wants transport-based security (e.g. "this peer must use TLS"), the supported model is:
- Run a dedicated TLS-only listener on its own bindport.
- Put the peer's
host=<ip>orpermit/denyACL on the IP address that reaches that listener. - Optionally combine with
lockuseragent+lockuseragent_prefixesto pin the device family.
This mirrors the FreeSWITCH model (profile-level listener + cidr= user
attribute + tls-only at profile level) and replaces the chan_sip
per-peer transport= allowlist, which ran after socket-accept + parse +
peer-lookup and provided no real attack-surface reduction.
Listener options include:
bindaddr=0.0.0.0
bindport=5060
tlsbindaddr=0.0.0.0
tlsbindport=5061
tlscertfile=/etc/gabpbx/keys
wsbindaddr=0.0.0.0
wsbindport=5066
wssbindaddr=0.0.0.0
wssbindport=7443chan_sofia uses Sofia-SIP transport binding directly:
-
bindaddrandbindportbuild the primarysip:listener. - Sofia-SIP opens SIP UDP and SIP TCP on that
sip:listener. -
tlsbindaddrandtlsbindportbuild the separatesips:TLS listener. -
tlscertfileis the certificate directory, or the real cert+key file: when it names a file,chan_sofiaderives the directory and soft-links the name Sofia-SIP expects (agent.pem) to that file (andcafile.pemtotlscafile), so an externally managed certificate can be used without renaming it. WSS names (wss.pem,ca-bundle.crt) are auto-linked from the TLS ones. Links are created only when missing and never overwrite. -
tlsenable/wsenable/wssenable(defaultyes) explicitly enable or disable each listener even when its bind port is set. - Certificate immunity: a TLS/WSS listener with no usable certificate is disabled with a logged error instead of failing the whole driver; UDP, TCP and WS keep serving and the module still loads.
Current chan_sofia configuration intentionally does not use the old
chan_sip keys tcpenable and tcpbindaddr. Do not rely on those keys for
chan_sofia unless the source parser is extended to read them.
Example with SIP TCP on port 60000 and SIP TLS on port 60001:
[general]
bindaddr=0.0.0.0
bindport=60000
tlsbindaddr=0.0.0.0
tlsbindport=60001
tlscertfile=/etc/gabpbx/keysBecause the primary sip: listener opens both UDP and TCP, this example also
opens UDP on 60000. If a deployment needs UDP and TCP on different ports,
that is a code feature request, not only a configuration change.
The TLS listener accepts three optional hardening knobs. tls_ciphers and
tls_verify_depth are off by default, so leaving them unset keeps
Sofia-SIP's own default with no change in behaviour; tls_min_version
defaults to 1.2 (see below).
[general]
tls_min_version = 1.2 ; minimum TLS version: 1.0, 1.1, 1.2 or 1.3 (default 1.2)
tls_ciphers = ECDHE+AESGCM:!aNULL:!MD5 ; OpenSSL cipher list
tls_verify_depth = 3 ; maximum peer certificate chain depth-
tls_min_versionsets the minimum accepted TLS version and enables that version and every higher one. It defaults to1.2(TLS 1.2 and TLS 1.3 accepted, TLS 1.0 and TLS 1.1 refused) — anopenssl s_client -tls1_1attempt is rejected with a protocol-version alert. Set1.0or1.1only to interoperate with legacy endpoints that cannot negotiate TLS 1.2. (Earlier builds defaulted to TLS 1.3 only, which rejected endpoints that cap at TLS 1.2.) -
tls_ciphersis passed verbatim to OpenSSL; the default is!eNULL:!aNULL:!EXP:!LOW:!MD5:ALL:@STRENGTH. -
tls_verify_depthbounds the certificate chain length (Sofia-SIP default is 2) and pairs withtlsverify.
Mutual TLS (mTLS) — verify the client certificate on the inbound listener:
[general]
tlsverify = yes ; verify the server cert on OUTBOUND TLS connections
tlsverifyclient = yes ; verify the client cert on the INBOUND TLS listener (mTLS)-
tlsverify(outbound) andtlsverifyclient(inbound) are combined into one verification policy: set both to require certificates in both directions. - With
tlsverifyclient = yes, a client that does not present a certificate trusted by the CA material intlscertfilefails the TLS handshake before any SIP is processed. - Per-subject pinning (an allowlist of accepted certificate subjects) is a planned follow-up.
These harden the TLS listener only — the WSS listener builds its own context (which
already disables SSLv2, SSLv3 and TLS 1.0) and ignores these keys. Changing any of
tlsverify, tlsverifyclient, tls_min_version, tls_ciphers or tls_verify_depth requires
systemctl restart gabpbx; a sip reload refuses the change with a
"listener config changed" message because the TLS context is built when the listener is
created.
Peers can then select the desired signaling transport:
[phone-tcp]
type=peer
host=dynamic
transport=tcp
[phone-tls]
type=peer
host=dynamic
transport=tlsTLS expects:
agent.pem
cafile.pem
For a test system, create a self-signed certificate like this:
install -d -m 750 /etc/gabpbx/keys
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
-subj "/CN=pbx.example.net" \
-addext "subjectAltName=DNS:pbx.example.net,IP:203.0.113.10" \
-keyout /etc/gabpbx/keys/agent.key \
-out /etc/gabpbx/keys/agent.crt
cat /etc/gabpbx/keys/agent.crt /etc/gabpbx/keys/agent.key > /etc/gabpbx/keys/agent.pem
cp /etc/gabpbx/keys/agent.crt /etc/gabpbx/keys/cafile.pem
chmod 600 /etc/gabpbx/keys/agent.key
chmod 640 /etc/gabpbx/keys/agent.pem /etc/gabpbx/keys/cafile.pem /etc/gabpbx/keys/agent.crtFor production, replace the self-signed certificate with a certificate signed by a trusted CA and keep the private key readable only by the GABPBX runtime user.
After changing listener ports, restart GABPBX. A Sofia listener port change is not a safe reload-only operation.
Verify runtime settings:
*CLI> sip show settings
Expected fields for the example above:
Bind Address: 0.0.0.0
Bind Port: 60000
TLS Bind Port: 60001
Verify operating-system sockets:
ss -lntup | grep -E ':(60000|60001)'Verify the TLS handshake:
openssl s_client -connect 127.0.0.1:60001 -servername pbx.example.net -brief </dev/nullWSS support reuses TLS material and includes helper behavior for expected WSS certificate file layout.
Important WebSocket note:
WebRTC media (DTLS-SRTP, ICE-lite, rtcp-mux and BUNDLE) is implemented as of 1.1.2: a browser registers over WSS and holds two-way audio and two-way video (VP8/H.264). See WebRTC.
For browser-facing deployments, put chan_sofia behind a reverse proxy that
enforces:
- TLS policy
- Origin allowlist
- WebSocket subprotocol policy
- Public HTTPS/WSS entrypoint
For a PBX behind NAT, externaddr (a static public IP) or externhost (a DDNS name,
re-resolved periodically) is advertised in SDP/Contact for peers outside the
localnet ranges. externip is accepted as a chan_sip-compatible alias of
externaddr:
[general]
externip = 203.0.113.1 ; chan_sip name; same as externaddr
localnet = 192.168.0.0/24media_address advertises a different address in the SDP c=/o= lines (audio,
video and T.38) than the one used for signalling — for multi-homed hosts where media
must leave a different interface:
[general]
media_address = 198.51.100.20It is advertise-only: the RTP sockets still bind bindaddr; only the address
offered to the far end changes. media_address wins over externaddr/externip (a
deliberate operator override) while direct-media redirection still takes precedence.
The value is validated as an IP (a port is stripped; IPv6 is emitted unbracketed in
SDP); an invalid value is ignored with a warning.
chan_sofia supports dynamic peer registration with multi-contact storage.
Core options:
host=dynamic
max_contacts=6
minexpiry=60
maxexpiry=3600
defaultexpiry=120Behavior:
-
REGISTERcan create or update peer contact entries. - Multiple contacts can be stored for the same peer.
-
max_contactslimits how many active contacts a peer may use (range1..6). Matchingchan_sipbehaviour, when the cap is reached and a new Contact URI arrives, the contact with the earliest expiry (LRU byexpires) is evicted and the new one is linked. The phone keeps a valid registration; the stale row is dropped. This is the chan_sip parity path; older builds returned 403 on the rejected REGISTER which broke phones that rotate source ports between REGISTERs. - The per-eviction trace
Sofia: peer '<name>' at max_contacts=N — evicting oldest contact <uri>is gated behindsip set debug; production runs silent. Turn it on withsip set debug on(global) orsip set debug peer <name>(scoped to a single peer) when investigating REGISTER churn. - The per-REGISTER summary "
Registered SIP …/Unregistered SIP …/contact moved …" lines are gated byVERBOSITY_ATLEAST(6); default verbose levels stay quiet. Bump the level withcore set verbose 6to surface them. AMIPeerStatusevents fire on a separate path and remain available regardless of these display gates. - A REGISTER from the same Contact URI as an existing entry refreshes the
expiresvalue in place (no eviction needed). - Requested expiry below
minexpirycan be rejected with423 Interval Too Brief. - Requested expiry above
maxexpirycan be capped. - Expiry
0unregisters. - Registration state can be saved through realtime.
Each Contact in a REGISTER is an independent binding, so a peer that
registers several devices under one account is handled per the RFC 3261 §10.3
registrar rules:
- Each Contact binds, refreshes and de-registers on its own. A REGISTER that carries one Contact does not disturb the peer's other bindings.
- Each binding honours its own
;expiresContact parameter (falling back to the requestExpiresheader, thendefaultexpiry), clamped tominexpiry/maxexpiry. -
expires=0on a single Contact removes only that binding; the peer's other contacts stay registered. AContact: *withExpires: 0clears them all. - The
200 OKechoes back each surviving binding with its own remaining expiry.
sip show peer <name> detail reflects the per-binding state — AVAILABLE
for a live binding, EXPIRED for one past its expiry, and IN-CALL while a
binding carries an active call.
All-expired no-route guard. A call placed to a peer whose bindings have
all expired returns CHANUNAVAIL instead of being routed to a stale
binding, so a call is never forked into a contact that is known to be gone.
Outbound registration is also supported through register-style peers. Related retry controls:
registertimeout=20
registerattempts=0registertimeout is the application-level retry interval for outbound
registration. It is not the same as a SIP transaction timeout.
For a connection-oriented binding — WSS, WS, TLS or TCP — chan_sofia
removes the binding promptly when its underlying connection closes, instead of
letting it linger until the SIP registration expires. The registrar watches the
transport connection and, when it is torn down, drops the contact reached over that
connection and corrects the routing state immediately. This is the RFC 5626 "flow"
model: a binding is tied to the flow that created it, so a closed flow is a dead
binding.
This matters most for WebSocket browser clients, which reconnect often — a page refresh tears down the old WSS connection and opens a new one. Without flow-close the old Contact would sit stale, and reachable in routing, until its registration expiry, so a call could be routed into a dead socket. With flow-close the stale binding is gone the moment the socket closes and only the live connection's Contact remains.
[general]
flowclose_emit_unregister = no ; default: silent flow-closeflowclose_emit_unregister (per-peer, or in [general]; default no) controls only
the external side-effects of a flow-close — the AMI PeerStatus event, the
BLF / hint device-state update, and regexten cleanup:
-
no(default) — the binding is removed and routing state is corrected silently. A browser refresh therefore does not flap the BLF lamp or emit an unregister event for what is really a momentary reconnect. -
yes— each flow-close additionally fires the full unregister side-effects (an explicitPeerStatus/ device-state update each time a flow closes), for deployments that want an event per connection drop.
The binding is always removed and the routing state is always corrected
regardless of the option; flowclose_emit_unregister changes only whether the
external unregister notifications fire.
Every REGISTER attempt is logged at NOTICE with the account, the source IP and
the User-Agent, so the registering handset can be identified from the log alone:
Sofia REGISTER OK: user='1001' ip=203.0.113.7:5060 useragent='Example SIP Phone 2.1'
Sofia REGISTER REJECT (unknown peer): user='9999' ip=203.0.113.7:41020 useragent='probe-tool/1.0'
-
Failures always log —
REJECTwith the reason:unknown peer,ACL,auth,interval too brief,user-agent lock, ormax contacts. - Successes log only on a state change — a new registration, an unregister, or a contact add / remove / move — not on every keepalive refresh, so a fleet of phones re-registering each minute does not flood the log.
- The line is
NOTICE(not asip set debugor verbose trace), so it appears at default log levels, and is independent of the AMIPeerStatusevent.
sip show registry lists every outbound registration the PBX maintains (one per
register => line) and its live state:
Host Username Refresh State
sip.provider.com:5060 myuser 3590 Registered
192.0.2.47:60000 test 0 Unregistered (3 attempts)
-
Refresh — seconds until the next refresh (
0when not registered). -
State —
RegisteredorUnregistered; the(N attempts)hint appears when an upstream keeps rejecting the digest, which usually means the trunksecretis wrong.
The same data is available to automation through the AMI SofiaShowRegistry action.
sip unregister <peer> force-expires a dynamic peer's inbound registration, so the
phone has to register again:
*CLI> sip unregister 1001
Unregistered peer '1001'
It clears the peer's contact bindings, marks it unregistered and emits the
PeerStatus: Unregistered AMI event and the device-state change (so any BLF lamp
watching the peer updates immediately) — without sending anything to the phone. It
operates on in-RAM peers only; a realtime peer's database binding is left untouched
(the peer simply re-registers, or prune it from realtime separately). Peer-name
tab-completion is supported.
When this server registers to an upstream registrar as a client (a
register => trunk: host set, secret set, not host=dynamic), gruu=yes
adds a stable +sip.instance parameter to the outbound REGISTER Contact
(RFC 5626 §4.1):
[mytrunk]
type=peer
host=registrar.example.com
defaultuser=...
secret=...
gruu=yesThe REGISTER Contact then carries a stable instance URN:
Contact: <sip:*@198.51.100.10:60000>;+sip.instance="<urn:uuid:eef0159c-cd0e-...>"
The URN is derived from this server's Entity ID and the peer name, so it is unique per peer and stable across restarts. A GRUU-capable registrar or a federation layer can then uniquely identify this UA instance — which matters because chan_sofia registers Contacts under a wildcard user, so the instance is the only stable discriminator.
The REGISTER also advertises Supported: gruu (RFC 5627 §4.1) — merged with the
stack's default timer, 100rel — which alerts the registrar to mint a GRUU. When
a GRUU-capable registrar returns a pub-gruu (and a rotating temp-gruu) for our
instance in the REGISTER 200 OK Contact (RFC 5627 §5.2), chan_sofia learns and
stores them; sip show peer shows them:
GRUU : advertised (+sip.instance)
Pub-GRUU : sip:alice@example.com;gr=urn:uuid:eef0159c-cd0e-...
Temp-GRUU : sip:tgruu.7hs==jd7vn...@example.com;gr
The learned GRUUs are kept as opaque URIs and cleared when the registration goes
inactive (unregister/expiry/failure). It does not enable reg-id or the RFC 5626
outbound flow and sends no validation/keepalive OPTIONS.
With use_gruu_contact=yes (the default when gruu=yes), chan_sofia then uses
the learned pub-gruu as the Contact of outbound calls and re-INVITEs placed
through the peer (RFC 5627 §4.4) — so the far end routes in-dialog and
target-refresh requests back to this exact instance via the registrar — and calls
on the peer also advertise Supported: gruu in their requests and responses. Set
use_gruu_contact=no to keep advertising/learning the GRUU but fall back to the
normal sip:user@our-ip Contact (an interop kill-switch). Default off; opt-in per
peer.
When this server registers to an upstream registrar (a register => trunk) that
returns a Service-Route header in the REGISTER 200 OK (RFC 3608),
service_route=yes ingests that route set and pre-loads it as a Route on
outbound INVITEs through the peer, so calls traverse the registrar's service
proxy chain (Kamailio-as-proxy / IMS-style deployments):
[mytrunk]
type=peer
host=registrar.example.com
secret=...
service_route=yesThe learned route is shown in sip show peer and is updated from the latest
200, cleared when the registrar stops returning it or on de-registration. It is
applied after any outboundproxy route (RFC 3608 §6.1 ordering), only while
the trunk is registered. Scope: outbound INVITEs only — the registrar's domain;
PUBLISH/SUBSCRIBE (which may target a different domain) are a documented
follow-on. Default off; opt-in per peer (applying a Service-Route diverts outbound
routing, so enable it only when your upstream uses it).
When a dynamic peer registers through an edge proxy / SBC that inserts a
Path header (RFC 3327), path=yes makes this server (as the registrar)
accept and store that Path with the contact and pre-load it as a Route on the
requests it sends back to that device — so they traverse the edge proxy to the
NATed device:
[myphone]
type=friend
host=dynamic
secret=...
path=yesThe stored Path is shown per-contact in sip show peer, echoed in the REGISTER
200 OK, and used as a Route on outbound INVITEs to the device (including each
contact of a multi-contact peer). If a Path is present but the device did not
advertise Supported: path, the REGISTER is rejected 420 Bad Extension with
Unsupported: path (RFC 3327 §5.3). Default off — accepting a Path is a trust
decision (RFC 3327 §7). v1 applies the Path to INVITEs; NOTIFY/OPTIONS/
MESSAGE are a documented follow-on.
Basic 100rel/PRACK already works out of the box: chan_sofia advertises
Supported: 100rel, PRACKs a reliable provisional a far end sends on an outbound
call, and its 183 Session Progress early-media response is sent reliably
whenever the caller advertises Supported: 100rel.
rel100=yes (per-peer, default off) extends that to the other provisionals:
180 Ringing (and any non-183 1xx) is then sent reliably too — with
Require: 100rel + RSeq, awaiting the caller's PRACK — for gateways/carriers
that want reliable ringing.
[mygateway]
type=peer
host=gw.example.com
rel100=yesDefault off because a few UACs advertise Supported: 100rel but mishandle a
reliable 180; the resulting PRACK timeout would fail the INVITE. Shown in
sip show peer.
When a peer registers outbound to an upstream registrar (a non-dynamic host
plus a secret), sip_outbound=yes makes the REGISTER advertise
Supported: outbound, path and carry a stable +sip.instance (a UUID URN derived
from the server EID and peer name, persistent across restarts) plus reg-id=1 on
the Contact — so a SIP-Outbound-capable registrar/edge proxy maintains the
client-initiated flow and routes inbound calls back over it (RFC 5626 §4.2.1):
[mytrunk]
type=peer
host=sip.example.com
defaultuser=...
secret=...
sip_outbound=yes
tcp_keepalive=30 ; if the trunk is TCP/TLSThis uses manual signalling, not the sofia-sip outbound engine (no STUN/OPTIONS
probing). The keep-alive that holds the flow open is the connection-oriented CRLF
ping — set tcp_keepalive (and tcp_pingpong) for TCP/TLS trunks; a warning is
logged if the registrar confirms outbound (Require: outbound in the 200) but no
keep-alive is configured or it is slower than the registrar's Flow-Timer. The
runtime state (confirmed + Flow-Timer) is shown in sip show peer. Shares the
+sip.instance with gruu=yes; default off (opt-in). UDP STUN keep-alive and
multi-flow registration are a documented follow-on.
The primary realtime family is:
sippeers
Example extconfig.conf style:
sippeers => pgsql,general,sippeersRegistration state can optionally be separated:
sipregs => pgsql,general,sipregsWhen sipregs is configured separately, peer configuration remains in
sippeers and registration state can be written to sipregs.
Read:
doc/realtime_sippeers.txt
doc/realtime_sipregs.txt
Runtime cache management:
*CLI> sip prune realtime peer <name>
*CLI> sip prune realtime peer all
*CLI> sip prune realtime peer like <pattern>
*CLI> sip prune realtime all
Use this when a realtime peer is edited in SQL and the in-memory cached copy must be dropped without restarting GABPBX.
Digest authentication supports:
MD5
SHA-256
Implemented behavior:
qop=auth- RFC 2069 fallback when the client omits
qop - nonce TTL
- stale nonce challenge
- nonce-count replay protection
- configurable algorithm offer (
auth_algorithms, see below) - anti-downgrade verification (only an offered algorithm is accepted)
-
alwaysauthrejectto avoid username enumeration - AMI
AuthFailureevent on authentication failure paths
Relevant options:
realm=gabpbx
auth_algorithms=md5
alwaysauthreject=yes
nonce_ttl_seconds=3600
force_invite_auth=noSecurity defaults should favor not revealing whether an account exists.
chan_sofia authenticates its own outbound requests to an upstream that challenges
them — both the periodic REGISTER from a register => line and every outbound INVITE
placed through a peer trunk. When the provider answers 401 Unauthorized or 407 Proxy Authentication Required, the driver computes the digest from the trunk peer's
credentials and resends the request with an Authorization / Proxy-Authorization
header, and the call or registration proceeds. A single response that carries both a
WWW-Authenticate (registrar/endpoint) and a Proxy-Authenticate (proxy) challenge
is answered in one step.
The credentials are the trunk peer's defaultuser/username and secret (for a
register => directive, the user:password it carries). Use a plaintext
secret=: a pre-hashed md5secret= cannot authenticate outbound because the
cleartext password is required to compute the response — md5secret stays available
for inbound authentication. Outbound attempts are bounded per request, so a wrong
credential can never drive an endless challenge loop.
chan_sofia builds the WWW-Authenticate challenge itself, so the algorithm(s)
it advertises are operator-selectable in [general]. This is a global switch:
the same offer is made to every peer — there is no per-peer override.
auth_algorithms |
Challenge advertises |
|---|---|
both |
MD5 and SHA-256 (MD5 listed first) |
md5 |
MD5 only |
sha256 |
SHA-256 only (RFC 7616; refuses MD5) |
Verification is anti-downgrade: the server accepts only an algorithm it
actually offered. A client that answers with an un-advertised algorithm — or that
omits algorithm= (which implies MD5) when MD5 was not offered — is rejected with
400 Bad Request. A sha256-only deployment therefore cannot be coerced back to
MD5.
The built-in default is both, but the shipped sofia.conf sets md5, because
chan_sip only ever challenges with MD5 — md5 is the drop-in-compatible choice
and maximises handset interoperability. Set both or sha256 to opt into SHA-256.
A peer configured with
md5secret(a pre-hashed MD5 credential) and no plaintextsecretcan only authenticate with MD5; keepauth_algorithmsatmd5orbothfor such peers.
chan_sofia carries chan_sip's lockuseragent flag and adds an optional
prefix-list mode that addresses the cases the strict anchor cannot serve.
Two operating modes, selected by whether lockuseragent_prefixes is empty:
lockuseragent |
lockuseragent_prefixes |
Behaviour |
|---|---|---|
no (default) |
any value | No gate. Default. |
yes |
empty | Strict-anchor mode (chan_sip parity). First successful REGISTER captures the User-Agent: header; subsequent REGISTERs with a different UA are silently rejected. |
yes |
non-empty | Prefix-list mode. Capture is bypassed. A REGISTER passes when its User-Agent: header starts (case-insensitive) with ANY listed prefix. |
Configuration:
[peer-with-two-devices]
type=peer
host=dynamic
secret=...
lockuseragent=yes
lockuseragent_prefixes=Yealink SIP-T,Grandstream GRP,MicroSIP/Properties:
- Comma-separated, case-insensitive prefix match.
- The list is the storage; tokenisation/trim/match happens at REGISTER-time, so
edits via
module reload chan_sofia.so(config-file) or realtime UPDATE +sip prune realtime peer <name>take effect on the very next REGISTER with no restart. - On rejection the existing silent 401 challenge fires (the attacker cannot
distinguish UA-mismatch from bad-secret), accompanied by an AMI
LockUserAgentRejectevent withMatchPolicy: prefix-listand the configuredPrefixes:payload (strict-anchor mode emitsMatchPolicy: strict-anchorand an emptyPrefixes:field). - Setting
lockuseragent=nodisables the gate regardless of the prefix list; the master switch remains the same.
Use cases:
- One subscriber with two devices on the same SIP credentials (desk phone + softphone) — list both UA prefixes.
- Vendor-family lockdowns (any Yealink T-series, any Grandstream GRP-series).
- Stricter than IP allowlists when SIP credentials are exposed but the UA string of the legitimate device is known and not trivially spoofable in the field.
Display surface:
-
sip show peer <name>showsLock user-agent,Locked UA(strict mode only, once captured), andUA prefixes(prefix mode only, when set). - AMI
SIPshowpeeremitsLockuseragent,LockedUserAgent, andLockUserAgentPrefixesfields.
chan_sofia includes a local SIP blacklist for repeated bad registration or
authentication behavior.
Current behavior:
- Default table size: 1024 entries (
sofia_blacklist_max). - Default lock threshold: 5 failures.
- Data structure:
ao2_container. - Locked IPs are silently dropped.
- No SIP error response is sent to locked IPs.
- When the table is full, the entry with the oldest
last_seenis evicted (LRU) to make room for the new offender. The previous "FULL → reject" behaviour let an attacker rotating IPs past the cap escape blacklisting entirely; LRU eviction keeps the defence working under sustained attack at the cost of forgetting the least-recently-seen offender. ANOTICE-level log entry is written on every eviction so the operator can sizesofia_blacklist_maxappropriately if the eviction rate is high.
CLI:
*CLI> sip show blacklist
*CLI> sip blacklist search <IP>
*CLI> sip blacklist delete <IP>
*CLI> sip blacklist clear
Configuration ([general] in sofia.conf):
; blacklist_ban - minutes a blocked IP stays banned after its last failure (and how
; long an accumulating failure counter is remembered). Default 1440 (24h).
; 0 = banning disabled entirely. Applied live on `sofia reload` (no restart).
blacklist_ban = 1440The silent-drop behavior is intentional and matches the desired security posture: once an address is locked, the PBX should look dead to that source.
Important options:
externaddr=203.0.113.1
externhost=pbx.example.net
externrefresh=10
localnet=192.168.0.0/24
localnet=10.0.0.0/8
nat=force_rport,comedia
directmedia=noBehavior:
- Local peers matching
localnetkeep local SDP addresses. - Peers outside local networks can receive external SDP addresses.
-
force_rportsupports symmetric response routing. -
comediasupports learning RTP source addresses from received media. -
directmedia=nokeeps RTP anchored through GABPBX.
NAT-aware in-dialog and SDP routing (chan_sip parity):
- Inbound INVITE matching falls back to a source-IP search of
peerswhen the From-username does not name a configured peer. This letshost=<ip>trunks be identified by their SBC source address even when the carrier uses the dialled number as the From-user. Without this fallback the default-denyalwaysauthrejectpath emits a 401 the trunk cannot answer. - The SDP
c=host on incoming responses is replaced withpeer->src_addrfor peers withnat=force_rportorcomediawhen the advertised host is a private LAN address. RTP is then sent to the public NAT-mapped source the phone actually registered from instead of the unroutable LAN address many NAT'd UAs leak in their SDP. - The outbound INVITE handle disables sofia-sip's auto-ACK for NAT'd peers
and emits a manual
ACKpinned topeer->src_addrviaNUTAG_PROXY. Without this override the 2xx-ACK would route to the LAN address in the 200 OKContact:URI and never arrive, leaving the phone to retransmit 200 OK until the dialog dies. The same NUTAG_PROXY override is applied to in-dialogBYEandREFER. - The outbound INVITE to a registered NAT'd peer (and each forked-contact
INVITE, plus any directmedia re-INVITE) is itself routed to the contact's
learned public source via an operation-level
NUTAG_PROXY, while the Request-URI keeps the (private)Contact. A registered PBX or phone behind NAT advertises a private LAN address in itsContact; without this override the call INVITE would be sent to that private address and black-hole, so the device never rings. The proxy is applied on the request operation itself - not only at handle creation, which does not steer the initial INVITE in this stack - and is copied onto the winning leg after a parallel fork so later re-INVITEs keep routing to the public source. The Request-URI is left as the Contact, matchingchan_sip(which sends to the registered source); a non-NAT peer is unaffected.
DNS manager integration:
- Hostname-based peers can use async DNS tracking.
- Address changes can produce AMI
DnsManagerUpdate.
chan_sofia integrates with the GABPBX RTP engine.
Supported media behavior:
- Codec offer/answer processing.
- Static RTP payloads for classic codecs.
- Dynamic RTP payloads for codecs such as Opus.
- DTMF mode handling.
- Inband DTMF through DSP when configured.
- RTP timeout, hold timeout and keepalive.
- Direct media policy.
- Hold detection and bridge hold signaling.
Common peer media options:
disallow=all
allow=opus
allow=alaw
allow=ulaw
dtmfmode=auto
directmedia=no
rtptimeout=30
rtpholdtimeout=300
rtpkeepalive=0Opus requires codec_opus and libopus. The codec module provides the Opus
translator and runtime CLI:
*CLI> opus show
dtmfmode selects how DTMF digits are carried, and chan_sofia follows
chan_sip semantics end to end:
dtmfmode=auto ; rfc2833 | info | inband | auto-
rfc2833— out-of-band telephone-event (RFC 4733/2833). Thetelephone-eventpayload is offered in SDP only for the RFC 2833 modes (rfc2833, and the RFC 2833 branch ofauto);infoandinbanddo not advertise it. The negotiated RTP DTMF property (AST_RTP_PROPERTY_DTMF) is set to the mode actually negotiated for the call, which can differ from the configured value. -
info— DTMF is carried in SIPINFO. InboundINFObodies are parsed in both theapplication/dtmf-relayand numeric forms, including the numericSignal=values (10→*,11→#,12-15→A-D) and aFlashsignal. -
inband— in-band tone detection through the GABPBX DSP. -
auto— resolved at SDP commit: if the far end offeredtelephone-eventthe call uses RFC 2833, otherwise it falls back to in-band DSP detection. The DSP is reconfigured in place when the mode is resolved, without being freed under a live call, so a fax tone (CNG) already being detected is preserved.
The SIPDtmfMode() dialplan application re-syncs the DTMF mode on a live
channel, and AST_OPTION_DIGIT_DETECT (used by applications that toggle digit
detection at runtime) is honoured.
SRTP support uses the GABPBX SRTP layer and helper code under
channels/sofia.
Important options:
encryption=no
srtpcipher=
srtp_per_suite_keys=noOperational notes:
-
res_srtp.somust be loaded for SRTP. - Unset encryption fields should behave as disabled.
- If encryption is enabled but SRTP setup fails, call setup should fail clearly.
- Per-suite fresh key policy is available as a stronger
chan_sofiaoption.
Supported identity behavior includes:
callingpres=allowed_not_screened
sendrpid=no
trustrpid=no
fromuser=
fromdomain=
usereqphone=noConcepts:
-
sendrpid=paiemits P-Asserted-Identity. -
sendrpid=rpidemits Remote-Party-ID. -
trustrpid=yestrusts inbound PAI/RPID from trusted peers. -
usereqphone=yescan append;user=phonefor numeric SIP users.
Use trustrpid=yes only for trusted upstream systems or SBCs.
When a call is forwarded out a trunk, some carriers validate the forwarded leg against
the diverting party — a number they provision for that trunk — rather than the
original caller. forceddiversion (per-peer) forces a trunk-owned DID as the diverting
party in the outbound Diversion header (RFC 5806):
forceddiversion=<trunk-DID> ; e.g. a DID the carrier provisions for this trunk- The
Diversionis emitted only when the call carries a redirect indication — setREDIRECTING(from-num)orREDIRECTING(reason)in the dialplan beforeDial(). A plain, non-forwarded call never gets aDiversion. - It overrides only the diverting number, with privacy off. It does not touch the
caller identity: keep
CALLERID(num)as the original caller and do not pointfromuser=at the DID, so theFromstill carries the real calling party. - Empty (default) keeps the legacy data-driven
Diversionbehavior. - Readable from the dialplan with
SIPPEER(<peer>,forceddiversion).
REFER transfer support is controlled by:
allowtransfer=yesBehavior:
- Inbound SIP REFER can trigger transfer handling.
-
allowtransfer=norejects REFER with policy response. - AMI
TransferRejectedcan report rejected transfer attempts. - AMI
ReferProgressreports transfer progress notifications.
Blind-transfer dialog teardown (chan_sip parity):
- After the terminal
NOTIFY 200 OKis delivered to the transferer, RFC 5589 §6.1 expects the transferer's UA to sendBYEto release its own dialog.chan_sofiadoes not race the UA with its ownnua_bye; doing so would cause sofia-sip to drop the pending terminal NOTIFY and leave the UA stuck with a connected-but-silent dialog. The transferer-side pvt is markeddefer_bye, the channel is detached, and the SIP dialog stays alive waiting for the UA-initiated BYE. - A safety-net timer (32 s, mirroring chan_sip
DEFAULT_TRANS_TIMEOUT) firesnua_byeitself if the transferer's UA never BYEs. This bounds the leak window for misbehaving UAs without disturbing well-behaved ones. - The failure path (no bridged channel found at REFER time) still queues an immediate hangup; only the success path defers.
chan_sofia can also originate a SIP REFER to push a connected call away —
the outbound half of transfer, used when the PBX (not the remote UA) decides to
transfer. Two entry points, both routed through the same REFER engine:
-
SofiaTransfer(<refer-to>[,<replaces>])— a dialplan application.<refer-to>is a configured peer name (routed to its registered contact), asip:/sips:URI, or a bare extension (resolved against the local domain). With no<replaces>it is a blind transfer; with<replaces>("callid;to-tag=X;from-tag=Y") it is an attended transfer — the Replaces is percent-encoded into the Refer-To so the referee replaces an existing dialog. -
Transfer(<dest>)— the standard blind-transfer app (requiresapp_transferloaded). chan_sofia now implements the channeltransfercallback, soast_transfer/Transfer()/ AMI blind transfer emit a SIPREFERon the call's dialog.
; blind: refer the caller to extension 200
exten = _X.,1,SofiaTransfer(200)
; blind to an explicit URI
exten = _X.,1,SofiaTransfer(sip:agent@example.com)
; attended: connect the caller to an existing dialog via Replaces
exten = _X.,1,SofiaTransfer(sip:agent@example.com,abc123@host;to-tag=t1;from-tag=f1)The REFER is sent on the Sofia event thread; the transfer result is fed back to the
dialplan as the standard transfer outcome — the implicit refer subscription's final
NOTIFY (message/sipfrag) maps a 2xx to success and anything else (or a 30 s
timeout, or a hangup) to failure, so Transfer() never blocks indefinitely. Only one
outbound REFER is allowed in flight per dialog at a time. Outbound REFER is initiated
by your own dialplan, so it is not gated by allowtransfer (that governs inbound
REFER only).
Early-media on outbound legs (chan_sip parity):
- When the dialplan signals
AST_CONTROL_PROGRESS(carrier sends 183 with SDP) andprematuremedia=no,chan_sofiaforwards183 Session Progresswith an SDP body that answers the inbound INVITE offer. This is the chan_sip behaviour. The body lets RFC 3262 100rel-capable UAs (which trigger reliable provisional responses on the outbound leg) settle offer/answer at the early-media stage; without SDP the offer would stay pending and a spurious empty200 OKto the INVITE would fire after the UA's PRACK.
Recommendation:
- Keep transfers open for trusted internal phones.
- Disable REFER on untrusted external trunks unless required.
Some forking proxies in the field violate RFC 3261 §16.7 by relaying a
non-2xx final response (typically 486 Busy Here) and then a 200 OK
on the same INVITE transaction. The UAC has already torn the call down
when it sees the 486; the late 200 OK would otherwise be silently
dropped, leaving the answering UA retransmitting its 200 OK for 64*T1
(~32 s) with a stuck media leg and a later BYE answered 481 Call/ Transaction Does Not Exist.
chan_sofia recognises this case and emits ACK followed by BYE per
RFC 3261 §13.2.2.4 (a UAC MUST ACK every 2xx and, if it no longer wants
the dialog, MUST send a BYE). The flag that drives the recognition is set
in the non-2xx final paths of the outbound INVITE response handler so the
late 2xx is correctly classified when it arrives.
What you will see when the case fires:
NOTICE Sofia: orphan 200 OK on terminated INVITE <call-id>: ACK + BYE per RFC 3261 13.2.2.4
The LOG_NOTICE is visible at default log level so the path is auditable
without enabling debug. The same fix is applied to chan_sip for parity,
where the equivalent log line is:
NOTICE Got 2xx (200) on already-terminated INVITE <call-id>: ACK + BYE per RFC 3261 13.2.2.4
The fix addresses the UAC-side hygiene gap only. The root cause of a
lost call in this scenario — the proxy emitting two final responses on
one transaction — must be fixed at the proxy. This handler stops the
~32 s retransmit storm, clears the ghost media leg on the answering UA,
and prevents the trailing 481 from the orphan BYE.
Options:
callcounter=no
call-limit=0
busylevel=0
busy_on_active=noBehavior:
- Active, ringing and on-hold counters are tracked.
-
call-limitenforces a hard cap. -
busylevelcan create a softer busy threshold. -
busy_on_activerejects new calls when the account is already active.
CLI:
*CLI> sip show inuse
*CLI> sip show inuse all
AMI:
-
SIPshowpeerreports call usage fields. -
PeerStatusreports call-limit and counter transitions.
Options:
allowsubscribe=yes
subscribecontext=presence
subscribemwi=no
mwiexpiry=3600
mailbox=100@defaultCurrent behavior:
-
message-summary(MWI) SUBSCRIBE is handled. -
presence/dialog/dialog-info(BLF) SUBSCRIBE is handled — see Presence and BLF below. -
allowsubscribe=norejects SUBSCRIBE by policy (global if no peer allows it, or per-peer). -
subscribecontextselects which dialplan-hint context a watcher may subscribe to (presence/BLF). - Unknown event packages are accepted with
202(no NOTIFY).
A peer with a mailbox= reflects that voicemail box's state. subscribemwi
(chan_sip parity, default no) chooses how:
-
subscribemwi=no(default) — unsolicited. The phone does not need to SUBSCRIBE: amessage-summaryNOTIFY is pushed to its registered contact on each successful REGISTER and whenever the mailbox count changes (RFC 3842,Subscription-State: active). -
subscribemwi=yes— SUBSCRIBE-only. MWI is delivered only on an active inbound MWI subscription dialog; nothing is pushed unsolicited. Use this for phones that subscribe for MWI themselves.
A subscribing phone keeps receiving solicited NOTIFYs on its dialog and is not also
pushed unsolicited (no double notification). The unsolicited NOTIFY is sent with
nua_method rather than nua_notify, because this Sofia-SIP build rejects an
out-of-dialog nua_notify locally; the same mechanism backs sip notify.
The options above are the server side — phones subscribe to this PBX. chan_sofia can also be the watcher: a per-peer
mwi_subscribe = 1234@default ; <local-mailbox>[@context]makes chan_sofia send a SUBSCRIBE for Event: message-summary to that trunk and
inject the upstream's Voice-Message: N/M (RFC 3842 / RFC 6665) into the local
MWI cache as 1234@default — so local phones watching that mailbox light up from
an upstream voicemail server. chan_sip had no equivalent.
It starts after the trunk's REGISTER 200 (for register=> trunks) or at load
(static-host peers), is refreshed by the stack on the mwiexpiry interval, honours
outboundproxy (initial Route), and clears the lamp when the upstream terminates
the subscription. A failed initial SUBSCRIBE (server momentarily unreachable) is
retried with backoff; a terminal authentication failure is not retried — fix the
secret and sip reload. This is distinct from the inbound subscribemwi setting.
AMI:
SubscribeRejectedSofiaPresenceState
chan_sofia serves inbound presence/BLF subscriptions so busy-lamp-field keys and
presence indicators track an extension's state — a chan_sip drop-in.
A watcher sends a SUBSCRIBE with Event: dialog (BLF) or Event: presence for
a watched extension. chan_sofia registers a dialplan hint watcher for that
extension (so core show hints counts the watcher), accepts the subscription
with 202 Accepted, and pushes a NOTIFY whenever the watched extension's state
changes:
-
Event: dialog→application/dialog-info+xml(<state>=terminated/early/confirmedfor idle/ringing/in-use). -
Event: presence→application/pidf+xmlorapplication/xpidf+xml, chosen by the inboundAcceptheader (xpidf for older / Polycom-style endpoints).
Requirements and behavior:
- The watched extension must have a dialplan hint, e.g.
exten => 211,hint,SIP/211. With no hint the SUBSCRIBE is answered404. - The hint is looked up in
subscribecontext(else the peer's context, elsedefault). -
allowsubscribegates whether subscriptions are accepted (global + per-peer). - The driver provides a device-state callback for
SIP/<peer>, so a hint overSIP/<peer>reflects offline (unregistered / unreachable), call-limit busy, and on-hold; in-call (in-use/ringing) is derived from the active channel. Registration and qualify transitions trigger a hint re-evaluation so watchers are notified when an endpoint comes online or goes offline. -
Subscription-Stateis sent on every NOTIFY (active;expires=N, andterminated;reason=timeouton teardown), per RFC 6665. - Subscriptions are torn down on an explicit unsubscribe (
Expires: 0), on a re-SUBSCRIBE that replaces a prior one, and — for endpoints that simply vanish — by a periodic expiry sweep that removes subscriptions past their grantedExpires.
Each pushed NOTIFY also emits an AMI SofiaPresenceState event (watcher, watched
extension, previous→new state, dialog state, body format, version) for
monitoring — useful for diagnosing BLF/presence in the field.
The watcher side above lets phones subscribe to this server. The publish
side is the mirror image: chan_sofia can act as an Event Publication Agent (EPA)
and PUBLISH an extension's dialog state out to a central presence server
(an Event State Compositor, e.g. Kamailio's presence module). The central server
then distributes state to watchers across a cluster or federation — so BLF works
beyond a single PBX. chan_sip had no equivalent.
For each peer with publish=yes, chan_sofia publishes the peer's hint state
(application/dialog-info+xml, RFC 3903) to the configured server:
[general]
publish_server = sip:presence.example.com:60000 ; the central ESC (host:port)
publish_domain = example.com ; presentity domain (default: the server host)
publish_expires = 3600 ; refresh interval, seconds (min 30)
publish_format = dialog-info ; dialog-info (RFC 4235, default) or pidf (RFC 3863 presence)
;publish_username = pbxpublisher ; only if the ESC challenges the PUBLISH
;publish_password = s3cr3t
[1001]
type=peer
host=dynamic
regexten=1001
subscribecontext=blf
publish=yes ; publish this peer's hint state
;publish_exten=1001 ; override which exten(s) to publish (default: regexten/name)The published presentity is sip:<regexten>@<publish_domain> and the state comes
from the peer's subscribecontext dialplan hint, so a published peer needs a
regexten, a subscribecontext, and a real hint. The full RFC 3903 lifecycle is
handled: the initial PUBLISH stores the server's SIP-ETag, refreshes and
state-changes carry SIP-If-Match with an incrementing version, 423 Interval Too Brief is honoured, and the publication is removed at shutdown.
The body format is selectable with publish_format: dialog-info (call state,
Event: dialog, the default) or pidf (RFC 3863 presence — open/closed/busy,
Event: presence, application/pidf+xml) for servers that key on a presence
package. Changing it rebuilds the publications on reload. publish_exten overrides
which extension(s) a peer publishes (same ext1&ext2@ctx grammar as regexten)
when the published presentity should differ from the dialplan regexten.
The feature is off until publish_server is set. Initial sends are jittered
and per-presentity sends are serialized, so a large fleet (thousands of peers)
does not overwhelm the central server at startup. A sip reload reconciles
publications live — it adds peers that gained publish=yes, removes (with an
Expires:0 unpublish) peers that dropped it, and rebuilds when the server/domain/
expires settings change — so no restart is needed for PUBLISH config changes.
The published state is taken straight from gabpbx's native dialplan hints: each
publication registers as an extension-state watcher (it appears as a watcher in
core show hints), so the state it publishes always matches the hint — there is
no parallel state system. Any hint works (SIP/<peer>, Custom:, mixed); when
the hint changes, the publication re-publishes.
sip show publications lists the live publications, showing the last published
state vs the current hint state, whether an ETag is held, the refresh interval,
the time to the next send, and the last result code:
Peer Presentity LastState Current ETag Expires NextSend Last Target
1001 sip:1001@example.com Idle InUse yes 3600 3210s 200 sip:1001@example.com:60000
The PUBLISH configuration also appears in sip show settings (the auth password
is shown as <set>, never printed).
chan_sofia carries SIP MESSAGE (RFC 3428, SIP SIMPLE) both in and out of a call.
In-call messages always work: a MESSAGE received on an established call is queued
to the channel as a text frame (so SendText() / ReceiveText-style flows behave as on
chan_sip), and SofiaSendMessage on a live channel sends one in dialog.
Out-of-dialog messages (standalone instant messages, not tied to a call) are
relayed natively between registered users by default (1.4.1,
message_autorelay=yes): the sender is authenticated by digest, the target
extension is resolved through the auto-created hint namespace (the same one BLF
uses), and the message is fanned out to every registered contact of the
target peer — request-URI built per contact, Path and learned NAT source
honored. The sender gets 202 Accepted when at least one contact was reached,
480 Temporarily Unavailable when none. If the target does not resolve to a
local peer, the message falls back to message_context dialplan routing; with
message_autorelay=no, everything routes through message_context as before.
Without either, an out-of-dialog MESSAGE is answered 405 Method Not Allowed.
[general]
message_autorelay = yes ; native user-to-user MESSAGE relay (default yes)
message_context = sipmessages ; fallback context for inbound out-of-dialog MESSAGE
[1001]
type = friend
; message_context = sipmessages ; optional per-peer overrideThe launched dialplan extension can read these channel variables:
SIPMESSAGE_FROM From URI (sip:user@host)
SIPMESSAGE_FROM_DISPLAY From display-name
SIPMESSAGE_TO To URI
SIPMESSAGE_RURI Request-URI
SIPMESSAGE_PEER matched local peer name (empty for a guest sender)
SIPMESSAGE_CONTENT_TYPE message Content-Type
SIPMESSAGE_BODY message text
An unknown sender is accepted only when allowguest=yes; otherwise it gets 403. A
sender is matched to a peer by the From user-part. The accepted response is 202, 404
if the extension does not exist, 405 if messaging is off.
Example dialplan that logs and auto-replies:
[sipmessages]
exten = _X.,1,NoOp(SIP message from ${SIPMESSAGE_FROM}: ${SIPMESSAGE_BODY})
same = n,SofiaSendMessage(${SIPMESSAGE_PEER},Got your message)
same = n,Hangup()Sending out of dialog — from the dialplan or AMI:
; dialplan
exten = _X.,1,SofiaSendMessage(1001,Hello from the PBX)
exten = _X.,1,SofiaSendMessage(sip:1001@example.com,Hello,sip:pbx@example.com); AMI
Action: SofiaMessageSend
To: 1001
From: sip:pbx@example.com
Body: Hello from the PBX
SofiaSendMessage(<peer-or-uri>,<body>[,<from>]) and the SofiaMessageSend AMI action
take either a configured peer name (routed to its registered contact) or a sip:/sips:
URI. The send is best-effort: the request is dispatched and the final response is logged.
The driver has fax and T.38 configuration/display support for migration from
chan_sip.
Options include:
faxdetect=no
t38pt_udptl=no
t38pt_usertpsource=no
t38_maxdatagram=-1Operational status should be checked against configs/sofia.conf.sample and
current source before a production fax deployment. T.38/Fax support is an area
with staged behavior and continuing work.
Peer and channel inspection:
sip show peers
sip show peer <name>
sip show channels
sip show channel <call-id-prefix>
sip show channelstats
sip show inuse [all]
sip show settings
sip show channel <call-id-prefix> prints one channel in detail (peer, username,
From, Request-URI, state, direction, audio/video RTP, SRTP, hold, T.38 and
session-timer), matched by a Call-ID prefix. sip show channelstats prints a
per-channel audio RTP table (tx/rx packets, tx/rx loss, jitter).
sip show peer <name> prints a concise, friendly summary of the peer followed by
its registered contacts:
-
Summary — SIP peer name, Registration state, Endpoint (
user@host:portvia transport), Context / source, Accountcode, Contact slots (used / allowed), Media (codecs, DTMF mode, NAT, directmedia), Calls (active / limit, ringing, on-hold), and Qualify status. - Contacts — the full list of contacts under the peer, each with its State, TTL, Source and User-Agent.
sip show peer <name> detail adds the complete per-field dump — session timers,
identity headers, network & media, limits & features, fax / T.38, timers & RTP,
routing & dialplan, security & ACL, registration, and contacts — for when every
configured field needs to be inspected. The detail keyword tab-completes.
On-demand qualify and NOTIFY (chan_sip parity):
sip qualify peer <name> [load]
sip notify <type> <peer> [<peer>...]
sip qualify peer sends an OPTIONS keep-alive to a peer immediately; the result is
asynchronous (watch sip show peers or the PeerStatus AMI event). load is
accepted for chan_sip compatibility (realtime peers load automatically).
sip notify sends a named NOTIFY built from a sip_notify.conf section to one or
more peers — for example sip notify check-sync 100 to reboot a phone. The section's
lines become SIP headers (Event and Content are special; a
Subscription-State: terminated is added when the type omits one); the file is
parsed exactly like chan_sip (Content=> lines join with CRLF, values are
semicolon-unescaped). Both reuse the AMI SIPqualifypeer / SIPnotify cores.
A compact, chronological per-call trace of SIP signalling (Tx/Rx of requests and
responses, dialog state, hangup) for after-the-fact debugging — chan_sip parity with
practical additions. Off by default (zero overhead); enable with recordhistory=yes
in [general] or at runtime:
sip set history on ; record every call
sip set history on <match> ; record only calls whose source or destination
; contains the substring <match> (busy systems)
sip set history off
sip show history ; list the calls that have history (live + retained)
sip show history <call-id> ; dump one call's history (Call-ID prefix)
sip show history <call-id> verbose ; dump + an analysis block
sip history clear ; discard the retained completed-call histories
Each call keeps a bounded ring of events (sequence, a millisecond delta from call
start, and a first-line summary — never message bodies or Authorization headers).
When a call ends its history is snapshotted into a small global ring so it stays
inspectable after hangup (the main thing plain chan_sip history can't do).
The verbose form adds a deterministic analysis: Outcome (answered / failed /
not answered), Reason (the SIP reason phrase of the final pre-answer failure, or
the real hangup cause for an answered call), a Timeline (post-dial delay, ring,
talk time), the negotiated codecs, and Warnings — so a failed call reports,
for example, 488 not acceptable here — no common codec offered. It reasons only over
recorded facts (no packet captures, no heuristics).
The capture filter (sip set history on <match>) matches a case-insensitive
substring against the call source (the From user / caller-id) and destination (the
To user and the Request-URI user), so on a high-traffic system you can record just
the calls for one number or account — e.g. sip set history on 960800120.
Tab completion of peer names. sip show peer <TAB><TAB> lists the
peers currently held in RAM; typing a prefix first narrows the list by
approximation (sip show peer 2<TAB> offers only the peers whose name
starts with 2). This is chan_sip parity. Completion iterates the
peers container and matches the typed word case-insensitively by prefix,
snapshotting each peer->name under peer->lock so it is safe against a
concurrent sip reload. The same per-peer completion also drives
sip set debug peer <name> and sip prune realtime peer <name>.
Debug:
sip set debug
sip set debug on
sip set debug off
sip set debug peer <name>
sip set debug ip <addr>
sip set debug sdp on
sip set debug sdp off
sip set debug fork on
sip set debug fork off
sip set debug capture <host:port>
sip set debug file <path>
sip set debug sdp {on|off} is a focused SDP/ICE negotiation tracer, off by
default and independent of the general debug. It dumps the generated offer SDP,
the WebRTC offer-gate decision, the incoming answer SDP, and the offerer
answer-apply path — showing the signalling of a WSS/TLS session already
decrypted, which a packet capture on the wire cannot. It is the primary tool
for diagnosing an inbound WebRTC call where GABPBX is the SDP offerer.
Every Sofia: … debug line carries a [from-user|to-user|callid] transaction
tag, so each log line is attributable to a specific SIP transaction when several
calls are in flight.
Beyond sip set debug sdp, chan_sofia can stream a decrypted copy of
every SIP message it sends or receives — over any transport (udp/tcp/tls/
ws/wss), SDP included — to an external capture server, and adds two focused
low-level tracers. All are off by default.
Decrypted SIP capture (HEP / file). Point chan_sofia at a Homer /
sipcapture (HEP) collector, or at a plain file, in [general]:
[general]
sip_capture_address = 127.0.0.1:9060 ; stream every SIP message as HEP
sip_capture_file = /var/log/gabpbx/sip-trace.txt ; or write the same to a fileThe stream carries the messages already decrypted, so a TLS/WSS session that a
wire capture cannot read is visible in full; read the HEP stream with a
HEP-capable viewer, e.g. sngrep -H udp:127.0.0.1:9060. Both can also be set
at runtime:
sip set debug capture <host:port> ; start / redirect the HEP stream
sip set debug file <path> ; write decrypted SIP to a file
Low-level tracers (default off):
rtp set debug ice ; ICE connectivity-check / candidate / DTLS trace (WebRTC media)
sip set debug fork on ; parallel-fork per-leg lifecycle trace
sip set debug fork off
sip set debug dtmf on ; log each received DTMF digit
sip set debug dtmf off
rtp set debug ice traces ICE connectivity checks and DTLS setup, and
sip set debug fork traces the per-leg lifecycle when an outbound INVITE is
forked to several contacts.
sip set debug dtmf on logs a NOTICE line for every DTMF digit chan_sofia
receives on a call — whether it arrives as RFC 2833 / telephone-event, in a SIP
INFO, or inband — to the CLI and the messages log, the way chan_sip did. It is
off by default and is a handy way to confirm IVR keypresses and feature-code entry
without a packet capture:
NOTICE ... sofia_read: Sofia: DTMF '5' received via RFC2833 on SIP/phone100-00000012
Reload:
sip reload
sip reload is a chan_sip-parity alias that re-reads
/etc/gabpbx/sofia.conf and applies changes to peers, trunks and
[general] settings without restarting GABPBX. It is equivalent to
module reload chan_sofia.so and to the reload hook in the module
registry. Active SIP dialogs and RTP streams are not paused.
The reload is executed on the Sofia-SIP NUA event thread itself
(sofia_thread) via the same sofia_dispatch_to_root_thread IPC that
SIPqualifypeer and SIPnotify use. Because the single consumer of
sofia_cfg, the peers container and per-peer state is now blocked
inside the reload worker for the duration of the critical section,
there is no concurrent reader and no need for a global config lock.
The previous "run on the caller's thread" model carried real
use-after-free races on sofia_cfg.localha / sofia_cfg.contact_ha
and peer->chanvars under concurrent SIP traffic; those races are
eliminated by the dispatch model.
Reload behaviour:
-
Serialised against itself: a module-level mutex (
sofia_reload_lock) makes a second concurrent reload returnanother reload is in progressinstead of racing with the first. -
Bounded by a 30-second timeout: if
sofia_threadis busy with a long-running handler the reload waits up to 30 seconds and then returnsreload timed out after 30000 ms (sofia_thread busy). The worker still completes its work safely on the next idle window (refcounted request struct, no use-after-free on the timed-out caller). -
Listener-config changes are refused, not silently accepted. The following fields are baked into the SIP listener at module load and cannot be changed by a reload:
bindaddr, bindport, tlsbindaddr, tlsbindport, tlscertfile, wsbindaddr, wsbindport, wssbindaddr, wssbindport, timert1, timerbEditing any of these and running
sip reloadreturnslistener config changed (<key1>,<key2>,...) —systemctl restart gabpbxrequiredand leaves the livesofia_cfgand the running NUA listener untouched. Apply listener changes by editing the file and runningsystemctl restart gabpbx. -
Mark-and-sweep removes peers no longer in
sofia.conf. Every peer is marked at reload start; surviving peers are unmarked as they are re-parsed; remaining marked (non-realtime) peers are unlinked from the directory and their dialplan hint extension is removed. Realtime peers are exempt because their lifecycle is per-lookup, driven bysofia_find_peer_realtime, independent ofsofia.conf. A removed peer producesSofia: peer '<name>' removed by reload sweep (no longer present in sofia.conf)atLOG_NOTICElevel so the operator sees what was dropped.
module reload chan_sofia.so routes through the same code path and
surfaces the same errors to LOG_WARNING (so they are visible in
/var/log/gabpbx/messages) and to the module manager's return code
(non-zero on failure, which AMI ModuleLoad propagates back to the
caller).
Memory hygiene on reload:
-
Swept peer with
host=hostname.dom: the sweep callback callsast_dnsmgr_releaseand drops the peer ao2 ref thatsofia_dnsmgr_setup_peerbumped for callback safety, so the destructor can actually run whenao2_unlinkdrops the container's ref. Without that handshake the peer + itsast_dnsmgr_entrywould leak permanently on every swept dynamic-DNS peer. -
Existing peer with
permit=/deny=/contactpermit/contactdeny/directmediapermit/directmediadeny: the reset-and-repopulate window freespeer->ha,peer->contacthaandpeer->directmediahabefore the parser re-appends rules. Without the reset, the ACL chains grew linearly with the reload count andast_apply_haslowed accordingly. -
Existing peer with
subscribecontext+regexten: the reset window removes the old dialplan hint extension viaast_context_remove_extensionbefore the new values are written, sosofia_create_peer_hintwrites a fresh entry without risking duplicates.core show hintsshows exactly one hint per reload-stable peer regardless of reload count. -
Global
domain_list: the worker drains it at the top ofsofia_apply_configso removing adomain=line fromsofia.confand reloading actually removes the entry — a stale entry would otherwise be a security regression (deleted domain still accepted as local for inbound INVITE / SUBSCRIBE). -
register =>lines in[general]: parsed via find-or-alloc so re-parsing the sameregister =>user on every reload reuses the existing peer struct instead of stacking duplicate structs under the same name in the peers container (which hashash_fn=NULLand does not natively refuse duplicates by name). -
Existing peer with
mailbox=: the reset-and-repopulate window drainspeer->mailboxes(synchronous unsubscribe of eachast_event_subscribehandle, then free the struct) before the parser re-populates it. Without this drain,sofia_peer_parse_ mailboxeswould AST_LIST_INSERT_TAIL fresh mailbox structs on top of the existing ones every reload, leaking memory and event subscriptions per cycle. -
Swept peer with active outbound REGISTER (
peer->nh) or pending qualify OPTIONS (peer->qualify_nh): the sweep callback destroys both nua_handles synchronously beforeao2_unlink. The sweep callback runs onsofia_thread(invoked by the reload worker which is itself dispatched there), sonua_handle_destroy's same-thread- as-create constraint is satisfied without a dispatch. The peer destructor carries defensive dispatch branches for both handles to catch orphan paths (e.g. realtime cache rebuild viasip prune realtime peer <name>while a register/qualify is in flight). -
nh->hmagicuse-after-free protection: every peer-boundnua_handle_destroyis preceded bynua_handle_bind(nh, NULL). Sofia-sip'snua_handle_bindis synchronous and thread-safe (it just updates a field in the handle struct), and detaches the backpointer from the peer immediately. Without this, the destructor path — which dispatches the destroy intosofia_threadasynchronously — would leave a real UAF window: the destructor finishes, the peer struct is freed, and only THEN does the dispatched destroy execute. Between those two moments sofia-sip could deliver an inbound event (REGISTER response, OPTIONS qualify response, MWI NOTIFY ack, ...) intosofia_event_callbackfor the still-alive handle, and the callback would dereferencenh->hmagic, which still points at the now-freed peer. By clearinghmagicbefore the dispatch, any event arriving in the window readsNULLand is handled by the existingif (hmagic)gates insofia_event_callback. The sweep path is already race- free (synchronous destroy onsofia_thread), but the bind-NULL idiom is applied there too as belt-and-braces. -
Realtime peer cache (
sippeersfamily):sofia_find_peernow holdsao2_lock(peers)across the cache iteration AND the realtime fallback (sofia_find_peer_realtime), so two concurrent cache-miss lookups for the same name cannot both build and link duplicate peers. The lock is held through the realtime DB query (which is brief for the typical sippeers schema); cache hits do not invoke the realtime path so concurrency on hot peers is unaffected. The alternative (optimistic build outside the lock + post-lock duplicate check) was rejected: by the time the duplicate check runs, the loser thread has already registered a dialplan hint, anast_dnsmgr_entrywith a peer ao2 ref-bump, and possibly appended adynamic_exclude_staticentry tosofia_cfg.contact_ha— none of which can be cleanly unwound.
The sipregs overlay (when extconfig.conf wires a separate
sipregs family) must carry ONLY registration-state columns
(ipaddr, port, regseconds, fullcontact, ...). Never
permit/deny, never contactpermit/contactdeny, never directmedia- permit/directmediadeny, never setvar=, never header=. Those
columns belong in sippeers exclusively. The second
sofia_apply_peer_variables call (sipregs overlay) APPENDS those
particular fields onto chains already populated by the sippeers
parse, so putting them in sipregs leaks across cache rebuilds.
Schema-design responsibility; not enforced in code.
On a 1000-reload stress test on an idle PBX the resident set size
grows by ~312 bytes per reload, a constant attributable to gabpbx-core
ast_config_load metadata churn and not to the chan_sofia per-reload
paths. A 200-reload stress test exercising register => + a peer
with regexten + subscribecontext + permit/deny ends with
exactly one hint entry, exactly one register-line peer struct, and
exactly two ACL rules on the test peer.
Realtime cache:
sip prune realtime peer <name>
sip prune realtime peer all
sip prune realtime peer like <pattern>
sip prune realtime all
Blacklist:
sip show blacklist
sip blacklist search <IP>
sip blacklist delete <IP>
sip blacklist clear
chan_sofia registers these chan_sip compatible AMI actions:
SIPpeers
SIPshowpeer
SIPqualifypeer
SIPshowregistry
SIPnotify
Action summary:
-
SIPpeerslists configured peers and reportsPeerEntryevents. -
SIPshowpeerreports detailed peer state. -
SIPqualifypeertriggers an immediate qualify probe. -
SIPshowregistrylists outbound registration peers. -
SIPnotifysends NOTIFY to a peer/contact.
Events emitted by current chan_sofia paths include:
PeerStatus
Registry
Hold
Transfer
ReferProgress
TransferRejected
AuthFailure
SubscribeRejected
LockUserAgentReject
RegisterIntervalRejected
RegextenOnQualifyTransition
DnsManagerUpdate
HintCreated
InsecureInviteBypass
SessionTimerRefresh
T38FaxNegotiation
LockUserAgentReject carries Peer, MatchPolicy (strict-anchor or
prefix-list), LockedUserAgent, Prefixes, AttemptedUserAgent,
RemoteAddr, and ChannelType. The empty Prefixes field signals
strict-anchor mode; a non-empty value reproduces the configured allowlist.
InsecureInviteBypass carries Peer, RemoteAddr, and ChannelType and is
the auditable surface for the insecure=invite auth-bypass path used by
IP-validated SBC trunks and PSTN-DDI peers. The companion per-call verbose
trace (Sofia: INVITE auth bypassed per insecure=invite for peer '<name>' from <addr>) is gated behind sip set debug so production runs silent;
surface it on demand with sip set debug on, scope to a peer with sip set debug peer <name>, or to a source IP with sip set debug ip <addr>. The
companion LOG_NOTICE for the rare force_invite_auth=yes override case
(where global auth policy overrules a per-peer insecure=invite) is left at
default log level — that's a security-relevant configuration transition that
operators should see without enabling debug.
Use AMI events for monitoring registration, transfer behavior, authentication failures, call limits, DNS changes and session timer refreshes.
chan_sofia registers SIP-compatible applications:
SIPDtmfMode
SIPAddHeader
SIPRemoveHeader
These preserve the common dialplan integration points used by existing SIP deployments.
Registered SIP-compatible functions:
SIP_HEADER()
CHECKSIPDOMAIN()
SIPPEER()
SIPCHANINFO()
These functions keep existing SIP dialplans compatible with chan_sofia.
Switch from chan_sip to chan_sofia:
1. Disable chan_sip.so in modules.conf.
2. Enable chan_sofia.so in modules.conf.
3. Move or create peer configuration in sofia.conf or realtime sippeers.
4. Restart GABPBX.
5. Verify with sip show settings and sip show peers.
6. Place test calls and verify RTP in both directions.
Debug a peer:
*CLI> sip set debug peer phone100
*CLI> sip show peer phone100
*CLI> sip show channels
Debug an IP:
*CLI> sip set debug ip 203.0.113.20
Clear a stale realtime peer:
*CLI> sip prune realtime peer phone100
Remove a test IP from blacklist:
*CLI> sip blacklist delete 127.0.0.1
The main GABPBX log (/var/log/gabpbx/messages and the CLI console) prefixes
each line with [caller|called] — the CDR caller-source and dialed
destination — so you can follow who is calling whom without correlating channel
names by hand. With core set verbose 5 a call reads cleanly top to bottom:
NOTICE chan_sofia.c: Sofia REGISTER OK: user='alice' ip=203.0.113.10:5060 useragent='Example SIP Phone'
VERBOSE [alice|1000] Executing [1000@internal:1] Dial("SIP/alice-00000003", "SIP/1000") in new stack
VERBOSE [alice|1000] Called SIP/1000
VERBOSE [alice|1000] SIP/1000-00000004 answered SIP/alice-00000003
VERBOSE [alice|1000] Spawn extension (internal, 1000, 1) exited non-zero on 'SIP/alice-00000003'
The same [caller|called] prefix appears on Goto/Spawn/DTMF and application log
lines, which makes grepping a busy log for one caller or one destination trivial.
Module does not load:
- Check that
chan_sip.sois not loaded. - Check Sofia-SIP library paths.
- Run
ldconfig. - Check
module show like sofia.
Peer cannot register:
- Check
realm,secret,defaultuserand username. - Check digest algorithm support.
- Check blacklist state.
- Check source IP ACLs.
- Check Contact ACLs.
- Check User-Agent lock state.
No audio:
- Check
nat=force_rport,comedia. - Check
externaddrandlocalnet. - Check codec negotiation with
sip set debug. - Check RTP firewall/NAT.
- Disable direct media while troubleshooting.
- Verify both legs negotiated compatible codecs or that transcoding is available.
Transfers fail:
- Check
allowtransfer. - Check REFER signaling.
- Check bridge state.
- Check AMI
TransferRejectedandReferProgress.
SRTP fails:
- Check
res_srtp.so. - Check
encryption. - Check crypto suite/cipher.
- Verify endpoint supports the offered crypto attributes.
The current driver is already broad, but the project keeps a clear distinction between completed runtime behavior and compatibility fields that are parsed, stored or displayed for migration.
Areas to continue:
- Presence and dialog event packages.
- Runtime
subscribecontextdispatch for general SUBSCRIBE routing. - Unsolicited MWI NOTIFY if required by deployments.
- Full outbound hold re-INVITE generation.
- Dynamic outbound Allow header from
disallowed_methods. - Compact SIP header emission if Sofia-SIP exposes a safe native control.
- 3xx redirect behavior for
promiscredir. -
autoframingptime/framesize behavior. - Exact
progressinband=nostate-machine parity. - Peer cache eviction behavior for
rtcachefriends=noandrtautoclear. - Per-peer dynamic T1 from qualify RTT.
- Text RTP QoS behavior.
- Video direct-media after audio paths remain stable.
- Fax/T.38 completion where staged behavior remains.
[phone100]
type=peer
host=dynamic
defaultuser=phone100
secret=change-this-secret
context=internal
transport=udp
disallow=all
allow=opus
allow=alaw
allow=ulaw
dtmfmode=auto
nat=force_rport,comedia
directmedia=no
qualify=yes
qualifyfreq=60
qualifytimeout=3
maxcontacts=3
call-limit=3
allowtransfer=yes
allowsubscribe=yes
encryption=no- Use strong secrets.
- Disable guest calls unless required.
- Set correct
externaddrandlocalnet. - Use
nat=force_rport,comediafor NATed phones. - Keep
directmedia=nountil RTP paths are proven. - Configure call limits for trunks and shared accounts.
- Disable transfers on untrusted trunks.
- Enable TLS/SRTP only after certificates and endpoint support are verified.
- Monitor AMI security and registration events.
- Use
sip show peerto confirm registrations, contacts, codecs, NAT and call counters. - Restart GABPBX after changing active SIP channel driver.
SIP — chan_sofia
Subsystems
Realtime & data
Operations