Skip to content

[Bug] Race Condition: Incorrect CSeq in ACK for re-INVITE when interleaved with mid-dialog request #956

@Tr1ckyy1

Description

@Tr1ckyy1

Environment

  • JsSIP Version: Unknown (Source files manually integrated)
  • Installation Method: I am using the source files (RTCSession.js, Dialog.js, etc.) directly in my project structure (not installed via npm/yarn and not a git fork).

Description
I have encountered a race condition in RTCSession.js / Dialog.js regarding how CSeq numbers are generated for ACK requests confirming a re-INVITE.

If a new request (such as a REFER) is sent immediately after receiving the 200 OK for a re-INVITE, but before the ACK is generated, the Dialog increments its local CSeq counter. When the ACK is subsequently generated by RTCSession, the Dialog uses the new (incremented) CSeq instead of the CSeq associated with the original INVITE.

This causes a protocol mismatch (RFC 3261 Section 17.1.1.3), resulting in the remote party rejecting the ACK or dropping the call.

Reproduction Steps

  1. Establish an active call.
  2. Trigger a re-INVITE.
  3. Immediately trigger a REFER (transfer) upon receiving the 200 OK (simulating a user pressing transfer quickly, or an automated flow).
  4. Observe the SIP packet flow.

Observed Behavior

  1. re-INVITE sent (CSeq: N).
  2. 200 OK received.
  3. REFER sent immediately. Dialog increments local CSeq to N+1.
  4. ACK sent for the re-INVITE. Dialog.js uses current _local_seqnum (N+1).
  5. Result: Remote server drops call because ACK CSeq N+1 does not match INVITE CSeq N.

Expected Behavior
The ACK must always carry the same CSeq number as the INVITE it acknowledges, regardless of other requests sent in the interim.

Suggested Fix
I have patched this locally by passing the specific CSeq from the RTCSession down to the Dialog.

1. RTCSession.js
In _sendReinvite (and _sendUpdate), pass the response CSeq explicitly when sending the ACK.

// RTCSession.js -> _sendReinvite -> onSucceeded

// Old Code:
// this.sendRequest(JsSIP_C.ACK);

// New Code:
this.sendRequest(JsSIP_C.ACK, { cseq: response.cseq });

// Dialog.js

sendRequest(method, options = {}) {

  // specific cseq extraction
  const cseq = options.cseq || null;
  
  // Pass cseq to _createRequest
  const request = this._createRequest(method, extraHeaders, body, cseq);
}

_createRequest(method, extraHeaders, body, cseqOverride = null) {
  if (!this._local_seqnum) {
    this._local_seqnum = Math.floor(Math.random() * 10000);
  }

  // Use override if provided, otherwise default logic
  let cseq;
  if (cseqOverride) {
      cseq = cseqOverride;
  } else {
      cseq = (method === JsSIP_C.CANCEL || method === JsSIP_C.ACK)
          ? this._local_seqnum
          : (this._local_seqnum += 1);
  }

  // ... create SIPMessage.OutgoingRequest using 'cseq' variable ...
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions