resip/dum: correlate the 2xx ACK to mInvite200 (fix endless 200 OK retransmission on overlapping/glare re-INVITE)
Summary
InviteSession decided whether an inbound ACK should stop retransmission of an outstanding 200 OK to an INVITE/re-INVITE by comparing mLastRemoteSessionModification's CSeq against the ACK's CSeq.
The previous logic was effectively:
lastRemote.CSeq > ack.CSeq means drop as stale.
That is the wrong correlator. The response being retransmitted is held in mInvite200, and RFC 3261 sections 13.2.2.4 and 17.1.1.3 require the ACK for a 2xx INVITE response to carry the same CSeq sequence number as the INVITE/2xx it acknowledges.
This PR correlates the ACK against mInvite200, applies the same key to the Retransmit200 and WaitForAck timers, and adds two defensive checks.
Symptoms
* A UAS-sent 200 OK to a re-INVITE retransmits forever, using T1, 2*T1, and then capped at T2, even though the peer's ACK is received and routed to the dialog.
* No onAckReceived callback is triggered; the log shows either no clear, or dropped stale ACK.
* After about 32 seconds, Timer H or the ACK wait timer expires and the call is torn down.
* This was observed in production against an SBC that hairpins a B2BUA call and emits bidirectional session-timer refresh re-INVITEs, causing re-INVITE glare.
Root cause
The clear-on-ACK logic in dispatchConnected, dispatchSentReinvite, dispatchReceivedReinviteSentOffer, dispatchWaitingToHangup, dispatchOthers, and the WaitForAck timer were keyed on mLastRemoteSessionModification.
That value can diverge from mInvite200 whenever:
1. Re-INVITE glare occurs.
We sent our own re-INVITE, so the state is now SentReinvite or a glare-related state, while we are still waiting for the ACK to a 2xx response we sent for the remote's re-INVITE.
2. Overlapping remote session-modification traffic occurs.
For example, the remote sends re-INVITE B with CSeq N+1 before the ACK for our 200 OK to re-INVITE A with CSeq N arrives.
dispatchConnected overwrites mLastRemoteSessionModification with B. When A's ACK with CSeq N arrives, the old guard evaluates N+1 > N as true, treats the ACK as stale, logs dropped stale ACK, and the retransmission never stops. The WaitForAck failsafe is armed with sequence N but is compared against CSeq N+1, so it also fails to fire.
3. A peer reuses, or does not strictly increase, the in-dialog CSeq.
Minimal reproducible sequence:
Remote sends INVITE with CSeq N:
re-INVITE A
DUM sends 200 OK with CSeq N:
starts Retransmit200 and WaitForAck with seq N
mInvite200 is 200(A)
Remote sends INVITE with CSeq N+1:
re-INVITE B
mLastRemoteSessionModification is updated to B
DUM sends 200 OK with CSeq N+1.
Remote sends ACK with CSeq N:
ACK for A
The old logic evaluates N+1 > N as true, treats the ACK as stale, and 200(A) retransmits until Timer H. The call then fails.
Fix
Add a new helper:
InviteSession::isAckForCurrent200(const SipMessage&) const
The helper returns true only if all of the following are true:
1. The message is an ACK request.
2. A 2xx retransmission is outstanding.
3. mCurrentRetransmit200 is non-zero.
4. mInvite200 is valid.
5. The ACK's CSeq sequence number equals mInvite200's CSeq sequence number.
Replace the stale guard with !isAckForCurrent200(msg) at all relevant ACK sites.
Each branch keeps its existing side effects, including clearing the retransmission state, calling onAckReceived, delivering answer-in-ACK, or sending BYE.
dispatchOthers is the universal fallback for re-INVITE and glare states, so this also covers states that do not special-case OnAck.
Key the timers off mInvite200.
WaitForAck and startRetransmit200Timer now use mInvite200's CSeq instead of mLastRemoteSessionModification's CSeq. This ensures the timers identify the specific 2xx response and continue to work even if mLastRemoteSessionModification is overwritten by later remote session-modification traffic.
Suppress stale Retransmit200 timer events.
The Retransmit200 branch now retransmits only when timeout.seq() still matches the current mInvite200's CSeq. This prevents a leftover timer for a superseded mInvite200 from retransmitting a replaced response.
The benign post-ACK case, where mCurrentRetransmit200 is zero, stays silent as before. Only genuine stale-chain suppression is logged.
The stale ACK log message is updated to:
dropped stale or duplicate ACK
Why this is safe
mInvite200 is constructor-initialised with new SipMessage, and all startRetransmit200Timer callers populate it via makeResponse(*mInvite200, ...) immediately before arming the timer. There is no null dereference.
Every mInvite200 mutation is paired with startRetransmit200Timer, so the live retransmit chain always matches and is never suppressed.
States whose ACK branch does more than clear are reached only with mCurrentRetransmit200 != 0.
WaitingToHangup and UAS_WaitingToHangup are entered only inside if(mCurrentRetransmit200), so BYE and termination still fire.
ReceivedReinviteSentOffer is entered through provideOffer(), which sends mInvite200 and starts the timer, so answer-in-ACK delivery still fires.
ServerInviteSession::dispatchAccepted, ServerInviteSession::dispatchWaitingToOffer, and ServerInviteSession::dispatchWaitingToHangup clear mCurrentRetransmit200 unconditionally. They do not use the fragile correlator and are not touched. The UAS initial-INVITE and queued-offer paths are unchanged.
This PR does not alter the main InviteSession state transitions.
The behavioral delta is limited to:
1. The ACK correlation bug fix.
2. Duplicate or stale ACKs no longer re-fire onAckReceived for the outstanding 2xx response.
3. Stale Retransmit200 timer events can no longer retransmit a replaced mInvite200.
Files changed
* resip/dum/InviteSession.cxx
* resip/dum/InviteSession.hxx
Testing
Verified by source audit of every ACK dispatch path and the Retransmit200 / WaitForAck timers. The new helper and timer guards compile clean under -std=c++20 -Wall -Wextra -Werror.
Recommended tests:
1. Run the existing DUM test suite under resip/dum/test.
2. Add a regression test for interleaved or glare re-INVITE.
The test should cover this sequence:
DUM sends 200 OK to re-INVITE A and starts Retransmit200 / WaitForAck.
mLastRemoteSessionModification advances because of re-INVITE B before ACK A arrives.
ACK A is received.
Assert that mCurrentRetransmit200 is cleared, onAckReceived is called once, and no further 200 OK is sent.
3. Add a stale Retransmit200 test.
The test should deliver a Retransmit200 timeout where timeout.seq() no longer matches the current mInvite200 CSeq.
Assert that no retransmission is sent.