π Part 11: Building a Carrier-Grade Class 4/5 Softswitch Platform (Ring2All PBX + Ring2All SBC + Ring2All BSS)
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) β ββββββββββββββββββββββββββββββββββββββββββββ- 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.
- 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.
- 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");}Why this is a win for developers:
Section titled βWhy this is a win for developers:β- Zero-Downtime Scaling: Adding a new Telephony node is as simple as inserting a record into the
dispatcherdatabase table. Kamailio handles failovers transparently usingds_next_dst()if a node goes down mid-transaction. - Tenant Isolation: By querying
domain_attrs, Ring2All SBC can route traffic for customerA.comto Telephony cluster set10, and customerB.comto Telephony cluster set20, 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;}The Magic of the Path Header:
Section titled βThe Magic of the Path Header:β- The SIP endpoint sends a
REGISTERpacket to the Ring2All SBC. - Ring2All SBC inserts:
Path: <sip:MY_IP;lr;received=sip:CLIENT_IP:CLIENT_PORT> - The register transaction is balanced to Telephony Node 2.
- Telephony Server authenticates the subscriber and writes the registration contact details to the shared database, appending the
Pathheader information. - When a call arrives for that subscriber, Telephony Server looks up the registration database, reads the
Pathheader, and formats the outboundRouteheader pointing to the Ring2All SBC instance. - 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"); } }}How RTPEngine rewrites SDP on the fly:
Section titled βHow RTPEngine rewrites SDP on the fly:β- An external softphone sends an
INVITEwith SDP:c=IN IP4 192.168.1.100(private address). - Kamailio intercepts the packet and calls
rtpengine_manage(). - RTPEngine allocates two UDP ports on its public interface (e.g., ports
30002and30003) to act as the media relay bridge. - RTPEngine modifies the SDP:
c=IN IP4 <SBC_PUBLIC_IP>andm=audio 30002 RTP/AVP .... - Kamailio relays the modified
INVITEto the backend Telephony node. - 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.
The Search Binding Workflow:
Section titled βThe Search Binding Workflow:β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.lualocal 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()endThe Benefits of Search Bindings:
Section titled βThe Benefits of Search Bindings:β- 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.cfgrouting 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 β ββββββββββββββββ ββββββββββββββββ1. Ring2All SBC Management (Class 4 Control Plane)
Section titled β1. Ring2All SBC Management (Class 4 Control Plane)β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
permissionsmodule 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.reloador 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.
π€ Key Architectural Takeaways
Section titled βπ€ Key Architectural TakeawaysβFor developers building high-availability, carrier-grade telecommunications applications:
- 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).
- Utilize Database-Driven Configurations: Avoid static file configurations. Leverage SQL databases for routing records, allowing dynamic additions of trunks, dialplans, and server nodes.
- 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.

