Voice Gateway Setup
The voice gateway (cdma-voice-gw) is a separate process that bridges CDMA voice calls to SIP/PSTN networks. It handles SIP signaling, RTP media, and EVRC↔G.711 transcoding.
The gateway supports both outbound (handset dials out through your SIP trunk) and inbound (trunk dials a subscriber on your network). See Inbound calls for the inbound side.
Architecture
CDMA Handset
↕ air interface (EVRC)
BTS / BSC / MSC media + policy
↕ A1 call control + A2p RTP bearer on the cellular side
cdma-voice-gw
↕ SIP/RTP (G.711)
SIP Trunk / PSTN
The cellular side separates call control from bearer media: MSC owns call policy, media orchestration, gateway control, and preemption over A1/A2p, while BSC executes the radio leg. The gateway remains the external SIP/RTP bridge for non-local calls.
Ubuntu libre/re Install
The gateway links against Baresip re/libre for SIP signaling. Some Ubuntu libre-dev packages are too old for SIP REGISTER support, so build upstream re before building cdma-voice-gw:
sudo apt install build-essential cmake pkg-config libssl-dev git
git clone https://github.com/baresip/re.git
cd re
git checkout v4.6.0
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local
cmake --build build -j"$(nproc)"
sudo cmake --install build
sudo ldconfig
MSC Configuration
Enable the gateway in config/msc.json:
{
"voice": {
"gateway": {
"enabled": true,
"endpoint": "http://127.0.0.1:17015",
"fallback_to_wav": true
}
}
}
voice.gateway.enabled toggles
MSC-owned routing of non-local calls through the gateway.
voice.gateway.endpoint is the
gRPC address; voice.gateway.fallback_to_wav
plays the configured WAV instead of failing when the gateway is unreachable.
Other MSC voice settings that affect SIP behaviour:
sip_ringback_disable (outbound
ringback source — see Ringback below),
inbound_sip_msc_ringback (inbound
ringback source),
generate_ringback and
send_tones_alert (caller-handset
ringback),
failure_tone_duration_ms (busy-tone
duration on call failure),
page_retry_cooldown_ms /
page_retry_max_duration_ms (MT page
hunt window).
What the caller hears
When a call fails (number not in service, denied by the trunk, etc.) the caller hears a busy tone for failure_tone_duration_ms (default 3 seconds), then the call ends with the handset showing the failure reason. The MSC tells the handset to play the tone locally — no audio is synthesized on our side.
When the trunk is unreachable entirely (gateway down, DNS failure, etc.) and gateway.fallback_to_wav is true, the caller instead hears the configured voice.wav_file after the normal answer delay. This is intended as a “the network ate your call” placeholder rather than a real ring-and-pickup experience.
Ringback
While a call is being set up, someone has to play the ringback the caller hears. Four flags decide who — two for the SIP side, two for the handset side. See each in the msc.json reference for the authoritative defaults and field-level descriptions.
SIP side — what the SIP caller hears
sip_ringback_disable— on outbound calls,false(default) makes your network play ringback toward the caller;truedefers to the SIP trunk’s early-media ringback.inbound_sip_msc_ringback— on inbound calls,true(default) makes your network play ringback toward the SIP caller (subscriber’s custom ringtone if uploaded, otherwise default tone);falsedefers to the trunk.
Handset side — what the calling handset hears
generate_ringback—true(default) streams ringback audio to the calling handset over the call’s audio channel.send_tones_alert—false(default). Whentrue, instructs the calling handset to play its own ringback tone. Independent ofgenerate_ringback; either, both, or neither are valid.
For most handsets, the defaults produce the most consistent experience. send_tones_alert is mainly useful as a legacy option for users who specifically want the handset to draw its own ringback tone. If you’re not sure, leave everything at the defaults.
Minimum Working Config
The smallest config that actually places an outbound call through a real SIP trunk. Substitute your provider’s hostname, username/DID, and env var name. Save as config/voice-gw.local.json:
{
"grpc": {
"listen_addr": "127.0.0.1:17015"
},
"sip": {
"listen_addr": "10.30.4.105:5062",
"transport": "udp",
"request_uri_template": "sip:{called}@sip.example.com:5060",
"from_domain": "sip.example.com",
"caller_id_override": "+14805551212",
"keepalive_interval_secs": 25,
"auth": {
"username": "your-sip-username",
"password_env": "VOICE_GW_SIP_PASSWORD"
},
"registration": {
"enabled": true,
"registrar_uri": "sip:sip.example.com:5060",
"expires_secs": 300
}
},
"rtp": {
"listen_addr": "10.30.4.105",
"port_range": [17100, 17200],
"preferred_codecs": ["PCMU", "PCMA"]
},
"nat": {
"mode": "stun_latch",
"stun_server": "stun.l.google.com:19302"
}
}
The five fields you must change for your environment:
sip.listen_addr— concrete local IP:port. Libre rejects wildcard binds (0.0.0.0,::); the gateway fails validation at startup. Use the LAN address of the host running the gateway.sip.from_domain— must match the domain your trunk expects in the From URI. Mismatches typically come back as403 Forbiddenor407 Proxy Authentication Required.sip.request_uri_template— provider-supplied SIP URI. The literal{called}is replaced with the dialed digits at INVITE time.sip.caller_id_override— your DID in E.164 (e.g.+14805551212). Whennull, the handset’s MDN is used as the From user, which most trunks reject as an un-owned number.sip.auth.password_env— name of the env var holding the SIP password. Prefer this overauth.passwordso secrets never live in the JSON file. Setting both is a validation error.
The voice-gw.json reference lists every available field.
Gateway Configuration
Field-level descriptions and defaults for every voice-gateway setting live in
the voice-gw.json reference.
This section covers the operational guidance that doesn’t fit in a reference
table — how to choose values, common deployment patterns, and gotchas.
SIP
The sip section is where most
deployment-specific values live. A few non-obvious points:
auth.password_envandauth.passwordare mutually exclusive — set exactly one. Withpassword_envthe gateway readsstd::env::var(<name>)at startup; if the var is unset, validation fails before any SIP traffic. Preferpassword_envso secrets never live in JSON.auth.usernameis the SIP digest username your provider issues — some carriers use a tech-prefix or account ID rather than the DID. If it’s not the same as the DID, the trunk’s401will point at the wrong field.caller_id_overrideshould be a DID you actually own on the trunk (E.164, e.g.+14805551212). Whennull, the handset’s MDN is used as the From user, which most public trunks reject.keepalive_interval_secsrequiresregistration.enabled=true— the gateway refuses to start otherwise.
RTP and NAT
Pick a configuration based on where the gateway runs relative to the public internet:
- Public IP on the host →
nat.mode = "disabled",rtp.listen_addr= the public IP (or0.0.0.0plusrtp.advertise_addr). - Behind NAT, known public IP →
nat.mode = "disabled",rtp.listen_addr= LAN IP,rtp.advertise_addr= public IP. - Behind NAT, dynamic public IP →
nat.mode = "stun_latch",rtp.listen_addr= LAN IP,rtp.advertise_addr=null(STUN fills it in).
For STUN, set rtp.listen_addr to your
actual LAN IP rather than 0.0.0.0: STUN binds to the literal RTP socket and
asks the STUN server what (IP, port) it sees, and that mapping is what goes
into outgoing SDP. A wildcard bind discovers whichever interface the OS
happened to source the packet from, which is fragile. Google’s public server
(stun.l.google.com:19302) is fine for testing; in production, use whatever
STUN your provider publishes.
At call time: STUN binding request from the chosen RTP socket → external
(IP, port) placed in outgoing SDP → first inbound RTP source from the SIP
peer is “latched” as the return path (handles asymmetric NAT mappings the
SDP couldn’t predict).
Calls and jitter buffer
See calls for concurrency and
timeout fields, and
jitter_buffer_ms for the
adaptive (40–80 ms) inbound RTP reorder buffer. EVRC silence frames are
inserted when packets arrive late.
Running the Gateway
The conventional name for your real config is config/voice-gw.local.json (gitignored alongside voice-gw.example.json). Point the gateway at it and supply the SIP password via the env var named in auth.password_env:
# Inline:
VOICE_GW_SIP_PASSWORD='...' \
cargo run --release -p cdma-voice-gw -- --config config/voice-gw.local.json
# Or sourced from a file kept out of git:
set -a
source ~/.config/1xbts/voice-gw.env # contains VOICE_GW_SIP_PASSWORD=...
set +a
cargo run --release -p cdma-voice-gw -- --config config/voice-gw.local.json
The gateway validates the config at startup — missing env var, wildcard SIP bind, mismatched password/password_env, or a 0.0.0.0 RTP bind without advertise_addr will all fail before any SIP traffic. The MSC runtime connects automatically when voice.gateway.enabled is true.
SIP Trunk Providers
The gateway works with any standard SIP trunk that accepts UDP/TCP/TLS digest-auth INVITEs. The Minimum Working Config above is the typical config for any public PSTN provider — registration enabled, digest auth via password_env, caller_id_override set to a DID you own on the trunk.
Local SIP server (no auth)
For development against a local FreeSWITCH/Asterisk on the LAN, you can skip auth and registration entirely:
{
"sip": {
"listen_addr": "192.168.1.50:5060",
"request_uri_template": "sip:{called}@192.168.1.200:5060",
"from_domain": "192.168.1.50",
"auth": { "username": "" },
"registration": { "enabled": false }
},
"rtp": {
"listen_addr": "192.168.1.50",
"port_range": [17100, 17200]
},
"nat": { "mode": "disabled" }
}
Inbound calls
When the gateway is reachable from your SIP trunk, an inbound INVITE is matched to a subscriber by comparing the Request-URI user portion (e.g. 4805551212) against subscriber.phone_number in the HLR. A leading + is stripped before matching, so +14805551212 and 14805551212 both work.
If a subscriber matches and is currently registered, the MSC pages the handset and bridges audio between SIP RTP and the bearer when the subscriber answers.
Reject mapping
| Trunk sees | Why |
|---|---|
404 Not Found | No subscriber has that DID, or the Request-URI user is not all digits |
480 Temporarily Unavailable | Subscriber exists but isn’t currently registered / paging timed out |
486 Busy Here | Subscriber is already on another call |
488 Not Acceptable Here | Trunk’s SDP didn’t offer a G.711 codec |
487 Request Terminated | Caller hung up before the subscriber answered (gateway sends this on CANCEL) |
408 Request Timeout | Gateway didn’t get a decision back in sip.inbound_decision_timeout_ms |
503 Service Unavailable | RTP socket couldn’t be allocated, MSC unreachable, or other gateway-side error |
Trust model
There is no authentication on inbound INVITEs in v1 — anyone who can reach the SIP listen port can attempt to call any subscriber on your network. Bind sip.listen_addr to a private interface, or firewall the port to your trunk provider’s source addresses.
If you publish the same SIP listen port to the public internet without filtering, expect drive-by INVITE traffic.
Transcoding
The gateway transcodes between EVRC (air interface) and G.711 (SIP/RTP):
| Direction | Pipeline |
|---|---|
| Handset → PSTN | EVRC primary bits → EVRC decode → PCM → G.711 encode → RTP |
| PSTN → Handset | RTP → G.711 decode → PCM → EVRC encode → EVRC primary bits |
Media frames on the cellular bearer are exchanged as raw EVRC codec payloads (without MuxPDU headers). The BSC adds or strips the MuxPDU MM=0 header for the Abis and air-interface side, and the MSC forwards non-local call media between A2p and the gateway. A2p uses RTP/UDP per circuit and the EVRC RTP payload format from RFC 3558. Erasure frames with rate_bps=0 are skipped in the gateway media path.
Troubleshooting
| Symptom | Check |
|---|---|
| Gateway not connecting | MSC voice.gateway.endpoint matches gateway grpc.listen_addr |
| SIP INVITE rejected | Auth credentials, from_domain, trunk provider settings |
| No audio | RTP listen address reachable from SIP peer, firewall rules on RTP port range |
| One-way audio | NAT config — try stun_latch mode, or set advertise_addr |
| Choppy audio | Jitter buffer too small, network latency to SIP peer |
| Calls fall back to WAV | Gateway process not running, or fallback_to_wav is true |
| DNS resolution failure | Check request_uri_template domain resolves, try IP directly |