Skip to content

πŸš€ Part 11: Building a Carrier-Grade Class 4/5 Softswitch Platform (Ring2All PBX + Ring2All SBC + Ring2All BSS)

12 min readUpdated: Sep 26, 2026
View as Markdown

Welcome to the eleventh installment of our β€œDebian 13 Clustering & Distribution” series. In the previous parts, we designed database clusters, replicated storage networks, packaged custom packages, and built out individual SBC and telephony nodes. In this guide, we will step back and explore the architectural patterns required to combine Telephony Server and Kamailio into a cohesive, high-performance Class 4/5 softswitch platform. We will focus on how the separation of concerns between signaling and media ensures carrier-grade scalability, how routing and registration flow across the system, and how Ring2All PBX, Ring2All SBC, and Ring2All BSS work together as a unified control plane to abstract the complex configuration details.


πŸ—οΈ The Architectural Pattern: Separation of Concerns

Section titled β€œπŸ—οΈ The Architectural Pattern: Separation of Concerns”

When building telecommunications systems, a common anti-pattern is overloading a single application. While Telephony Server is an incredibly powerful Back-to-Back User Agent (B2BUA) with unrivaled media capabilities, using it directly on the public edge as a registrar for tens of thousands of endpoints, or attempting to use it for high-volume least-cost-routing (LCR), can lead to CPU exhaustion, memory leaks, and registration storms.

Conversely, Kamailio is a state-of-the-art SIP proxy designed to handle SIP transactions at lightning speeds. It operates statelessly or transactionally with a tiny memory footprint, making it the perfect engine for edge security, NAT traversal, dynamic load balancing, and high-throughput routing (Class 4 transit). However, it is not designed to execute business logic like IVRs, call centers, or voicemail, nor does it process media.

By combining the two, we implement a classic layered architecture:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ PUBLIC INTERNET / PSTN β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β–²
SIP Trafficβ”‚ β”‚SIP Traffic
β–Ό β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ EDGE LAYER: Ring2All SBC β”‚
β”‚ (Kamailio 6.x + RTPEngine) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β–²
Internal SIP β”‚ β”‚Internal SIP
(via wg0 / β”‚ β”‚(via wg0 /
Private IP) β–Ό β”‚ Private IP)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CORE APPLICATION LAYER: Ring2All PBX β”‚
β”‚ (Telephony Server Telephony) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β–²
ODBC/SQL β”‚ β”‚Dynamic Directory
& CDRs β–Ό β”‚& Dialplan (LUA)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ DATABASE STATE LAYER β”‚
β”‚ (Patroni PostgreSQL 17 Cluster) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  1. The Edge Layer (Ring2All SBC): Kamailio acts as the primary contact point for SIP endpoints and wholesale carriers. It handles security (anti-flood, rate-limiting, IP ACLs), parses incoming SIP packets, offloads TLS termination, proxies media using Sipwise RTPEngine, and load balances signaling to the backend.
  2. The Core Layer (Ring2All PBX Telephony): Telephony nodes process media, execute business logic (IVRs, routing profiles, queues, recordings), and write CDRs. Telephony Server is protected from the public internet, only communicating with Kamailio and the database cluster.
  3. The Database Layer (Patroni PostgreSQL): Holds the unified state. Both layers query the database for dynamic routing attributes, subscriber authentication credentials, and dispatching parameters.

πŸ› οΈ Deep Dive 1: Edge Load Balancing and Dynamic Dispatching

Section titled β€œπŸ› οΈ Deep Dive 1: Edge Load Balancing and Dynamic Dispatching”

To achieve horizontal scalability, we must load balance SIP requests across multiple Telephony nodes. Kamailio achieves this using its dispatcher module.

In a traditional setup, the dispatcher list is hardcoded in a text file. At scale, this is a maintenance bottleneck. In the Ring2All SBC + Ring2All PBX architecture, Kamailio queries the dispatcher table directly from the shared PostgreSQL database. This allows administrators to dynamically add, remove, or change the state of Telephony nodes via the Ring2All SBC REST API or Admin Portal.

Here is the exact routing configuration block used in the Ring2All SBC kamailio.cfg script to load balance requests dynamically based on the incoming domain name:

