-
Notifications
You must be signed in to change notification settings - Fork 35
Description
While integrating rsipstack as a SIP UAC for an outbound calling system, I encountered two severe issues that prevent correct CANCEL behavior and make client-side dialog management unreliable:
ClientInviteDialog produces malformed CANCEL requests (To: ...;tag=), breaking SIP parsing on servers such as OpenSIPS.
DialogLayer cannot retrieve client-side dialogs after 1xx responses, because the DialogId used internally diverges from the updated DialogId seen in DialogState events.
Both issues prevent the application from cancelling early-stage calls (before 18x) and even cancelling calls after 183/180 reliably.
The details and full reproduction follow.
- Malformed CANCEL when to_tag is empty (;tag=)
When calling ClientInviteDialog::cancel(), the current implementation modifies the To header:
cancel_request
.to_header_mut()?
.mut_tag(self.id().to_tag.clone().into())?;
If self.id().to_tag is empty (normal for Calling/Trying phases), this produces:
To: sip:...;tag=
This is syntactically invalid SIP.
OpenSIPS (and others) reject such CANCEL messages:
ERROR:core:parse_to_param: unexpected char [C] in status 13: <<;tag=#15#012>>
ERROR:core:_parse_to: unexpected end of header in state 13
ERROR:core:get_hdr_field: bad to header
ERROR:core:parse_headers: bad header field
ERROR:core:receive_msg: Unable to parse msg received from [IP]
RFC 3261 §9.1
CANCEL must reuse the same To as the original INVITE.
That means no To-tag should be added, even if a 1xx with tag was previously received.
In other words, CANCEL should use:
To: sip:[email protected]
not:
To: sip:[email protected];tag=gKxxxx # not required
To: sip:[email protected];tag= # invalid
Minimal correct fix
Remove the modification of To when building CANCEL:
// REMOVE THIS (causes malformed CANCELs)
// cancel_request
// .to_header_mut()?
// .mut_tag(self.id().to_tag.clone().into())?;
// CANCEL must use To-header from initial INVITE
After removing this line, CANCELs become RFC-compatible and OpenSIPS accepts them in all phases, including immediately after 100 Trying.
- Client-side dialogs cannot be retrieved via DialogLayer::get_dialog
For server-side INVITE dialogs, DialogLayer::get_or_create_server_invite registers dialogs in:
self.inner.dialogs: HashMap<DialogId, Dialog>
But for client-side INVITE dialogs (ClientInviteDialog), external code cannot reliably retrieve them.
Why?
Because client dialog registration looks like this (from do_invite):
let (dialog, tx) = self.create_client_invite_dialog(...)?;
let id = dialog.id();
self.inner.dialogs.write().unwrap().insert(
id.clone(),
Dialog::ClientInvite(dialog.clone()),
);
At this moment:
DialogId.to_tag == "" (empty)
Later, during process_invite, a 1xx or 2xx response updates the remote tag:
self.update_remote_tag(tag);
So DialogId seen externally via DialogState::Early(id, ...) becomes:
call_id: ...
from_tag: ...
to_tag: "gKxxxxx"
But the key in dialogs stays:
call_id: ...
from_tag: ...
to_tag: "" # never updated
Result: DialogLayer::get_dialog(&DialogId) never finds the client dialog.
Example log:
DialogState::Early DialogId { ..., to_tag: "gK04b9f41b" }
[end] ... dialog_id = DialogId { to_tag: "gK04b9f41b" }
hangup_by_call_id: dialog not found in dialog_layer for DialogId { to_tag: "gK04b9f41b" }
This makes it impossible for application code to cancel calls externally (Dialog::ClientInvite(d).cancel()), even after 183/180.
Workaround (not ideal)
I must:
store the initial DialogId from the Calling state (where to_tag == ""),
and never update it during Early/Confirmed.
This works only because the dialog is stored in dialogs under the initial DialogId.
Why this is problematic
DialogId is supposed to uniquely identify a dialog; users expect DialogState to contain the correct DialogId.
After 18x, the SIP dialog is legally identified with the new to_tag.
But DialogLayer still indexes dialogs by the old DialogId (without tag).
This prevents any external dialog manipulation (cancel/bye) via the public API.
Suggested improvements
Fix 1 — Do not modify To header in CANCEL
(small, safe, RFC-correct)
Remove:
cancel_request
.to_header_mut()?
.mut_tag(self.id().to_tag.clone().into())?;
CANCEL should reuse the To-header from the original INVITE.
Fix 2 — Stable dialog lookup for UAC dialogs
One of these options should be adopted:
Option A (minimal): Document behavior
Document that DialogId for UAC dialogs must be taken from DialogState::Calling and not overwritten by Early/Confirmed.
And that only the initial DialogId can be used with DialogLayer::get_dialog.
Option B (better): Make DialogId stable
Either:
change the HashMap key to (Call-ID, from-tag) only, ignoring to_tag,
or
update the key inside dialogs when update_remote_tag() is invoked.
Option C (best): Add a dedicated lookup API
E.g.:
impl DialogLayer {
pub fn get_uac_dialog_by_call_id(&self, call_id: &str) -> Option;
}
This would allow external cancellation logic without relying on the evolving DialogId.
Conclusion
These two issues combined mean:
CANCEL is malformed in early phases → rejected by SIP servers.
External cancellation (d.cancel()) does not work because UAC dialogs are not discoverable after 18x.
After applying:
CANCEL To-header fix, and
Stable ID or proper dialog lookup,
client-side INVITE cancellation becomes fully functional and RFC-compliant.
Happy to submit a PR if maintainers approve the approach.