fix(proxy-engine): fix inbound route browser ringing and provider-facing SDP advertisement while preventing RTP port exhaustion

This commit is contained in:
2026-04-11 18:40:56 +00:00
parent 21ffc1d017
commit 81441e7853
17 changed files with 208 additions and 469 deletions

View File

@@ -20,6 +20,14 @@ use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::UdpSocket;
/// Result of creating an inbound call — carries both the call id and
/// whether browsers should be notified (flows from the matched inbound
/// route's `ring_browsers` flag, or the fallback default).
pub struct InboundCallCreated {
pub call_id: String,
pub ring_browsers: bool,
}
/// Emit a `leg_added` event with full leg information.
/// Free function (not a method) to avoid `&self` borrow conflicts when `self.calls` is borrowed.
fn emit_leg_added_event(tx: &OutTx, call_id: &str, leg: &LegInfo) {
@@ -94,26 +102,6 @@ impl CallManager {
self.sip_index.contains_key(sip_call_id)
}
/// Get an RTP socket for a call's provider leg (used by webrtc_link).
pub fn get_call_provider_rtp_socket(&self, call_id: &str) -> Option<Arc<UdpSocket>> {
let call = self.calls.get(call_id)?;
for leg in call.legs.values() {
if leg.kind == LegKind::SipProvider {
return leg.rtp_socket.clone();
}
}
None
}
/// Get all active call statuses for the dashboard.
pub fn get_all_statuses(&self) -> Vec<serde_json::Value> {
self.calls
.values()
.filter(|c| c.state != CallState::Terminated)
.map(|c| c.to_status_json())
.collect()
}
// -----------------------------------------------------------------------
// SIP message routing
// -----------------------------------------------------------------------
@@ -426,8 +414,8 @@ impl CallManager {
// Find the counterpart leg.
let other_leg = call.legs.values().find(|l| l.id != this_leg_id && l.state != LegState::Terminated);
let (other_addr, other_rtp_port, other_leg_id) = match other_leg {
Some(l) => (l.signaling_addr, l.rtp_port, l.id.clone()),
let (other_addr, other_rtp_port, other_leg_id, other_kind, other_public_ip) = match other_leg {
Some(l) => (l.signaling_addr, l.rtp_port, l.id.clone(), l.kind, l.public_ip.clone()),
None => return false,
};
let forward_to = match other_addr {
@@ -438,8 +426,14 @@ impl CallManager {
let lan_ip = config.proxy.lan_ip.clone();
let lan_port = config.proxy.lan_port;
// Get this leg's RTP port (for SDP rewriting — tell the other side to send RTP here).
let this_rtp_port = call.legs.get(this_leg_id).map(|l| l.rtp_port).unwrap_or(0);
// Pick the IP to advertise to the destination leg. Provider legs face
// the public internet and need `public_ip`; every other leg kind is
// on-LAN (or proxy-internal) and takes `lan_ip`. This rule is applied
// both to the SDP `c=` line and the Record-Route header below.
let advertise_ip: String = match other_kind {
LegKind::SipProvider => other_public_ip.unwrap_or_else(|| lan_ip.clone()),
_ => lan_ip.clone(),
};
// Check if the other leg is a B2BUA leg (has SipLeg for proper dialog mgmt).
let other_has_sip_leg = call.legs.get(&other_leg_id)
@@ -533,10 +527,11 @@ impl CallManager {
// Forward other requests with SDP rewriting.
let mut fwd = msg.clone();
// Rewrite SDP to point the other side to this leg's RTP port
// (so we receive their audio on our socket).
// Rewrite SDP so the destination leg sends RTP to our proxy port
// at an address that is routable from its vantage point
// (public IP for provider legs, LAN IP for everything else).
if fwd.has_sdp_body() {
let (new_body, _) = rewrite_sdp(&fwd.body, &lan_ip, other_rtp_port);
let (new_body, _) = rewrite_sdp(&fwd.body, &advertise_ip, other_rtp_port);
fwd.body = new_body;
fwd.update_content_length();
}
@@ -548,7 +543,8 @@ impl CallManager {
}
}
if fwd.is_dialog_establishing() {
fwd.prepend_header("Record-Route", &format!("<sip:{lan_ip}:{lan_port};lr>"));
// Record-Route must also be routable from the destination leg.
fwd.prepend_header("Record-Route", &format!("<sip:{advertise_ip}:{lan_port};lr>"));
}
let _ = socket.send_to(&fwd.serialize(), forward_to).await;
return true;
@@ -560,15 +556,10 @@ impl CallManager {
let cseq_method = msg.cseq_method().unwrap_or("").to_uppercase();
let mut fwd = msg.clone();
// Rewrite SDP so the forward-to side sends RTP to the correct leg port.
// Rewrite SDP so the forward-to side sends RTP to the correct
// leg port at a routable address (see `advertise_ip` above).
if fwd.has_sdp_body() {
let rewrite_ip = if this_kind == LegKind::SipDevice {
// Response from device → send to provider: use LAN/public IP.
&lan_ip
} else {
&lan_ip
};
let (new_body, _) = rewrite_sdp(&fwd.body, rewrite_ip, other_rtp_port);
let (new_body, _) = rewrite_sdp(&fwd.body, &advertise_ip, other_rtp_port);
fwd.body = new_body;
fwd.update_content_length();
}
@@ -690,7 +681,7 @@ impl CallManager {
rtp_pool: &mut RtpPortPool,
socket: &UdpSocket,
public_ip: Option<&str>,
) -> Option<String> {
) -> Option<InboundCallCreated> {
let call_id = self.next_call_id();
let lan_ip = &config.proxy.lan_ip;
let lan_port = config.proxy.lan_port;
@@ -707,17 +698,41 @@ impl CallManager {
.unwrap_or("")
.to_string();
// Resolve target device.
let device_addr = match self.resolve_first_device(config, registrar) {
// Resolve via the configured inbound routing table. This honors
// user-defined routes from the UI (numberPattern, callerPattern,
// sourceProvider, targets, ringBrowsers). If no route matches, the
// fallback returns an empty `device_ids` and `ring_browsers: true`,
// which preserves pre-routing behavior via the `resolve_first_device`
// fallback below.
//
// TODO: Multi-target inbound fork is not yet implemented.
// - `route.device_ids` beyond the first registered target are ignored.
// - `ring_browsers` is informational only — browsers see a toast but
// do not race the SIP device. First-to-answer-wins requires a
// multi-leg fork + per-leg CANCEL, which is not built yet.
// - `voicemail_box`, `ivr_menu_id`, `no_answer_timeout` are not honored.
let route = config.resolve_inbound_route(provider_id, &called_number, &caller_number);
let ring_browsers = route.ring_browsers;
// Pick the first registered device from the matched targets, or fall
// back to any-registered-device if the route has no resolved targets.
let device_addr = route
.device_ids
.iter()
.find_map(|id| registrar.get_device_contact(id))
.or_else(|| self.resolve_first_device(config, registrar));
let device_addr = match device_addr {
Some(addr) => addr,
None => {
// No device registered → voicemail.
return self
let call_id = self
.route_to_voicemail(
&call_id, invite, from_addr, &caller_number,
provider_id, provider_config, config, rtp_pool, socket, public_ip,
)
.await;
.await?;
return Some(InboundCallCreated { call_id, ring_browsers });
}
};
@@ -781,6 +796,7 @@ impl CallManager {
webrtc_session_id: None,
rtp_socket: Some(provider_rtp.socket.clone()),
rtp_port: provider_rtp.port,
public_ip: public_ip.map(|s| s.to_string()),
remote_media: provider_media,
signaling_addr: Some(from_addr),
metadata: HashMap::new(),
@@ -801,6 +817,7 @@ impl CallManager {
webrtc_session_id: None,
rtp_socket: Some(device_rtp.socket.clone()),
rtp_port: device_rtp.port,
public_ip: None,
remote_media: None, // Learned from device's 200 OK.
signaling_addr: Some(device_addr),
metadata: HashMap::new(),
@@ -844,7 +861,7 @@ impl CallManager {
}
}
Some(call_id)
Some(InboundCallCreated { call_id, ring_browsers })
}
/// Initiate an outbound B2BUA call from the dashboard.
@@ -920,6 +937,7 @@ impl CallManager {
webrtc_session_id: None,
rtp_socket: Some(rtp_alloc.socket.clone()),
rtp_port: rtp_alloc.port,
public_ip: public_ip.map(|s| s.to_string()),
remote_media: None,
signaling_addr: Some(provider_dest),
metadata: HashMap::new(),
@@ -1030,6 +1048,7 @@ impl CallManager {
sip_leg: None,
sip_call_id: Some(device_sip_call_id.clone()),
webrtc_session_id: None,
public_ip: None,
rtp_socket: Some(device_rtp.socket.clone()),
rtp_port: device_rtp.port,
remote_media: device_media,
@@ -1076,6 +1095,7 @@ impl CallManager {
webrtc_session_id: None,
rtp_socket: Some(provider_rtp.socket.clone()),
rtp_port: provider_rtp.port,
public_ip: public_ip.map(|s| s.to_string()),
remote_media: None,
signaling_addr: Some(provider_dest),
metadata: HashMap::new(),
@@ -1114,7 +1134,7 @@ impl CallManager {
public_ip: Option<&str>,
registered_aor: &str,
) -> Option<String> {
let call = self.calls.get(call_id)?;
self.calls.get(call_id)?; // existence check; the call is re-fetched via get_mut below
let lan_ip = &config.proxy.lan_ip;
let lan_port = config.proxy.lan_port;
@@ -1151,6 +1171,7 @@ impl CallManager {
webrtc_session_id: None,
rtp_socket: Some(rtp_alloc.socket.clone()),
rtp_port: rtp_alloc.port,
public_ip: public_ip.map(|s| s.to_string()),
remote_media: None,
signaling_addr: Some(provider_dest),
metadata: HashMap::new(),
@@ -1182,7 +1203,7 @@ impl CallManager {
socket: &UdpSocket,
) -> Option<String> {
let device_addr = registrar.get_device_contact(device_id)?;
let call = self.calls.get(call_id)?;
self.calls.get(call_id)?; // existence check; the call is re-fetched via get_mut below
let lan_ip = &config.proxy.lan_ip;
let lan_port = config.proxy.lan_port;
@@ -1221,6 +1242,7 @@ impl CallManager {
webrtc_session_id: None,
rtp_socket: Some(rtp_alloc.socket.clone()),
rtp_port: rtp_alloc.port,
public_ip: None,
remote_media: None,
signaling_addr: Some(device_addr),
metadata: HashMap::new(),
@@ -1581,6 +1603,7 @@ impl CallManager {
webrtc_session_id: None,
rtp_socket: Some(rtp_alloc.socket.clone()),
rtp_port: rtp_alloc.port,
public_ip: public_ip.map(|s| s.to_string()),
remote_media: Some(provider_media),
signaling_addr: Some(from_addr),
metadata: HashMap::new(),