# ── Load Balancer towards PBX β€” Multi-Domain DB-driven (Class 5 Proxy) ────────
route[DISPATCH] {
# Dynamic DB query to fetch the dispatch set assigned to this domain
sql_query("kam", "SELECT value FROM domain_attrs WHERE name='dispatch_set' AND did=(SELECT id FROM domain WHERE domain='$rd')", "res");
if ($dbr(res=>rows) > 0) {
$var(dispatch_set) = (int)$dbr(res=>[0,0]);
sql_result_free("res");
} else if ($rd == "192.168.10.32") {
$var(dispatch_set) = 1; # Direct IP fallback
sql_result_free("res");
} else {
sql_result_free("res");
xlog("L_WARN", "DISPATCH: Unknown domain $rd with no dispatch_set assigned β€” rejecting\n");
sl_send_reply("404", "Unknown Domain");
exit;
}
# Algorithm 4: Round-robin load balancing with automatic failover support
if (!ds_select_dst($var(dispatch_set), 4)) {
xlog("L_ERR", "DISPATCH: No active destinations available in set $var(dispatch_set)\n");
sl_send_reply("503", "No Destinations Available");
exit;
}
xlog("L_DBG", "Dispatching $rm from $rd -> set $var(dispatch_set) -> $du\n");
t_on_failure("FAIL_DISPATCH");
route(RELAY);
}
# ── Dispatcher Failover Route ──────────────────────────────────
failure_route[FAIL_DISPATCH] {
if (t_is_canceled()) exit;
# If the selected node responds with a 503 Service Unavailable,
# route the transaction to the next available node in the dispatcher set
if (t_check_status("503")) {
if (ds_next_dst()) {
t_on_failure("FAIL_DISPATCH");
route(RELAY);
exit;
}
}
t_reply("503", "All telephony destinations failed");
}
  • Zero-Downtime Scaling: Adding a new Telephony node is as simple as inserting a record into the dispatcher database table. Kamailio handles failovers transparently using ds_next_dst() if a node goes down mid-transaction.
  • Tenant Isolation: By querying domain_attrs, Ring2All SBC can route traffic for customer A.com to Telephony cluster set 10, and customer B.com to Telephony cluster set 20, ensuring strict multi-tenant hardware partition paths.

πŸ“ž Deep Dive 2: Multi-Tenant SIP Registrations and Path Routing

Section titled β€œπŸ“ž Deep Dive 2: Multi-Tenant SIP Registrations and Path Routing”

Managing subscriber registrations in a distributed cluster presents a challenge: how does an incoming call find where a user is currently registered if they are balanced across multiple Telephony nodes?

We solve this using Kamailio’s path module. When a SIP endpoint registers through Ring2All SBC, the SBC intercepts the REGISTER request, appends a Path header containing its own public IP, and forwards the packet to the core Telephony cluster.

# ── SIP Registrar (Path Proxy) ───────────────────────────
route[REGISTRAR] {
# Append the Path header so Telephony Server knows how to route subsequent requests
add_path_received();
# Forward the REGISTER request to the core PBX telephony cluster
route(DISPATCH);
exit;
}
  1. The SIP endpoint sends a REGISTER packet to the Ring2All SBC.
  2. Ring2All SBC inserts: Path: <sip:MY_IP;lr;received=sip:CLIENT_IP:CLIENT_PORT>
  3. The register transaction is balanced to Telephony Node 2.
  4. Telephony Server authenticates the subscriber and writes the registration contact details to the shared database, appending the Path header information.
  5. When a call arrives for that subscriber, Telephony Server looks up the registration database, reads the Path header, and formats the outbound Route header pointing to the Ring2All SBC instance.
  6. The outbound call is routed directly to the specific SBC that maintains the active TCP/UDP connection state with the phone.

This architecture enables an active-active horizontal registration tier, where endpoints can register to any SBC, and Telephony nodes can reach them regardless of where they landed.


πŸ”„ Deep Dive 3: Media Anchoring and NAT Traversal with RTPEngine

Section titled β€œπŸ”„ Deep Dive 3: Media Anchoring and NAT Traversal with RTPEngine”

