← Back to list

How to Build a VoIP Stack with OpenSIPS, Asterisk, and MariaDB on Ubuntu

Building a production-ready VoIP infrastructure requires a clear separation between signaling and media. While containerization is popular…

Rehman Ahmad Dar · 2026-03-04 22:52 · 0 claps · 3.9 min read
#voip #telephony #ip-telephony #voip-pbx #asterisk
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🔓 · Open Source

How to Build a VoIP Stack with OpenSIPS, Asterisk, and MariaDB on Ubuntu

Building a production-ready VoIP infrastructure requires a clear separation between signaling and media. While containerization is popular, a bare-metal deployment on AWS EC2 provides the direct hardware access and networking control necessary for low-latency real-time communication. This guide outlines how to deploy a stack using OpenSIPS 3.5.9 as the SIP proxy and Asterisk 20.6.0 as the media engine (B2BUA).

VoIP Setup

VoIP Setup

The Architecture

  • Signaling Proxy (OpenSIPS 3.5.9): Acts as the entry point on port 5060 (UDP/TCP). It manages SIP registrations, digest authentication via MariaDB, and NAT detection.
  • Media Engine (Asterisk 20.6.0): Functions as a Back-to-Back User Agent (B2BUA) on port 5080. It handles the dialplan, codec negotiation, and RTP relay.
  • Database Backend (MariaDB 10.11.14): A centralized voipdb stores subscriber credentials and real-time location data.
  • Network Strategy: Uses SDP rewriting to map internal private IPs to the AWS Public IP, ensuring seamless audio flow through the EC2 NAT.

Step 1: MariaDB Configuration & Schema

First, establish the database layer to manage users and locations.

-- 1. Create the database and dedicated users
CREATE DATABASE voipdb;
GRANT ALL PRIVILEGES ON voipdb.* TO 'opensips'@'localhost' IDENTIFIED BY 'opensipsrw';
GRANT ALL PRIVILEGES ON voipdb.* TO 'asterisk'@'localhost' IDENTIFIED BY 'asteriskrw';
FLUSH PRIVILEGES;

USE voipdb;

-- 2. Subscriber Table: Stores SIP credentials and Auth Hashes
CREATE TABLE subscriber (
    id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(64) DEFAULT '',
    domain VARCHAR(64) DEFAULT '',
    password VARCHAR(64) DEFAULT '',
    ha1 VARCHAR(64) DEFAULT '',
    UNIQUE KEY account_idx (username, domain)
);

-- 3. Location Table: Manages active SIP registrations (Memory + DB)
CREATE TABLE location (
    id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(64) DEFAULT '',
    domain VARCHAR(64) DEFAULT '',
    contact VARCHAR(512) DEFAULT '',
    received VARCHAR(512) DEFAULT NULL,
    path VARCHAR(512) DEFAULT NULL,
    expires DATETIME DEFAULT NULL,
    q FLOAT(10,2) DEFAULT 1.0,
    callid VARCHAR(255) DEFAULT '',
    cseq INT(11) DEFAULT 0,
    last_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    flags INT(11) DEFAULT 0,
    cflags INT(11) DEFAULT 0,
    user_agent VARCHAR(255) DEFAULT '',
    socket VARCHAR(64) DEFAULT '',
    methods INT(11) DEFAULT NULL,
    instance VARCHAR(255) DEFAULT NULL,
    kv_store TEXT DEFAULT NULL
);

-- 4. Domain Table: Defines the local domains served by the proxy
CREATE TABLE domain (
    id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    domain VARCHAR(64) DEFAULT '',
    last_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY domain_idx (domain)
);

-- 5. Add a test user (Extension 1001 / Password: password123)
-- Note: 'ha1' is md5(username:domain:password)
INSERT INTO subscriber (username, domain, password, ha1) 
VALUES ('1001', 'YOUR_PUBLIC_IP', 'password123', MD5('1001:YOUR_PUBLIC_IP:password123'));

Step 2: OpenSIPS Configuration (opensips.cfg)

OpenSIPS is configured to listen on both UDP and TCP. It handles the “heavy lifting” of NAT and relays calls to Asterisk for media processing.

####### Global Parameters #######
log_level=3
log_stderror=no
log_facility=LOG_LOCAL0

children=4

/* Bind to Private IP but advertise Public IP for SIP headers */
listen=udp:172.31.10.208:5060 as 52.43.226.78:5060
listen=tcp:172.31.10.208:5060 as 52.43.226.78:5060

####### Modules Section #######
mpath="/usr/lib/x86_64-linux-gnu/opensips/modules/"

loadmodule "db_mysql.so"
loadmodule "signaling.so"
loadmodule "sl.so"
loadmodule "tm.so"
loadmodule "rr.so"
loadmodule "maxfwd.so"
loadmodule "usrloc.so"
loadmodule "registrar.so"
loadmodule "auth.so"
loadmodule "auth_db.so"
loadmodule "nathelper.so"
loadmodule "proto_udp.so"
loadmodule "proto_tcp.so"

# ---- database params ----
modparam("usrloc", "db_url", "mysql://opensips:opensipsrw@localhost/voipdb")
modparam("usrloc", "db_mode", 2) # Write-Back mode for performance

modparam("auth_db", "db_url", "mysql://opensips:opensipsrw@localhost/voipdb")
modparam("auth_db", "calculate_ha1", 0) # Uses pre-calculated HA1 from DB

