fix(proxy-engine): fix inbound route browser ringing and provider-facing SDP advertisement while preventing RTP port exhaustion
This commit is contained in:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user