I wanted to actually understand SIP, not just read about it. So I set myself a hands-on goal: run my own SIP server in the cloud and make a real voice call between my phone and my laptop, with the audio flowing through that server. This post is the full story, including the dead ends I hit, because the mistakes taught me more than the happy path did.

What you'll have at the end: one small AWS EC2 instance running Kamailio (the SIP server) and rtpengine (the audio relay), two softphones registered to it, and a working two-way call.

What SIP actually is (the mental model I needed)

Before touching anything, here's the model that made it click for me.

SIP (Session Initiation Protocol) is just the "who's calling whom and are they picking up" part of a call. It's signaling. It does not carry your voice. Think of it like the ringing and the "hello?" negotiation.

The actual audio travels over a separate protocol called RTP on a different set of ports. This split is the single most important thing to understand, because it's the source of 90% of the problems people hit:

  • SIP → port 5060, sets up and tears down the call
  • RTP → a range of ports (I used 10000–20000), carries the voice

There are two roles in the system:

  • A server (the registrar/proxy) that knows where everyone is and routes calls. I used Kamailio.
  • Clients (softphones) that have a mic and speaker. These live on devices with audio hardware — my phone and my laptop. Not on the server. A cloud server has no microphone, so a softphone running on it couldn't capture my voice. That realization saved me from a wrong turn early on.
 My phone EC2 (one instance) My laptop [softphone 1001] <--SIP--> [Kamailio + rtpengine] <--SIP--> [softphone 1002] mic/spk <------ RTP audio relayed through EC2 ------> mic/spk 

Why I moved from WSL to AWS

My first attempt was on my Windows laptop using WSL (Ubuntu). It technically works, but WSL2 puts Linux behind a NAT'd virtual network with a 172.x address, so reaching the SIP server from another device meant fighting with mirrored networking and port forwarding. On top of that, Ubuntu 20.04's Kamailio control tool had an old Python 2 vs 3 bug that broke user creation.

A disposable EC2 instance sidestepped all of it: a clean public IP, no WSL networking quirks, and a newer Ubuntu with a working toolchain. And when I'm done, I just terminate it.

Lesson learned: for anything involving inbound network traffic, a cloud VM with a real public IP is far less fiddly than WSL.

Step 1 — Launch the EC2 instance

I launched a small Ubuntu 24.04 instance (a t3.small is plenty for a lab).

  • Public IP (what the softphones connect to): 52.91.109.231
  • Private IP (what the instance sees on itself): 172.31.42.238

That public/private split is the NAT situation I'd have to handle later. Keep both numbers handy; everything references them.

Here's my instance in the EC2 console. Note the public IPv4 address — that's the one every softphone will connect to.

EC2 console showing the running Ubuntu instance and its public IP

Step 2 — Open the ports (AWS Security Group)

This is where cloud SIP most often "silently fails." The firewall is the Security Group, and SIP needs more than one port family open. My inbound rules:

Type Protocol Port range Source Why
All UDP UDP 0–65535 0.0.0.0/0 covers SIP 5060 and the RTP media range in one rule
Custom TCP TCP 5060 0.0.0.0/0 SIP fallback if a network blocks UDP
SSH TCP 22 my IP so I can log in

The gotcha: if you only open 5060 and forget the RTP range, calls will connect and ring, but you'll hear silence. Opening All UDP is the lazy-but-reliable lab choice.

Security note: All UDP open to the whole internet means SIP scanners will start probing within minutes. Fine for a short-lived lab — just terminate the instance when done. Don't leave it running for days.

My inbound rules. The All-UDP rule is the important one — it covers both SIP signaling (5060) and the RTP media ports in a single line, which is what lets the audio through.

AWS Security Group inbound rules opening UDP, TCP 5060, and SSH

Step 3 — Install Kamailio and rtpengine

sudo apt update sudo apt install -y kamailio kamailio-extra-modules sqlite3 sudo apt install -y rtpengine 

Ubuntu 24.04 gave me Kamailio 5.7.4 and rtpengine 11.5.1 straight from the repos — no building from source. rtpengine is the piece that makes audio work across NAT: it relays the RTP stream and rewrites the media addresses so each side sends voice to a reachable IP.