In VoIP deployments, NAT (Network Address Translation) is the ultimate enemy. Endpoints behind home routers or strict firewalls advertise private IP addresses in their Session Description Protocol (SDP) payloads. If a backend Telephony node tries to send RTP packets directly to these private IPs, the media will be dropped, resulting in β€œone-way audio” or β€œno-audio” calls.

We solve this at the edge by combining Kamailio with Sipwise RTPEngine. When an incoming INVITE request or SIP reply passes through Kamailio, the proxy engages RTPEngine to inspect and rewrite the SDP.

Here is the exact routing logic used inside the SBC’s Kamailio configuration to handle media proxying:

# ── Media Proxy Routing via RTPEngine ────────────────────────
route[PROXY_MEDIA] {
# Only manage media if the SIP packet contains an SDP body
if (has_body("application/sdp")) {
# Check if the transaction is coming from our trusted PBX nodes
if (check_source_address("0")) {
# Outbound calls (PBX -> PSTN/Carrier):
# 'replace-origin' rewrites the session owner IP in SDP to the SBC IP.
# 'replace-session-connection' rewrites the connection line (c=) to the SBC IP.
# 'trust-address' forces RTPEngine to learn the media destination port from received packets.
rtpengine_offer("replace-origin replace-session-connection trust-address");
} else {
# Inbound calls or Registrations (External -> PBX):
# Tell RTPEngine to listen and prepare to anchor the media.
rtpengine_manage("replace-origin replace-session-connection trust-address");
}
}
}
  1. An external softphone sends an INVITE with SDP: c=IN IP4 192.168.1.100 (private address).
  2. Kamailio intercepts the packet and calls rtpengine_manage().
  3. RTPEngine allocates two UDP ports on its public interface (e.g., ports 30002 and 30003) to act as the media relay bridge.
  4. RTPEngine modifies the SDP: c=IN IP4 <SBC_PUBLIC_IP> and m=audio 30002 RTP/AVP ....
  5. Kamailio relays the modified INVITE to the backend Telephony node.
  6. When Telephony Server replies with its own SDP, Kamailio routes the response through rtpengine_manage() again. RTPEngine bridges the two streams:
    • Leg A (External Endpoint <-> RTPEngine Public IP)
    • Leg B (RTPEngine Private IP <-> Telephony Server Private IP)

This ensures topology hiding (internal network IPs are never exposed to the public) and guarantees media delivery regardless of strict symmetric NAT environments.


πŸŽ›οΈ Deep Dive 4: Dynamic Configuration & Telephony Server XML Provisioning via LUA & DB

Section titled β€œπŸŽ›οΈ Deep Dive 4: Dynamic Configuration & Telephony Server XML Provisioning via LUA & DB”

One of Telephony Server’s most powerful architectures is the XML Search Binding. By default, Telephony Server parses static XML files located in /etc/freeswitch/ to search for user directories, dialplans, and gateway configs. However, reloading a static directory file for every new client setup is highly inefficient and does not scale in a multi-tenant environment.

In the Ring2All architecture, Telephony Server binds its directory and dialplan engines directly to an internal LUA script that connects to the clustered PostgreSQL backend.

When Telephony Server needs to authenticate a user (e.g., 1001@company.com) or route an extension, it triggers an event. The LUA script captures this request, queries the database, and returns a dynamically constructed XML response.

Here is the conceptual implementation of the Telephony Server directory binding script:

-- /usr/share/freeswitch/scripts/app/xml_directory_handler.lua
local dbh = freeswitch.Dbh("pgsql://host=127.0.0.1 port=5000 dbname=ss_telephony user=ss_db_user password=PASSWORD")
local req_domain = params:getHeader("domain")
local req_user = params:getHeader("user")
if dbh:connected() then
local query = string.format([[
SELECT password, voicemail_pin
FROM extensions
WHERE number = '%s' AND domain_name = '%s' AND enabled = TRUE
]], req_user, req_domain)
dbh:query(query, function(row)
-- Construct the XML structure expected by the Telephony core
XML_STRING = string.format([[
<?xml version="1.0" encoding="UTF-8"?>
<document type="freeswitch/xml">
<section name="directory">
<domain name="%s">
<params>
<param name="dial-string" value="{presence_id=${dialed_user}@${dialed_domain}}${sofia_contact(${dialed_user}@${dialed_domain})}"/>
</params>
<users>
<user id="%s">
<params>
<param name="password" value="%s"/>
</params>
<variables>
<param name="voicemail_pin" value="%s"/>
<param name="user_context" value="default"/>
</variables>
</user>
</users>
</domain>
</section>
</document>
]], req_domain, req_user, row.password, row.voicemail_pin)
end)
dbh:release()
end
  • Zero Disk I/O: Telephony Server does not read the filesystem during call setups, resulting in extremely low call latency.
  • Instant Provisioning: The moment a user is created via the Ring2All API, the extension is instantly active and able to register, without needing to run reloadxml.
  • Database Resiliency: By routing the DSN connection through the local HAProxy loopback on port 5000, the LUA script is automatically protected against database leader node failures.

