Skip to content

sip: redirect RTP destination on re-INVITE port change#728

Open
VictorUvarov wants to merge 11 commits into
livekit:mainfrom
VictorUvarov:vu/issue-661
Open

sip: redirect RTP destination on re-INVITE port change#728
VictorUvarov wants to merge 11 commits into
livekit:mainfrom
VictorUvarov:vu/issue-661

Conversation

@VictorUvarov

Copy link
Copy Markdown

Problem

When a SIP carrier sends a mid-call re-INVITE with a new RTP port (e.g. after a NAT rebind or media relay failover), LiveKit SIP was responding 200 OK but continuing to send RTP to the stale address — violating RFC 3264 §8, which requires the answerer to direct media to the address/port in the new offer.

Closes #661

What changed

pkg/sip/media_port.go: two new methods on MediaPort:

  • UpdateRemote: atomically redirects the RTP destination.
  • RemoteAddr: read accessor, used in tests.

pkg/sip/inbound.go : both re-INVITE paths now parse the SDP body and call UpdateRemote before responding 200 OK.

Tests

pkg/sip/media_port_test.goTestMediaPortUpdateRemote covers thehappy path, the zero-value no-op, and the unspecified-addr no-op. pkg/sip/signaling_test.goTestReinvite extended:

  • inbound/normal: asserts RemoteAddr() changes after a re-INVITE.
  • outbound/normal: same for the outbound path.
  • inbound/no_body: asserts a body-less re-INVITE leaves RemoteAddr() unchanged.

Unit tests

o test ./pkg/sip/... -run 'TestMediaPortUpdateRemote|TestReinvite/inbound/normal|TestReinvite/inbound/no_body|TestReinvite/outbound' -v  -count=1

Integration tests

go test ./pkg/sip/... -count=1

Thought process summary

The root cause was a one-liner gap: the re-INVITE handler called AcceptAsKeepAlive (200 OK) but never forwarded the new c=/m= address to the RTP layer. The SDP body already contained the new addr+port as desc.Addr after ParseWith — nothing needed to be renegotiated, just the destination pointer swapped.

The tricky edge case was RFC 3264 §8.4 call-hold, where c=0.0.0.0 signals hold and netip.MustParseAddrPort("0.0.0.0:N").IsValid() returns true. A plain IsValid() guard would have let hold re-INVITEs overwrite the real destination with the zero address. The !addr.Addr().IsUnspecified() check inside UpdateRemote blocks that.

When a carrier sends a re-INVITE with a new SDP offer containing a different
RTP destination address/port, update the media port to send packets to the new
destination. This fixes mid-call carrier port changes which were previously
ignored.

The fix parses the re-INVITE SDP offer body and calls UpdateRemote() to change
the RTP destination before accepting the re-INVITE. Parsing errors are logged
but don't prevent accepting the re-INVITE.
…INVITE test

- In MediaPort.UpdateRemote(), add !addr.Addr().IsUnspecified() guard to reject
  RFC 3264 §8.4 call-hold form (c=0.0.0.0 with non-zero port). netip.AddrPort
  with unspecified address is IsValid() == true, so prior guard missed it.

- Add TestMediaPortUpdateRemote case verifying 0.0.0.0:N is a no-op.

- Add TestReinvite/inbound/no_body subtest: verify a body-less re-INVITE leaves
  RemoteAddr unchanged (guards against offer-in-ACK / no-SDP-body cases).
- Add sdpBodyFromRequest helper to guard updateRemoteFromSDP against
  non-SDP content types; avoids spurious warn log on e.g. application/json
- Derive expected RemoteAddr in tests from newOffer.Addr instead of a
  hardcoded string literal
- Add outbound/no_body test symmetric to inbound/no_body; use
  call.NewRequest(INVITE) in both no_body cases for a truly body-less
  re-INVITE (Invite(nil) silently generates an SDP offer)
@VictorUvarov VictorUvarov requested a review from a team as a code owner June 21, 2026 20:18
@CLAassistant

CLAassistant commented Jun 21, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

udpConn.SetDst logged a nil *netip.AddrPort as "prev" on the first call
(before any destination was stored). The logger called .String() on the
nil pointer, causing a panic. Replace with the zero value netip.AddrPort{}.
Same class of bug as SetDst: udpConn.Read logged a nil *netip.AddrPort
as "prev" on the first packet received. Replace with netip.AddrPort{}.
req.ContentType() returns *ContentTypeHeader (a Stringer). When there is
no body the header is nil; fmt calls String() on the typed nil pointer,
causing a recovered panic that tparse reports as PANIC.

Drop content-type from the reinvite log calls — content-length already
indicates whether a body is present.
@codecov

codecov Bot commented Jun 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.41%. Comparing base (0460b40) to head (60cb302).
⚠️ Report is 313 commits behind head on main.

Files with missing lines Patch % Lines
pkg/sip/inbound.go 75.00% 3 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #728      +/-   ##
==========================================
+ Coverage   65.25%   66.41%   +1.15%     
==========================================
  Files          51       41      -10     
  Lines        6588     7625    +1037     
==========================================
+ Hits         4299     5064     +765     
- Misses       1915     2099     +184     
- Partials      374      462      +88     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The "setting media destination/source" branch fires when prev is nil
(no previous value). Logging prev as an empty AddrPort was misleading.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SIP service does not redirect RTP to new remote port after re-INVITE changes m= line

2 participants