Confirming both packages installed cleanly. Ubuntu 24.04 ships recent-enough versions that I didn't have to compile anything.

Terminal showing Kamailio 5.7.4 and rtpengine 11.5.1 version output

Step 4 — Configure rtpengine (the NAT fix)

The whole audio problem comes down to one line in /etc/rtpengine/rtpengine.conf:

interface = 172.31.42.238!52.91.109.231 port-min = 10000 port-max = 20000 

That interface = PRIVATE!PUBLIC syntax is the magic. rtpengine binds on the private IP the OS actually has (172.31.x), but advertises the public IP (52.91.x) in the call setup, so remote phones know where to send audio. Without this, you get one-way or no audio every time.

Then:

sudo systemctl restart rtpengine-daemon sudo systemctl status rtpengine-daemon 

rtpengine up and running. "active (running)" is what you want to see here — if it's not, the audio relay is dead and calls will be silent.

systemctl status showing rtpengine-daemon active and running

Step 5 — Point the control tool at the database

I used DBTEXT, a flat-file database. Zero extra services to run, which is perfect for a lab. In /etc/kamailio/kamctlrc:

SIP_DOMAIN=52.91.109.231 DBENGINE=DBTEXT DB_PATH="/etc/kamailio/dbtext" 

I used the public IP as the SIP domain so the softphones don't need any DNS — they just point at the IP.

Step 6 — Turn on the features in kamailio.cfg

Kamailio's default config is modular; you switch features on with #!define flags at the very top of /etc/kamailio/kamailio.cfg:

#!define WITH_AUTH #!define WITH_USRLOCDB #!define WITH_NAT #!define WITH_RTPENGINE #!define DBURL "text:///etc/kamailio/dbtext" 
  • WITH_AUTH — require a password to register
  • WITH_USRLOCDB — remember where users registered
  • WITH_NAT + WITH_RTPENGINE — enable NAT handling and route media through rtpengine
  • DBURL — use the flat-file DB

I also added one listen line so Kamailio advertises its public IP in SIP headers (otherwise phones try to reply to the unreachable private IP):

listen=udp:172.31.42.238:5060 advertise 52.91.109.231:5060 

The very top of my kamailio.cfg. These five define lines are the switches that turn on auth, NAT handling, and the rtpengine integration. They must appear exactly once — more on that mistake below.

Top of kamailio.cfg showing the WITH_AUTH, WITH_NAT, WITH_RTPENGINE defines

Step 7 — Create the two users

sudo kamdbctl create # y to presence tables, n to extra tables sudo kamctl add 1001 test1001 sudo kamctl add 1002 test1002 sudo kamctl db show subscriber 

1001 is my phone, 1002 is my laptop. The db show listed both with hashed passwords, so I knew they were stored.

Both users stored in the database. You can see 1001 and 1002, each with a hashed password — Kamailio never keeps the plaintext.

kamctl db show subscriber output listing users 1001 and 1002 with hashed passwords

The three mistakes I had to fix (this is where I learned the most)

Here's the honest part. Kamailio refused to start three separate times, and each failure taught me something.

Mistake 1 — Duplicate defines

I'd added the #!define lines in the editor, then re-added them with a command. Result:

CRITICAL: pp_define(): already defined: WITH_AUTH 

Kamailio rejects a flag defined twice. I deleted the duplicate block and kept exactly one set.

Takeaway: kamailio -c -f /etc/kamailio/kamailio.cfg checks syntax before you start the service. Use it every time.

Mistake 2 — The database module wasn't loaded

Config parsed fine, but the service still died. Running it in the foreground (sudo kamailio -f ... -D -E) finally showed the real error:

ERROR: db_bind_mod(): Module db_text not found. Missing loadmodule? 

I'd told Kamailio to use DBTEXT but never loaded the db_text module. The default config only loads the MySQL one. Fix:

sudo sed -i '/^loadmodule "jsonrpcs.so"/i loadmodule "db_text.so"' /etc/kamailio/kamailio.cfg 

Takeaway: systemctl status hides the real error. Running the daemon in the foreground shows exactly why it won't start. That one trick unblocked me twice.

Mistake 3 — File permissions

The phone was now reaching the server (I could see REGISTER requests in the log — huge milestone), but every one failed with:

