Turn a low-end Linux VPS into a pure SIP relay between AgentBridge and a smartphone. The box does no codec handling, no transcoding, no voicemail — it only forwards SIP signalling (Kamailio) and relays RTP (rtpengine, userspace). Both AgentBridge and the phone act as clients of this box, so neither needs open ports at its own location.
A = AgentBridge (home PC, NAT) B = this server C = smartphone (mobile NAT)
A ──REGISTER──► B ◄──INVITE── C signalling (UDP 5060)
A ────RTP──────► B ◄────RTP─── C media relay (UDP 40000-41000, no codecs)
UDP 5060 (SIP) and UDP 40000-41000 (RTP relay) — without this, inbound UDP is
silently dropped and nothing works. This is the #1 pitfall.Registrar sip:<this-host>:5060, Username, shared
Password, and a non-standard ListenPort if the AgentBridge ISP drops inbound UDP
on 5060 (common on home lines — see troubleshooting).Copy the whole folder to the server and run:
sudo bash setup-entrypoint.sh
The script installs kamailio kamailio-extra-modules rtpengine, writes
/etc/kamailio/kamailio.cfg, /etc/rtpengine/rtpengine.conf, enables the services,
auto-generates the shared secret and prints everything you need:
SIP entry point ready
server: 195.20.235.5:5060/udp (SIP signalling)
media relay: UDP 40000-41000 (rtpengine, userspace)
AgentBridge: Username=agent Password=<printed> RegisterExpiry=60
smartphone: proxy 195.20.235.5:5060 (UDP), ANY non-empty password, dial sip:agent@195.20.235.5
REMEMBER: open UDP 5060 and UDP 40000-41000 in the provider firewall
Customize with environment variables (all optional):
SIP_SECRET=mysecret SIP_USER=agent SIP_PUBLIC_IP=1.2.3.4 SIP_PORT=5060 sudo bash setup-entrypoint.sh
apt install kamailio kamailio-extra-modules rtpengine
(kamailio-extra-modules provides the rtpengine module; on Debian 12 there is no
rtpproxy package — rtpengine is the equivalent userspace media relay)./etc/rtpengine/rtpengine.conf: userspace-only (table = -1), media
ports 40000-41000, control socket udp:127.0.0.1:2223. A systemd drop-in empties the
stock ExecStartPre/ExecStartPost (they call rtpengine-iptables-setup, part of a
package we do not need in userspace mode)./etc/kamailio/kamailio.cfg (template in this folder):
REGISTER for the AgentBridge AOR (__USER__) is protected by digest auth
(shared secret, HA1 precomputed, pv_auth_check + www_challenge 401 flow).REGISTER for any other user (the phone) is answered 200 OK but not stored
— the phone registers happily but can never hijack the AgentBridge AOR.INVITE is rtpengine_offer/answer("relay") (media forced through the relay)
and routed via lookup("location") to the AgentBridge registration. NAT media
handling: rtpengine’s endpoint learning must stay ON (the default) — the relay
learns each leg’s real NAT-mapped address from the first RTP packets and forwards
there. Do NOT set endpoint-learning = off, and do NOT rely on fix_nated_sdp():
in Kamailio 5.6 it needs specific flags and empirically did not rewrite the private
SDP addresses, so with learning off the relay forwards to the unreachable private IPs
and the media dies (this bit us live — welcome message not heard, no DTMF feedback).nat_bflag 6 + received_avp/fix_nated_register(): registrations behind NAT keep
the real source address so replies and INVITEs are routed back through the NAT.DTMF INFO (RFC 2976) for the AgentBridge AOR is forwarded straight to the
registration before the generic dialog routing (some clients build an incomplete Route
set and loose_route would drop them with 404) — the PIN gate is the real security.listen=udp:0.0.0.0:<port> advertise <public-ip>:<port>: Via/Contact/Record-Route must
carry the public IP, never the 0.0.0.0 bind address (the global advertised_address
core parameter does NOT affect record_route()).On the machine running AgentBridge, from the TUI:
/sip config set Enabled true
/sip config set Registrar sip:<this-host>:5060
/sip config set Username agent
/sip config set Password <shared secret from the script>
/sip config set ListenPort 6070 # only if the home ISP blocks inbound UDP 5060
/sip config set RegisterExpiry 60 # keep NAT mappings alive on consumer routers
/sip config set Pin 12345
/sip config set Lang it
/sip status shows registered on once the entry point accepted the REGISTER. Every
set persists to appsettings.json and restarts the SIP transport automatically.
(There are also non-interactive config scripts: AgentBridge/scripts/sip-config.bat and
sip-config.sh — same result without the TUI.)
Any standard SIP client (Linphone, Zoiper, …):
phone), any non-empty password, domain/proxy
<this-host> port 5060, transport UDP.sip:<user>@<this-host> → spoken welcome → type the PIN → conversation.AgentBridge/e2e/SipClientTest is a console softphone that dials, answers the PIN with
DTMF, speaks an Italian TTS greeting and captures the agent’s reply:
dotnet run --project e2e\SipClientTest # Windows, defaults to sip:agent@<host>
dotnet run --project e2e/SipClientTest -- sip:agent@<host> 12345 5071 45
Success looks like: answered: Ok, PIN sent, greeting spoken, and the agent log shows
SIP caller said: Ciao, mi senti? → SIP agent replied: … (the agent speaks the reply
back over RTP).
| Symptom | Cause / fix |
|---|---|
| REGISTER reaches the server but never completes (client retransmits) | Provider firewall not opened (UDP 5060), or the home ISP drops inbound UDP 5060 → move AgentBridge to a non-standard ListenPort (e.g. 6070) |
| Calls fail intermittently (408) between successful ones | Home NAT mapping times out before the REGISTER refresh → set RegisterExpiry 60 (the mapping is re-created every minute) |
| Phone registers but the call goes nowhere | The phone is registering with the AgentBridge AOR user — only sip:<user>@… routes to the agent; other registrations are acknowledged but not stored |
| Call connects, no audio | RTP range 40000-41000 blocked in the provider firewall; or a client-side firewall drops the relayed media; or endpoint-learning = off was set on rtpengine (relay forwards to the private SDP IPs) → keep it ON |
| Call rings, phone hears silence, hangs up; next call gets busy (486); 200 OK retransmitted in tcpdump | Two Kamailio bugs (fixed 2026-08-23 in the template): (1) the 200 OK Contact of the NAT’d AgentBridge was not rewritten — ACK/BYE to the private LAN IP were unroutable, so the dialog never stabilized and the softphone held off sending RTP (the media relay never learned its address → welcome dropped). Fix: fix_nated_contact() in onreply_route. (2) lookup("location") ran on in-dialog requests too — their RURI is the Contact URI, not the agent AOR, so ACK/BYE got 404’d. Fix: route in-dialog requests (ACK/BYE/re-INVITE) with loose_route() + t_relay() only; lookup only on new INVITEs. Re-verified end-to-end: welcome + PIN + reply all arrive through the relay. |
| No welcome message heard, no DTMF feedback | Same media-path cause as above (verify with e2e/SipClientTest — it must receive the agent’s speech over RTP) |
kamcmd unavailable on the server |
The ctl module was added to the template after the first deploy — rerun the script or add loadmodule "ctl.so" + modparam("ctl","binrpc","unix:/run/kamailio/kamailio.sock") and restart |