πŸŽ›οΈ Configuration Management: Bridging the Dev-to-Ops Gap

Section titled β€œπŸŽ›οΈ Configuration Management: Bridging the Dev-to-Ops Gap”

While standardizing on Kamailio + Telephony Server is a proven architectural pattern, managing these platforms introduces significant operational friction:

  • Writing thousands of lines of raw, complex kamailio.cfg routing loops.
  • Maintaining static Telephony Server XML configurations (directory/*.xml, dialplan/*.xml).
  • Manually reloading modules or calling RPC commands (kamcmd, fs_cli) across multiple servers during updates.

This is where Ring2All PBX, Ring2All SBC, and Ring2All BSS bridge the gap.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ADMIN FRONTEND & REST API β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚
Provisioning APIs β”‚ β”‚ RPC / DB Commands
β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Ring2All PBX β”‚ β”‚ Ring2All SBC β”‚
β”‚ (Class 5) β”‚ β”‚ (SBC) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚
LUA / XML β”‚ β”‚ SQL / RPC
β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Telephony β”‚ β”‚ Kamailio β”‚
β”‚ Telephony β”‚ β”‚ SIP Edge β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Ring2All SBC packages a REST API (built on Node.js/Fastify) and a modern React single-page application (SPA). Instead of editing configuration files manually, developers can manage their SBC cluster programmatically or visually:

  • Dynamic Dispatchers: Group and manage Telephony nodes, monitor their real-time state, and change load balancing algorithms on-the-fly.
  • IP ACLs & Firewalling: Configure permitted CIDR blocks (whitelisting carriers) and restrict unwanted traffic. Ring2All SBC interacts directly with Kamailio’s permissions module database tables.
  • Subscribers & Domains: CRUD operations for multi-tenant SIP domains and subscriber authentication details.
  • Kamailio RPC Gateway: White-listed RPC routing that allows administrators to call commands like dispatcher.reload or inspect active registrations via HTTP requests securely.

2. Ring2All PBX Telephony Management (Class 5 Control Plane)

Section titled β€œ2. Ring2All PBX Telephony Management (Class 5 Control Plane)”

Ring2All PBX abstracts the complex configuration of Telephony nodes:

  • Dynamic XML Handler (LUA + Database): Instead of reading large, flat XML files, Telephony Server directories, dialplans, and configurations are resolved dynamically. When a call triggers a dialplan lookup, Telephony Server calls custom Ring2All PBX LUA scripts that query the Patroni PostgreSQL cluster to retrieve routing rules in real-time.
  • Distributed Storage Mounting: Audio assets, voicemail, and recordings are automatically synchronized using GlusterFS active-active replicated clusters, making media files available instantly to all Telephony nodes.

For developers building high-availability, carrier-grade telecommunications applications:

  1. Decouple Signaling and Media: Place a lightweight SIP proxy (Kamailio) at the edge, and push heavy media transactions to a protected pool of backend application nodes (Telephony Server).
  2. Utilize Database-Driven Configurations: Avoid static file configurations. Leverage SQL databases for routing records, allowing dynamic additions of trunks, dialplans, and server nodes.
  3. Build API-Driven Interfaces: Encapsulate command-line tools and low-level module configurations behind clean REST APIs (like Fastify) to streamline operations and support modern web portals.

This wraps up Part 11 of our series. You now have the complete architectural blueprint for designing, deploying, and managing a robust Class 4/5 distributed softswitch platform.