db_text: failed to open file [/etc/kamailio/dbtext/subscriber] auth_db: get_ha1(): failed to query database 

The subscriber file was owned by root with -rw------- permissions, but the Kamailio service runs as the kamailio user, which couldn't read it. kamctl had created it as root with locked-down perms because it holds password hashes. Fix:

sudo chown -R kamailio:kamailio /etc/kamailio/dbtext sudo chmod 640 /etc/kamailio/dbtext/subscriber sudo systemctl restart kamailio 

Takeaway: "it works when I run it with sudo but not as a service" almost always means a permission or user mismatch.

Step 8 — Register the softphones

Phone (1001): I used Zoiper (Linphone works too). On the account setup screen that asks about an authentication username / outbound proxy, I left it unchecked and tapped Skip — my auth username is the same as my SIP username. The account settings:

Field Value
Username 1001
Password test1001
Domain / host 52.91.109.231
Transport UDP

A green checkmark and "Account is ready" meant it had registered to the cloud server.

The moment it clicked: the green check and "Account is ready" mean my phone successfully registered to the server running in AWS.

Zoiper on the phone showing 1001@52.91.109.231 registered and Account is ready

Laptop (1002): I installed MicroSIP on Windows (lightweight, simple) and added an account with the same shape but username 1002 / password test1002. It showed "Online" at the bottom.

The laptop side, registered as 1002. "Online" here is the Windows equivalent of the phone's green check — both endpoints are now live on the server.

MicroSIP on Windows showing the 1002 account online

Step 9 — Make the call

I kept the live log running on the server the whole time:

sudo journalctl -u kamailio -f 

Then I dialed 1002 from my phone. The laptop rang, I answered, and the log showed the money line:

INVITE ... code=200;reason=OK;src_user=1001;...;dst_user=1002 BYE ... code=200;reason=OK ... 

A 200 OK on the INVITE means the call connected end to end. The BYE with 200 OK means it hung up cleanly. Two-way audio, through my own server in the cloud.

Tip: if the phone and laptop are in the same room, use headphones on at least one of them or you'll get awful echo/feedback as each picks up the other's speaker.

The call connected on the phone — live and counting.

Active call in progress on the phone

And the same call on the laptop side in MicroSIP. Both ends are talking through the cloud server.

Active call in progress on MicroSIP on the laptop

The proof in the server log: the INVITE got a 200 OK (call answered) and the BYE got a 200 OK (clean hang-up). This is exactly what a successful call looks like from Kamailio's side.

journalctl log showing the INVITE with 200 OK and the BYE

Reading the SIP call flow

Once it worked, the log became a great teacher. A successful call looks like this:

  1. INVITE — caller asks to start a call
  2. 100 Trying — server acknowledges it's working on it
  3. 180 Ringing — the other phone is ringing
  4. 200 OK — the callee answered
  5. ACK — caller confirms, media starts flowing via rtpengine
  6. BYE / 200 OK — someone hangs up

I also saw a few 486 Busy Here responses — those were just calls I declined or didn't pick up. Not errors, just normal call outcomes.

What I'd do differently / next steps

  • Try SIP over TCP or TLS. Some networks block UDP 5060; TCP got through reliably, and TLS would encrypt the signaling.
  • Tighten the Security Group. All-UDP-open is fine for an afternoon, not for anything persistent.
  • Add a third user and try a group scenario, or point a desk phone at it.
  • Watch the raw packets with ngrep -d any port 5060 or sngrep to see the actual SIP messages, not just the accounting log.

The whole thing in one glance

Piece What it does Where it runs
Kamailio SIP registrar + proxy (signaling) EC2
rtpengine relays the voice (RTP), fixes NAT EC2
DBTEXT stores users 1001 / 1002 EC2 (flat files)
Zoiper (1001) softphone with a mic my phone
MicroSIP (1002) softphone with a mic my laptop

One cloud server, two phones, a real call. The hard parts weren't the concepts — they were the duplicate config line, the unloaded module, and the file permission. Each one failed loudly, and reading the error (especially in the foreground) was always the way out.

Don't forget: when you're finished, stop or terminate the EC2 instance so it isn't billing or sitting open to the internet.