# ---- nathelper params ----
modparam("nathelper", "natping_interval", 30)
modparam("registrar", "tcp_persistent_flag", "TCP_PERSIST_FLAG")

####### Routing Logic #######
route {
    if (!mf_process_maxfwd_header("10")) {
        sl_send_reply("483","Too Many Hops");
        exit;
    }

    if (has_totag()) {
        # Sequential request handling
        if (loose_route()) {
            route(relay);
        } else {
            if ( is_method("ACK") ) {
                if ( t_check_trans() ) {
                    route(relay);
                    exit;
                } else {
                    exit;
                }
            }
            sl_send_reply("404","Not Found");
        }
        exit;
    }

    # Handle CANCEL
    if (is_method("CANCEL")) {
        if (t_check_trans())
            t_relay();
        exit;
    }

    # Record Routing for signaling continuity
    record_route();

    # --- Registration Logic ---
    if (is_method("REGISTER")) {
        if (!www_authorize("", "subscriber")) {
            www_challenge("", "0");
            exit;
        }
        if (!save("location")) {
            sl_reply_error();
        }
        exit;
    }

    # --- Inbound Invite Logic ---
    if (is_method("INVITE")) {
        # If it's not coming from our local Asterisk, authenticate it
        if ($si != "127.0.0.1") {
            if (!www_authorize("", "subscriber")) {
                www_challenge("", "0");
                exit;
            }
        }

        # Route to Asterisk for B2BUA/Media Handling
        # Asterisk listens on 5080
        if ($si != "127.0.0.1") {
            sethostport("127.0.0.1:5080");
        } else {
            # If coming BACK from Asterisk, look up the destination
            if (!lookup("location")) {
                sl_send_reply("404", "Not Found");
                exit;
            }
        }
        route(relay);
    }
}

route[relay] {
    if (!t_relay()) {
        sl_reply_error();
    }
    exit;
}

Step 3: Asterisk Configuration

Asterisk must be set to port 5080 to avoid conflicts and configured with PJSIP.

  1. Disable Legacy Drivers (modules.conf):
[modules]
autoload=yes
; Disable the old SIP stack to avoid port conflicts
noload => chan_sip.so
  1. PJSIP Trunk and Extensions (pjsip.conf):
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5080
; Crucial for AWS: Rewrite SDP to use Public IP
external_media_address=52.43.226.78
external_signaling_address=52.43.226.78

[transport-tcp]
type=transport
protocol=tcp
bind=0.0.0.0:5080
external_media_address=52.43.226.78
external_signaling_address=52.43.226.78

; --- OpenSIPS Trunk ---
; This allows Asterisk to receive calls from and send calls back to OpenSIPS
[opensips-trunk]
type=endpoint
context=from-opensips
disallow=all
allow=ulaw,alaw
rtp_symmetric=yes
force_rport=yes
rewrite_contact=yes
direct_media=no
identify_by=ip

[opensips-identify]
type=identify
endpoint=opensips-trunk
match=127.0.0.1

[opensips-aor]
type=aor
contact=sip:127.0.0.1:5060
  1. Dialplan (extensions.conf):
[general]
static=yes
writeprotect=no

[from-opensips]
; --- Testing Extensions ---

; Echo Test (Dial 9999)
exten => 9999,1,NoOp(Starting Echo Test for ${CALLERID(all)})
 same => n,Answer()
 same => n,Playback(demo-echotest)
 same => n,Echo()
 same => n,Playback(demo-echodone)
 same => n,Hangup()

; Music on Hold Test (Dial 1234)
exten => 1234,1,NoOp(Starting Music on Hold for ${CALLERID(all)})
 same => n,Answer()
 same => n,MusicOnHold()
 same => n,Hangup()

; --- Routing Extensions ---

; Route all 4-digit extensions (1001, 1002, etc.) back to OpenSIPS
exten => _XXXX,1,NoOp(Routing call to Extension ${EXTEN} via OpenSIPS)
 same => n,Dial(PJSIP/${EXTEN}@opensips-trunk)
 same => n,Hangup()

Step 4: Final Deployment & Testing

  1. Security Groups: Ensure AWS allows 5060 (UDP/TCP), 5080 (UDP/TCP), and 10000–20000 (UDP).

2. Service Restart:

  • systemctl restart mysql
  • opensips -c (check syntax) then systemctl restart opensips
  • asterisk -rx "core reload"

3. Verification:

  • Register two extensions (e.g., 1001 and 1002) using a softphone like Linphone.
  • Dial 9999 to verify two-way audio (Echo Test).
  • Dial extension to extension to verify the B2BUA path.
  • OpenSIPS performs a lookup("location") and delivers the call to Phone B.

메타데이터
post_id
abe91a1dab4a
slug
how-to-build-a-voip-stack-with-opensips-asterisk-and-mariadb-on-ubuntu-abe91a1dab4a
url
https://medium.com/@rehmand110/how-to-build-a-voip-stack-with-opensips-asterisk-and-mariadb-on-ubuntu-abe91a1dab4a
canonical_url
https://medium.com/@rehmand110/how-to-build-a-voip-stack-with-opensips-asterisk-and-mariadb-on-ubuntu-abe91a1dab4a
author_url
https://medium.com/@rehmand110
status
ok
fetched_at
2026-07-10 05:19:05